From bd21ff3a6f90f32881867e874ecca812963aacf8 Mon Sep 17 00:00:00 2001 From: Jared Vu Date: Thu, 3 Oct 2024 13:14:56 -0700 Subject: [PATCH] feat(launch-market): Add LineSeries chart (#1105) --- src/components/visx/TimeSeriesChart.tsx | 2 +- src/constants/markets.ts | 46 +++++ src/hooks/useLaunchableMarkets.ts | 25 ++- src/hooks/usePotentialMarkets.tsx | 46 +---- src/pages/vaults/VaultPnlChart.tsx | 4 +- .../MarketDetails/LaunchableMarketDetails.tsx | 7 +- src/views/charts/LaunchableMarketChart.tsx | 170 +++++++++++++++--- 7 files changed, 222 insertions(+), 78 deletions(-) diff --git a/src/components/visx/TimeSeriesChart.tsx b/src/components/visx/TimeSeriesChart.tsx index b54d620a0..4b4010ac9 100644 --- a/src/components/visx/TimeSeriesChart.tsx +++ b/src/components/visx/TimeSeriesChart.tsx @@ -78,7 +78,7 @@ type ElementProps = { onVisibleDataChange?: (data: Datum[]) => void; onZoom?: (_: { zoomDomain: number | undefined }) => void; slotEmpty: React.ReactNode; - children: React.ReactNode; + children?: React.ReactNode; className?: string; }; diff --git a/src/constants/markets.ts b/src/constants/markets.ts index 8d0cb3818..547839682 100644 --- a/src/constants/markets.ts +++ b/src/constants/markets.ts @@ -113,3 +113,49 @@ export enum FundingDirection { export const PREDICTION_MARKET = { TRUMPWIN: 'TRUMPWIN-USD', }; + +// Liquidity Tiers +export const LIQUIDITY_TIERS = { + 0: { + label: 'Large-cap', + initialMarginFraction: 0.05, + maintenanceMarginFraction: 0.03, + impactNotional: 10_000, + }, + 1: { + label: 'Small-cap', + initialMarginFraction: 0.1, + maintenanceMarginFraction: 0.05, + impactNotional: 5_000, + }, + 2: { + label: 'Long-tail', + initialMarginFraction: 0.2, + maintenanceMarginFraction: 0.1, + impactNotional: 2_500, + }, + 3: { + label: 'Safety', + initialMarginFraction: 1, + maintenanceMarginFraction: 0.2, + impactNotional: 2_500, + }, + 4: { + label: 'Isolated', + initialMarginFraction: 0.05, + maintenanceMarginFraction: 0.03, + impactNotional: 2_500, + }, + 5: { + label: 'Mid-cap', + initialMarginFraction: 0.05, + maintenanceMarginFraction: 0.03, + impactNotional: 5_000, + }, + 6: { + label: 'FX', + initialMarginFraction: 0.01, + maintenanceMarginFraction: 0.0005, + impactNotional: 2_500, + }, +}; diff --git a/src/hooks/useLaunchableMarkets.ts b/src/hooks/useLaunchableMarkets.ts index 72372ffe0..9c15b788b 100644 --- a/src/hooks/useLaunchableMarkets.ts +++ b/src/hooks/useLaunchableMarkets.ts @@ -1,10 +1,11 @@ import { useMemo } from 'react'; -import { useQueries } from '@tanstack/react-query'; +import { useQueries, useQuery } from '@tanstack/react-query'; import { shallowEqual } from 'react-redux'; import { MetadataServiceAsset, + MetadataServiceCandlesTimeframes, MetadataServiceInfoResponse, MetadataServicePricesResponse, } from '@/constants/assetMetadata'; @@ -16,6 +17,7 @@ import { getMarketIds } from '@/state/perpetualsSelectors'; import metadataClient from '@/clients/metadataService'; import { getAssetFromMarketId } from '@/lib/assetUtils'; import { getTickSizeDecimalsFromPrice } from '@/lib/numbers'; +import { mapMetadataServiceCandles } from '@/lib/tradingView/utils'; export const useMetadataService = () => { const metadataQuery = useQueries({ @@ -116,3 +118,24 @@ export const useLaunchableMarkets = () => { data: filteredPotentialMarkets, }; }; + +export const useMetadataServiceCandles = ( + asset?: string, + timeframe?: MetadataServiceCandlesTimeframes +) => { + const candlesQuery = useQuery({ + enabled: !!asset && !!timeframe, + queryKey: ['candles', asset, timeframe], + queryFn: async () => { + return metadataClient.getCandles({ asset: asset!, timeframe: timeframe! }); + }, + refetchInterval: timeUnits.minute * 5, + refetchOnMount: false, + refetchOnWindowFocus: false, + }); + + return { + ...candlesQuery, + data: candlesQuery.data?.[asset ?? ''].map(mapMetadataServiceCandles), + }; +}; diff --git a/src/hooks/usePotentialMarkets.tsx b/src/hooks/usePotentialMarkets.tsx index 926a78844..2c51bafad 100644 --- a/src/hooks/usePotentialMarkets.tsx +++ b/src/hooks/usePotentialMarkets.tsx @@ -1,6 +1,7 @@ import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { STRING_KEYS } from '@/constants/localization'; +import { LIQUIDITY_TIERS } from '@/constants/markets'; import type { NewMarketProposal } from '@/constants/potentialMarkets'; import { log } from '@/lib/telemetry'; @@ -10,50 +11,7 @@ import { useStringGetter } from './useStringGetter'; const PotentialMarketsContext = createContext>({ potentialMarkets: undefined, hasPotentialMarketsData: false, - liquidityTiers: { - 0: { - label: 'Large-cap', - initialMarginFraction: 0.05, - maintenanceMarginFraction: 0.03, - impactNotional: 10_000, - }, - 1: { - label: 'Small-cap', - initialMarginFraction: 0.1, - maintenanceMarginFraction: 0.05, - impactNotional: 5_000, - }, - 2: { - label: 'Long-tail', - initialMarginFraction: 0.2, - maintenanceMarginFraction: 0.1, - impactNotional: 2_500, - }, - 3: { - label: 'Safety', - initialMarginFraction: 1, - maintenanceMarginFraction: 0.2, - impactNotional: 2_500, - }, - 4: { - label: 'Isolated', - initialMarginFraction: 0.05, - maintenanceMarginFraction: 0.03, - impactNotional: 2_500, - }, - 5: { - label: 'Mid-cap', - initialMarginFraction: 0.05, - maintenanceMarginFraction: 0.03, - impactNotional: 5_000, - }, - 6: { - label: 'FX', - initialMarginFraction: 0.01, - maintenanceMarginFraction: 0.0005, - impactNotional: 2_500, - }, - }, + liquidityTiers: LIQUIDITY_TIERS, }); PotentialMarketsContext.displayName = 'PotentialMarkets'; diff --git a/src/pages/vaults/VaultPnlChart.tsx b/src/pages/vaults/VaultPnlChart.tsx index 90419aac8..f352cbf26 100644 --- a/src/pages/vaults/VaultPnlChart.tsx +++ b/src/pages/vaults/VaultPnlChart.tsx @@ -254,9 +254,7 @@ export const VaultPnlChart = ({ className }: VaultPnlChartProps) => { numGridLines={0} tickSpacingX={210} tickSpacingY={75} - > - {undefined} - + /> ); diff --git a/src/views/MarketDetails/LaunchableMarketDetails.tsx b/src/views/MarketDetails/LaunchableMarketDetails.tsx index 7e19f3c27..e3b93bb2f 100644 --- a/src/views/MarketDetails/LaunchableMarketDetails.tsx +++ b/src/views/MarketDetails/LaunchableMarketDetails.tsx @@ -1,4 +1,5 @@ import { STRING_KEYS } from '@/constants/localization'; +import { LIQUIDITY_TIERS } from '@/constants/markets'; import { useMetadataServiceAssetFromId } from '@/hooks/useLaunchableMarkets'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -11,11 +12,7 @@ import { BIG_NUMBERS } from '@/lib/numbers'; import { MarketDetails } from './MarketDetails'; -const ISOLATED_LIQUIDITY_TIER_INFO = { - initialMarginFraction: 0.05, - maintenanceMarginFraction: 0.03, - impactNotional: 2_500, -}; +const ISOLATED_LIQUIDITY_TIER_INFO = LIQUIDITY_TIERS[4]; export const LaunchableMarketDetails = ({ launchableMarketId }: { launchableMarketId: string }) => { const stringGetter = useStringGetter(); diff --git a/src/views/charts/LaunchableMarketChart.tsx b/src/views/charts/LaunchableMarketChart.tsx index db4307ee2..5586f2150 100644 --- a/src/views/charts/LaunchableMarketChart.tsx +++ b/src/views/charts/LaunchableMarketChart.tsx @@ -1,30 +1,42 @@ -import { useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; +import { curveLinear } from '@visx/curve'; +import { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip'; import styled from 'styled-components'; import tw from 'twin.macro'; +import { MetadataServiceCandlesTimeframes } from '@/constants/assetMetadata'; import { ButtonSize } from '@/constants/buttons'; +import { TradingViewBar } from '@/constants/candles'; import { STRING_KEYS } from '@/constants/localization'; +import { LIQUIDITY_TIERS } from '@/constants/markets'; +import { timeUnits } from '@/constants/time'; -import { useMetadataServiceAssetFromId } from '@/hooks/useLaunchableMarkets'; +import { + useMetadataServiceAssetFromId, + useMetadataServiceCandles, +} from '@/hooks/useLaunchableMarkets'; import { useStringGetter } from '@/hooks/useStringGetter'; import { LinkOutIcon } from '@/icons'; import { Details } from '@/components/Details'; import { Link } from '@/components/Link'; +import { LoadingSpace } from '@/components/Loading/LoadingSpinner'; import { Output, OutputType } from '@/components/Output'; import { Tag } from '@/components/Tag'; import { ToggleGroup } from '@/components/ToggleGroup'; +import { TimeSeriesChart } from '@/components/visx/TimeSeriesChart'; + +import { useAppSelector } from '@/state/appTypes'; +import { getChartDotBackground } from '@/state/configsSelectors'; +import { getSelectedLocale } from '@/state/localizationSelectors'; import { getDisplayableAssetFromBaseAsset } from '@/lib/assetUtils'; +import { BIG_NUMBERS } from '@/lib/numbers'; import { orEmptyObj } from '@/lib/typeUtils'; -enum ChartResolution { - DAY = 'day', - WEEK = 'week', - MONTH = 'month', -} +const ISOLATED_LIQUIDITY_TIER_INFO = LIQUIDITY_TIERS[4]; export const LaunchableMarketChart = ({ className, @@ -34,36 +46,113 @@ export const LaunchableMarketChart = ({ ticker?: string; }) => { const stringGetter = useStringGetter(); - const [resolution, setResolution] = useState(ChartResolution.DAY); + const [timeframe, setTimeframe] = useState('1d'); const launchableAsset = useMetadataServiceAssetFromId(ticker); const { id, marketCap, name, price, logo, tickSizeDecimals, urls } = orEmptyObj(launchableAsset); const websiteLink = urls?.website ?? undefined; - - if (!ticker) return null; + const candlesQuery = useMetadataServiceCandles(id, timeframe); + const selectedLocale = useAppSelector(getSelectedLocale); + const chartDotsBackground = useAppSelector(getChartDotBackground); const items = [ { key: 'market-cap', label: stringGetter({ key: STRING_KEYS.MARKET_CAP }), - value: , + value: ( + + ), }, { key: 'max-leverage', label: stringGetter({ key: STRING_KEYS.MAXIMUM_LEVERAGE }), - value: , + value: ( + + ), }, ]; + const xAccessorFunc = useCallback((datum: TradingViewBar) => datum.time, []); + const yAccessorFunc = useCallback((datum: TradingViewBar) => datum.close, []); + + const colorString = useMemo(() => { + if (!candlesQuery.data) return 'var(--color-text-1)'; + const first = candlesQuery.data[0]; + const last = candlesQuery.data[candlesQuery.data.length - 1]; + if (first.close > last.close) return 'var(--color-negative)'; + return 'var(--color-positive)'; + }, [candlesQuery.data]); + + const series = useMemo( + () => [ + { + dataKey: 'pnl', + xAccessor: xAccessorFunc, + yAccessor: yAccessorFunc, + colorAccessor: () => colorString, + getCurve: () => curveLinear, + threshold: { + aboveAreaProps: { + fill: 'var(--color-text-0)', + fillOpacity: 0.2, + stroke: colorString, + }, + // This yAccessor displays a gradient from the line to the bottom (0) of the chart. + yAccessor: () => 0, + }, + }, + ], + [colorString, xAccessorFunc, yAccessorFunc] + ); + + const renderTooltip = (tooltipParam: RenderTooltipParams) => { + const datum = tooltipParam?.tooltipData?.nearestDatum?.datum; + if (!datum) return
; + + return ( +
+ + + {stringGetter({ key: STRING_KEYS.PRICE })}:{' '} + + +
+ ); + }; + return ( <$LaunchableMarketChartContainer className={className}> <$ChartContainerHeader tw="flex flex-row items-center justify-between">
- {name} + {logo ? ( + {name} + ) : ( +
+ )}

- - {name} - - + {name && ( + + {name} + + + )}

@@ -83,6 +172,7 @@ export const LaunchableMarketChart = ({ type={OutputType.Fiat} tw="text-color-text-1" value={price} + isLoading={!ticker} fractionDigits={tickSizeDecimals} /> ), @@ -93,24 +183,52 @@ export const LaunchableMarketChart = ({ size={ButtonSize.Base} items={[ { - value: ChartResolution.DAY, + value: '1d', label: `1${stringGetter({ key: STRING_KEYS.DAYS_ABBREVIATED })}`, }, { - value: ChartResolution.WEEK, + value: '7d', label: `7${stringGetter({ key: STRING_KEYS.DAYS_ABBREVIATED })}`, }, { - value: ChartResolution.MONTH, + value: '30d', label: `30${stringGetter({ key: STRING_KEYS.DAYS_ABBREVIATED })}`, }, ]} - value={resolution} - onValueChange={setResolution} + value={timeframe} + onValueChange={setTimeframe} />
- <$ChartContainer /> + <$ChartContainer chartBackground={chartDotsBackground}> + {candlesQuery.isLoading || !candlesQuery.data ? ( + + ) : ( + + )} + <$Details layout="rowColumns" items={items} /> @@ -146,4 +264,8 @@ const $ToggleGroup = styled(ToggleGroup)` } ` as typeof ToggleGroup; -const $ChartContainer = tw.div`h-[8.75rem] rounded-[1rem] border-[length:--border-width] border-color-border p-1.5 [border-style:solid]`; +const $ChartContainer = styled.div<{ chartBackground?: string }>` + ${tw`h-[8.75rem] overflow-hidden rounded-[1rem] border-[length:--border-width] border-color-border [border-style:solid]`} + background: url(${({ chartBackground }) => chartBackground}) no-repeat center; + background-size: 175%; +`;