Skip to content

Commit

Permalink
Showing 4 changed files with 344 additions and 322 deletions.
60 changes: 10 additions & 50 deletions src/hooks/tradingView/useTradingView.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react';
import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo } from 'react';

import BigNumber from 'bignumber.js';
import isEmpty from 'lodash/isEmpty';
import {
type IBasicDataFeed,
LanguageCode,
ResolutionString,
TradingTerminalWidgetOptions,
@@ -15,21 +15,16 @@ import { STRING_KEYS, SUPPORTED_LOCALE_BASE_TAGS } from '@/constants/localizatio
import { tooltipStrings } from '@/constants/tooltips';
import type { TvWidget } from '@/constants/tvchart';

import { store } from '@/state/_store';
import { getSelectedNetwork } from '@/state/appSelectors';
import { useAppDispatch, useAppSelector } from '@/state/appTypes';
import { getAppColorMode, getAppTheme } from '@/state/configsSelectors';
import { getSelectedLocale } from '@/state/localizationSelectors';
import { getCurrentMarketConfig, getCurrentMarketId } from '@/state/perpetualsSelectors';
import { getCurrentMarketId } from '@/state/perpetualsSelectors';
import { updateChartConfig } from '@/state/tradingView';
import { getTvChartConfig } from '@/state/tradingViewSelectors';

import { getDydxDatafeed } from '@/lib/tradingView/dydxfeed';
import { getSavedResolution, getWidgetOptions, getWidgetOverrides } from '@/lib/tradingView/utils';
import { orEmptyObj } from '@/lib/typeUtils';

import { useDydxClient } from '../useDydxClient';
import { useLocaleSeparators } from '../useLocaleSeparators';
import { useAllStatsigGateValues } from '../useStatsig';
import { useStringGetter } from '../useStringGetter';
import { useURLConfigs } from '../useURLConfigs';
@@ -50,6 +45,8 @@ export const useTradingView = ({
buySellMarksToggleOn,
setBuySellMarksToggleOn,
setIsChartReady,
tickSizeDecimals,
datafeed,
}: {
tvWidgetRef: React.MutableRefObject<TvWidget | null>;
orderLineToggleRef: React.MutableRefObject<HTMLElement | null>;
@@ -62,41 +59,28 @@ export const useTradingView = ({
buySellMarksToggleOn: boolean;
setBuySellMarksToggleOn: Dispatch<SetStateAction<boolean>>;
setIsChartReady: React.Dispatch<React.SetStateAction<boolean>>;
tickSizeDecimals?: number;
datafeed: IBasicDataFeed;
}) => {
const stringGetter = useStringGetter();
const urlConfigs = useURLConfigs();
const featureFlags = useAllStatsigGateValues();
const dispatch = useAppDispatch();

const { group, decimal } = useLocaleSeparators();

const appTheme = useAppSelector(getAppTheme);
const appColorMode = useAppSelector(getAppColorMode);

const marketId = useAppSelector(getCurrentMarketId);
const selectedLocale = useAppSelector(getSelectedLocale);
const selectedNetwork = useAppSelector(getSelectedNetwork);

const { getCandlesForDatafeed, getMarketTickSize } = useDydxClient();

const savedTvChartConfig = useAppSelector(getTvChartConfig);

const savedResolution = useMemo(
() => getSavedResolution({ savedConfig: savedTvChartConfig }),
[savedTvChartConfig]
);

const [tickSizeDecimalsIndexer, setTickSizeDecimalsIndexer] = useState<{
[marketId: string]: number | undefined;
}>({});
const { tickSizeDecimals: tickSizeDecimalsAbacus } = orEmptyObj(
useAppSelector(getCurrentMarketConfig)
);
const tickSizeDecimals =
(marketId
? tickSizeDecimalsIndexer[marketId] ?? tickSizeDecimalsAbacus
: tickSizeDecimalsAbacus) ?? undefined;

const initializeToggle = useCallback(
({
toggleRef,
@@ -124,42 +108,17 @@ export const useTradingView = ({
[]
);

useEffect(() => {
// we only need tick size from current market for the price scale settings
// if markets haven't been loaded via abacus, get the current market info from indexer
(async () => {
if (marketId && tickSizeDecimals === undefined) {
const marketTickSize = await getMarketTickSize(marketId);
setTickSizeDecimalsIndexer((prev) => ({
...prev,
[marketId]: BigNumber(marketTickSize).decimalPlaces() ?? undefined,
}));
}
})();
}, [marketId, tickSizeDecimals, getMarketTickSize]);

const tradingViewLimitOrder = useTradingViewLimitOrder(marketId, tickSizeDecimals);

useEffect(() => {
if (marketId && tickSizeDecimals !== undefined) {
if (marketId) {
const widgetOptions = getWidgetOptions();
const widgetOverrides = getWidgetOverrides({ appTheme, appColorMode });

const initialPriceScale = BigNumber(10)
.exponentiatedBy(tickSizeDecimals ?? 2)
.toNumber();
const options: TradingTerminalWidgetOptions = {
...widgetOptions,
...widgetOverrides,
datafeed: getDydxDatafeed(
store,
getCandlesForDatafeed,
initialPriceScale,
orderbookCandlesToggleOn,
{ decimal, group },
selectedLocale,
stringGetter
),
datafeed,
interval: (savedResolution ?? DEFAULT_RESOLUTION) as ResolutionString,
locale: SUPPORTED_LOCALE_BASE_TAGS[selectedLocale] as LanguageCode,
symbol: marketId,
@@ -249,6 +208,7 @@ export const useTradingView = ({
selectedLocale,
selectedNetwork,
!!marketId,
datafeed,
tickSizeDecimals !== undefined,
orderLineToggleRef,
orderbookCandlesToggleRef,
287 changes: 287 additions & 0 deletions src/hooks/tradingView/useTradingViewDatafeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
import { useMemo, useRef } from 'react';

import BigNumber from 'bignumber.js';
import { groupBy } from 'lodash';
import { DateTime } from 'luxon';
import type {
DatafeedConfiguration,
ErrorCallback,
GetMarksCallback,
HistoryCallback,
LibrarySymbolInfo,
Mark,
OnReadyCallback,
ResolutionString,
ResolveCallback,
SearchSymbolsCallback,
SubscribeBarsCallback,
Timezone,
} from 'public/tradingview/charting_library';
import { useDispatch } from 'react-redux';

import { Candle, RESOLUTION_MAP } from '@/constants/candles';
import { DEFAULT_MARKETID } from '@/constants/markets';
import { DisplayUnit } from '@/constants/trade';

import { useDydxClient } from '@/hooks/useDydxClient';

import { Themes } from '@/styles/themes';

import { store } from '@/state/_store';
import { getMarketFills } from '@/state/accountSelectors';
import { useAppSelector } from '@/state/appTypes';
import { getAppColorMode, getAppTheme } from '@/state/configsSelectors';
import { getSelectedLocale } from '@/state/localizationSelectors';
import { setCandles } from '@/state/perpetuals';
import { getMarketData, getPerpetualBarsForPriceChart } from '@/state/perpetualsSelectors';

import { objectKeys } from '@/lib/objectHelpers';
import { log } from '@/lib/telemetry';
import { lastBarsCache } from '@/lib/tradingView/dydxfeed/cache';
import { subscribeOnStream, unsubscribeFromStream } from '@/lib/tradingView/dydxfeed/streaming';
import { getMarkForOrderFills } from '@/lib/tradingView/dydxfeed/utils';
import { getHistorySlice, getSymbol, mapCandle } from '@/lib/tradingView/utils';

import { useLocaleSeparators } from '../useLocaleSeparators';
import { useStringGetter } from '../useStringGetter';

const timezone = DateTime.local().get('zoneName') as unknown as Timezone;

const configurationData: DatafeedConfiguration = {
supported_resolutions: objectKeys(RESOLUTION_MAP),
supports_marks: true,
exchanges: [
{
value: 'dYdX', // `exchange` argument for the `searchSymbols` method, if a user selects this exchange
name: 'dYdX', // filter name
desc: 'dYdX v4 exchange', // full exchange name displayed in the filter popup
},
],
symbols_types: [
{
name: 'crypto',
value: 'crypto', // `symbolType` argument for the `searchSymbols` method, if a user selects this symbol type
},
],
};

type Props = {
orderbookCandlesToggleOn: boolean;
tickSizeDecimals?: number;
};

export const useTradingViewDatafeed = ({ tickSizeDecimals, orderbookCandlesToggleOn }: Props) => {
const resetCacheRef = useRef<() => void | undefined>();

const dispatch = useDispatch();
const stringGetter = useStringGetter();

const { group, decimal } = useLocaleSeparators();
const appTheme = useAppSelector(getAppTheme);
const appColorMode = useAppSelector(getAppColorMode);
const selectedLocale = useAppSelector(getSelectedLocale);

const { getCandlesForDatafeed } = useDydxClient();

return useMemo(
() => ({
resetCache: () => {
resetCacheRef.current?.();
},
datafeed: {
onReady: (callback: OnReadyCallback) => {
setTimeout(() => callback(configurationData), 0);
},

searchSymbols: (
userInput: string,
exchange: string,
symbolType: string,
onResultReadyCallback: SearchSymbolsCallback
) => {
onResultReadyCallback([]);
},

resolveSymbol: async (symbolName: string, onSymbolResolvedCallback: ResolveCallback) => {
const symbolItem = getSymbol(symbolName || DEFAULT_MARKETID);
const initialPriceScale = BigNumber(10)
.exponentiatedBy(tickSizeDecimals ?? 2)
.toNumber();
const pricescale = tickSizeDecimals ? 10 ** tickSizeDecimals : initialPriceScale ?? 100;

const symbolInfo: LibrarySymbolInfo = {
ticker: symbolItem.symbol,
name: symbolItem.full_name,
description: symbolItem.description,
type: symbolItem.type,
exchange: 'dYdX',
listed_exchange: 'dYdX',
has_intraday: true,
has_daily: true,

minmov: 1,
pricescale,

session: '24x7',
intraday_multipliers: ['1', '5', '15', '30', '60', '240'],
supported_resolutions: configurationData.supported_resolutions!,
data_status: 'streaming',
timezone,
format: 'price',
};

setTimeout(() => onSymbolResolvedCallback(symbolInfo), 0);
},

getMarks: async (
symbolInfo: LibrarySymbolInfo,
fromSeconds: number,
toSeconds: number,
onDataCallback: GetMarksCallback<Mark>,
resolution: ResolutionString
) => {
const [fromMs, toMs] = [fromSeconds * 1000, toSeconds * 1000];

const market = getMarketData(store.getState(), symbolInfo.ticker!);
if (!market) return;

const fills = getMarketFills(store.getState())[symbolInfo.ticker!] ?? [];

const inRangeFills = fills.filter(
(fill) => fill.createdAtMilliseconds >= fromMs && fill.createdAtMilliseconds <= toMs
);
const fillsByOrderId = groupBy(inRangeFills, 'orderId');
const marks = Object.entries(fillsByOrderId).map(([orderId, orderFills]) =>
getMarkForOrderFills(
store,
orderFills,
orderId,
fromMs,
resolution,
stringGetter,
{ group, decimal },
selectedLocale,
Themes[appTheme][appColorMode]
)
);
onDataCallback(marks);
},

getBars: async (
symbolInfo: LibrarySymbolInfo,
resolution: ResolutionString,
periodParams: {
countBack: number;
from: number;
to: number;
firstDataRequest: boolean;
},
onHistoryCallback: HistoryCallback,
onErrorCallback: ErrorCallback
) => {
if (!symbolInfo) return;

const { countBack, from, to, firstDataRequest } = periodParams;
const fromMs = from * 1000;
let toMs = to * 1000;

// Add 1ms to the toMs to ensure that today's candle is included
if (firstDataRequest && resolution === '1D') {
toMs += 1;
}

try {
const currentMarketBars = getPerpetualBarsForPriceChart(orderbookCandlesToggleOn)(
store.getState(),
symbolInfo.ticker!,
resolution
);

// Retrieve candles in the store that are between fromMs and toMs
const cachedBars = getHistorySlice({
bars: currentMarketBars,
fromMs,
toMs,
firstDataRequest,
orderbookCandlesToggleOn,
});

let fetchedCandles: Candle[] | undefined;

// If there are not enough candles in the store, retrieve more from the API
if (cachedBars.length < countBack) {
const earliestCachedBarTime = cachedBars?.[cachedBars.length - 1]?.time;

fetchedCandles = await getCandlesForDatafeed({
marketId: symbolInfo.ticker!,
resolution,
fromMs,
toMs: earliestCachedBarTime || toMs,
});

dispatch(
setCandles({ candles: fetchedCandles, marketId: symbolInfo.ticker!, resolution })
);
}

const volumeUnit = store.getState().configs.displayUnit;

const bars = [
...cachedBars,
...(fetchedCandles?.map(mapCandle(orderbookCandlesToggleOn)) ?? []),
]
.map((bar) => ({
...bar,
volume: volumeUnit === DisplayUnit.Fiat ? bar.usdVolume : bar.assetVolume,
}))
.reverse();

if (bars.length === 0) {
onHistoryCallback([], {
noData: true,
});

return;
}

if (firstDataRequest) {
lastBarsCache.set(`${symbolInfo.ticker}/${RESOLUTION_MAP[resolution]}`, {
...bars[bars.length - 1],
});
}

onHistoryCallback(bars, {
noData: false,
});
} catch (error) {
log('tradingView/dydxfeed/getBars', error);
onErrorCallback(error);
}
},

subscribeBars: (
symbolInfo: LibrarySymbolInfo,
resolution: ResolutionString,
onTick: SubscribeBarsCallback,
listenerGuid: string,
onResetCacheNeededCallback: () => void
) => {
resetCacheRef.current = onResetCacheNeededCallback;
subscribeOnStream({
symbolInfo,
resolution,
onRealtimeCallback: onTick,
listenerGuid,
onResetCacheNeededCallback,
lastBar: lastBarsCache.get(`${symbolInfo.ticker}/${RESOLUTION_MAP[resolution]}`),
});
},

unsubscribeBars: (subscriberUID: string) => {
unsubscribeFromStream(subscriberUID);
},
},
}),
[tickSizeDecimals !== undefined, orderbookCandlesToggleOn]
);
};
269 changes: 0 additions & 269 deletions src/lib/tradingView/dydxfeed/index.ts

This file was deleted.

50 changes: 47 additions & 3 deletions src/views/charts/TradingView/TvChart.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react';

import BigNumber from 'bignumber.js';
import type { ResolutionString } from 'public/tradingview/charting_library';

import { DEFAULT_MARKETID } from '@/constants/markets';
@@ -10,19 +11,26 @@ import { useChartLines } from '@/hooks/tradingView/useChartLines';
import { useChartMarketAndResolution } from '@/hooks/tradingView/useChartMarketAndResolution';
import { useOrderbookCandles } from '@/hooks/tradingView/useOrderbookCandles';
import { useTradingView } from '@/hooks/tradingView/useTradingView';
import { useTradingViewDatafeed } from '@/hooks/tradingView/useTradingViewDatafeed';
import { useTradingViewTheme } from '@/hooks/tradingView/useTradingViewTheme';
import { useTradingViewToggles } from '@/hooks/tradingView/useTradingViewToggles';
import { useDydxClient } from '@/hooks/useDydxClient';
import usePrevious from '@/hooks/usePrevious';

import { useAppSelector } from '@/state/appTypes';
import { getSelectedDisplayUnit } from '@/state/configsSelectors';
import { getCurrentMarketId } from '@/state/perpetualsSelectors';
import { getCurrentMarketConfig, getCurrentMarketId } from '@/state/perpetualsSelectors';

import { orEmptyObj } from '@/lib/typeUtils';

import { BaseTvChart } from './BaseTvChart';

export const TvChart = () => {
const [isChartReady, setIsChartReady] = useState(false);
const currentMarketId: string = useAppSelector(getCurrentMarketId) ?? DEFAULT_MARKETID;
const { tickSizeDecimals: tickSizeDecimalsAbacus } = orEmptyObj(
useAppSelector(getCurrentMarketConfig)
);

const tvWidgetRef = useRef<TvWidget | null>(null);
const tvWidget = tvWidgetRef.current;
@@ -36,6 +44,31 @@ export const TvChart = () => {
const buySellMarksToggleRef = useRef<HTMLElement | null>(null);
const buySellMarksToggle = buySellMarksToggleRef.current;

const [tickSizeDecimalsIndexer, setTickSizeDecimalsIndexer] = useState<{
[marketId: string]: number | undefined;
}>({});

const tickSizeDecimals =
(currentMarketId
? tickSizeDecimalsIndexer[currentMarketId] ?? tickSizeDecimalsAbacus
: tickSizeDecimalsAbacus) ?? undefined;

const { getMarketTickSize } = useDydxClient();

useEffect(() => {
// we only need tick size from current market for the price scale settings
// if markets haven't been loaded via abacus, get the current market info from indexer
(async () => {
if (currentMarketId && tickSizeDecimals === undefined) {
const marketTickSize = await getMarketTickSize(currentMarketId);
setTickSizeDecimalsIndexer((prev) => ({
...prev,
[currentMarketId]: BigNumber(marketTickSize).decimalPlaces() ?? undefined,
}));
}
})();
}, [currentMarketId, getMarketTickSize, tickSizeDecimals]);

const {
orderLinesToggleOn,
setOrderLinesToggleOn,
@@ -44,6 +77,12 @@ export const TvChart = () => {
setBuySellMarksToggleOn,
buySellMarksToggleOn,
} = useTradingViewToggles();

const { datafeed, resetCache } = useTradingViewDatafeed({
orderbookCandlesToggleOn,
tickSizeDecimals,
});

const { savedResolution } = useTradingView({
tvWidgetRef,
orderLineToggleRef,
@@ -56,6 +95,8 @@ export const TvChart = () => {
buySellMarksToggleOn,
setBuySellMarksToggleOn,
setIsChartReady,
tickSizeDecimals,
datafeed,
});
useChartMarketAndResolution({
currentMarketId,
@@ -92,9 +133,12 @@ export const TvChart = () => {
// Only reset data if displayUnit has actually changed
if (prevDisplayUnit !== displayUnit) {
const chart = tvWidget.activeChart?.();
chart?.resetData();
if (chart) {
chart.resetData();
resetCache();
}
}
}, [displayUnit, tvWidget, isChartReady, prevDisplayUnit]);
}, [displayUnit, tvWidget, isChartReady, prevDisplayUnit, resetCache]);

return <BaseTvChart isChartReady={isChartReady} />;
};

0 comments on commit b13c383

Please sign in to comment.