From fdb0d03ec0f64fe2287c8d653693d169b10c7343 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Tue, 21 Nov 2023 21:46:01 -0800 Subject: [PATCH] feat: add datafeed hook for trading view --- .../client-pages/market/trade-grid.tsx | 4 +- .../client-pages/market/trade-views.tsx | 9 +- .../chart-container/chart-container.spec.tsx | 24 +- .../chart-container/chart-container.tsx | 254 ++++++++++++++++++ .../components/chart-container/index.ts | 1 + .../use-candles-chart-settings.ts | 23 +- .../trading-view-container/index.ts | 1 + .../trading-view-container.tsx | 5 + apps/trading/pages/index.page.tsx | 12 +- apps/trading/pages/styles.css | 68 +++++ libs/candles-chart/src/lib/candles-chart.tsx | 37 +-- libs/candles-chart/src/lib/candles-menu.tsx | 161 ----------- libs/candles-chart/src/lib/index.ts | 1 - libs/trading-view/src/lib/datafeed.ts | 214 +++++++++++++++ libs/trading-view/src/lib/trading-view.tsx | 79 +++++- tsconfig.base.json | 2 +- 16 files changed, 680 insertions(+), 215 deletions(-) rename libs/candles-chart/src/lib/candles-menu.spec.tsx => apps/trading/components/chart-container/chart-container.spec.tsx (88%) create mode 100644 apps/trading/components/chart-container/chart-container.tsx create mode 100644 apps/trading/components/chart-container/index.ts rename {libs/candles-chart/src/lib => apps/trading/components/chart-container}/use-candles-chart-settings.ts (88%) create mode 100644 apps/trading/components/trading-view-container/index.ts create mode 100644 apps/trading/components/trading-view-container/trading-view-container.tsx delete mode 100644 libs/candles-chart/src/lib/candles-menu.tsx create mode 100644 libs/trading-view/src/lib/datafeed.ts diff --git a/apps/trading/client-pages/market/trade-grid.tsx b/apps/trading/client-pages/market/trade-grid.tsx index c10b4831b..cf96266f8 100644 --- a/apps/trading/client-pages/market/trade-grid.tsx +++ b/apps/trading/client-pages/market/trade-grid.tsx @@ -192,7 +192,7 @@ const MainGrid = memo( ); - } + }, ); MainGrid.displayName = 'MainGrid'; @@ -200,7 +200,7 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => { const featureFlags = useFeatureFlags((state) => state.flags); const wrapperClasses = classNames( 'h-full grid', - 'grid-rows-[min-content_1fr]' + 'grid-rows-[min-content_1fr]', ); return ( diff --git a/apps/trading/client-pages/market/trade-views.tsx b/apps/trading/client-pages/market/trade-views.tsx index 959e2797f..33e537270 100644 --- a/apps/trading/client-pages/market/trade-views.tsx +++ b/apps/trading/client-pages/market/trade-views.tsx @@ -1,8 +1,4 @@ import { DepthChartContainer } from '@vegaprotocol/market-depth'; -import { - CandlesChartContainer, - CandlesMenu, -} from '@vegaprotocol/candles-chart'; import { Filter, OpenOrdersMenu } from '@vegaprotocol/orders'; import { TradesContainer } from '../../components/trades-container'; import { OrderbookContainer } from '../../components/orderbook-container'; @@ -16,13 +12,14 @@ import { OrdersContainer } from '../../components/orders-container'; import { StopOrdersContainer } from '../../components/stop-orders-container'; import { AccountsMenu } from '../../components/accounts-menu'; import { PositionsMenu } from '../../components/positions-menu'; +import { ChartContainer, ChartMenu } from '../../components/chart-container'; export type TradingView = keyof typeof TradingViews; export const TradingViews = { candles: { - component: CandlesChartContainer, - menu: CandlesMenu, + component: ChartContainer, + menu: ChartMenu, }, depth: { component: DepthChartContainer, diff --git a/libs/candles-chart/src/lib/candles-menu.spec.tsx b/apps/trading/components/chart-container/chart-container.spec.tsx similarity index 88% rename from libs/candles-chart/src/lib/candles-menu.spec.tsx rename to apps/trading/components/chart-container/chart-container.spec.tsx index a2964685a..3bbfafb4d 100644 --- a/libs/candles-chart/src/lib/candles-menu.spec.tsx +++ b/apps/trading/components/chart-container/chart-container.spec.tsx @@ -1,18 +1,18 @@ import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { CandlesMenu } from './candles-menu'; +import { ChartMenu } from './chart-container'; import { useCandlesChartSettingsStore, DEFAULT_CHART_SETTINGS, } from './use-candles-chart-settings'; import { Overlay, Study, overlayLabels, studyLabels } from 'pennant'; -describe('CandlesMenu', () => { +describe('ChartMenu', () => { const openDropdown = async () => { await userEvent.click( screen.getByRole('button', { name: 'Indicators', - }) + }), ); }; @@ -22,7 +22,7 @@ describe('CandlesMenu', () => { }); it.each(Object.values(Overlay))('can set %s overlay', async (overlay) => { - render(); + render(); await openDropdown(); @@ -35,12 +35,12 @@ describe('CandlesMenu', () => { expect(screen.getByText(overlayLabels[overlay as Overlay])).toHaveAttribute( 'data-state', - 'checked' + 'checked', ); }); it.each(Object.values(Study))('can set %s study', async (study) => { - render(); + render(); await openDropdown(); @@ -53,33 +53,33 @@ describe('CandlesMenu', () => { expect(screen.getByText(studyLabels[study as Study])).toHaveAttribute( 'data-state', - 'checked' + 'checked', ); }); it('should render with the correct default studies and overlays', async () => { useCandlesChartSettingsStore.setState(DEFAULT_CHART_SETTINGS); - render(); + render(); await userEvent.click( screen.getByRole('button', { name: 'Indicators', - }) + }), ); const menu = within(await screen.findByRole('menu')); expect(menu.getByText(studyLabels.volume)).toHaveAttribute( 'data-state', - 'checked' + 'checked', ); expect(menu.getByText(studyLabels.macd)).toHaveAttribute( 'data-state', - 'checked' + 'checked', ); expect(menu.getByText(overlayLabels.movingAverage)).toHaveAttribute( 'data-state', - 'checked' + 'checked', ); }); }); diff --git a/apps/trading/components/chart-container/chart-container.tsx b/apps/trading/components/chart-container/chart-container.tsx new file mode 100644 index 000000000..b6528dae7 --- /dev/null +++ b/apps/trading/components/chart-container/chart-container.tsx @@ -0,0 +1,254 @@ +import { CandlesChartContainer } from '@vegaprotocol/candles-chart'; +import { + ChartType, + Interval, + Overlay, + Study, + chartTypeLabels, + intervalLabels, + overlayLabels, + studyLabels, +} from 'pennant'; +import { + TradingButton, + TradingDropdown, + TradingDropdownCheckboxItem, + TradingDropdownContent, + TradingDropdownItemIndicator, + TradingDropdownRadioGroup, + TradingDropdownRadioItem, + TradingDropdownTrigger, + Icon, +} from '@vegaprotocol/ui-toolkit'; +import { type IconName } from '@blueprintjs/icons'; +import { IconNames } from '@blueprintjs/icons'; +import { useT } from '../../lib/use-t'; + +import { + useCandlesChartSettings, + STUDY_SIZE, +} from './use-candles-chart-settings'; +import { TradingViewContainer } from '../../components/trading-view-container'; + +const chartTypeIcon = new Map([ + [ChartType.AREA, IconNames.TIMELINE_AREA_CHART], + [ChartType.CANDLE, IconNames.WATERFALL_CHART], + [ChartType.LINE, IconNames.TIMELINE_LINE_CHART], + [ChartType.OHLC, IconNames.WATERFALL_CHART], +]); + +/** + * Renders either the pennant chart or the tradingview chart + */ +export const ChartContainer = ({ marketId }: { marketId: string }) => { + const { + chartlib, + interval, + chartType, + overlays, + studies, + studySizes, + setStudies, + setStudySizes, + setOverlays, + } = useCandlesChartSettings(); + + switch (chartlib) { + case 'tradingview': { + return ; + } + case 'pennant': { + return ( + + ); + } + default: { + throw new Error('invalid chart lib'); + } + } +}; + +export const ChartMenu = () => { + const { + chartlib, + interval, + chartType, + studies, + overlays, + setChartlib, + setInterval, + setType, + setStudies, + setOverlays, + } = useCandlesChartSettings(); + const t = useT(); + + const contentAlign = 'end'; + const triggerClasses = 'text-xs'; + const triggerButtonProps = { size: 'extra-small' } as const; + + const chartlibDropdown = ( + + {chartlib} + + } + > + + { + // @ts-ignore need to cast to Chartlib + setChartlib(value); + }} + > + + Tradingview + + + + + Pennant + + + + + + ); + + switch (chartlib) { + case 'tradingview': { + return <>{chartlibDropdown}; + } + case 'pennant': { + return ( + <> + {chartlibDropdown} + + + {t('Interval: {{interval}}', { + interval: intervalLabels[interval], + })} + + + } + > + + { + setInterval(value as Interval); + }} + > + {Object.values(Interval).map((timeInterval) => ( + + {intervalLabels[timeInterval]} + + + ))} + + + + + + + + + } + > + + { + setType(value as ChartType); + }} + > + {Object.values(ChartType).map((type) => ( + + {chartTypeLabels[type]} + + + ))} + + + + + + {t('Indicators')} + + + } + > + + {Object.values(Overlay).map((overlay) => ( + { + const newOverlays = [...overlays]; + const index = overlays.findIndex( + (item) => item === overlay, + ); + + index !== -1 + ? newOverlays.splice(index, 1) + : newOverlays.push(overlay); + + setOverlays(newOverlays); + }} + > + {overlayLabels[overlay]} + + + ))} + {Object.values(Study).map((study) => ( + { + const newStudies = [...studies]; + const index = studies.findIndex((item) => item === study); + + index !== -1 + ? newStudies.splice(index, 1) + : newStudies.push(study); + + setStudies(newStudies); + }} + > + {studyLabels[study]} + + + ))} + + + + ); + } + default: { + throw new Error('invalid chart lib'); + } + } +}; diff --git a/apps/trading/components/chart-container/index.ts b/apps/trading/components/chart-container/index.ts new file mode 100644 index 000000000..e01a5c388 --- /dev/null +++ b/apps/trading/components/chart-container/index.ts @@ -0,0 +1 @@ +export { ChartContainer, ChartMenu } from './chart-container'; diff --git a/libs/candles-chart/src/lib/use-candles-chart-settings.ts b/apps/trading/components/chart-container/use-candles-chart-settings.ts similarity index 88% rename from libs/candles-chart/src/lib/use-candles-chart-settings.ts rename to apps/trading/components/chart-container/use-candles-chart-settings.ts index 4e22c3e7c..3fc9ae68d 100644 --- a/libs/candles-chart/src/lib/use-candles-chart-settings.ts +++ b/apps/trading/components/chart-container/use-candles-chart-settings.ts @@ -6,8 +6,10 @@ import { persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; type StudySizes = { [S in Study]?: number }; +type Chartlib = 'pennant' | 'tradingview'; interface StoredSettings { + chartlib: Chartlib; interval: Interval; type: ChartType; overlays: Overlay[]; @@ -25,6 +27,7 @@ const STUDY_ORDER: Study[] = [ ]; export const DEFAULT_CHART_SETTINGS = { + chartlib: 'tradingview' as const, interval: Interval.I15M, type: ChartType.CANDLE, overlays: [Overlay.MOVING_AVERAGE], @@ -39,6 +42,7 @@ export const useCandlesChartSettingsStore = create< setOverlays: (overlays?: Overlay[]) => void; setStudies: (studies?: Study[]) => void; setStudySizes: (sizes: number[]) => void; + setChartlib: (lib: Chartlib) => void; } >()( persist( @@ -81,11 +85,16 @@ export const useCandlesChartSettingsStore = create< }); }); }, + setChartlib: (lib) => { + set((state) => { + state.chartlib = lib; + }); + }, })), { name: 'vega_candles_chart_store', - } - ) + }, + ), ); export const useCandlesChartSettings = () => { @@ -94,25 +103,25 @@ export const useCandlesChartSettings = () => { const interval: Interval = getValidItem( settings.interval, Object.values(Interval), - Interval.I15M + Interval.I15M, ); const chartType: ChartType = getValidItem( settings.type, Object.values(ChartType), - ChartType.CANDLE + ChartType.CANDLE, ); const overlays: Overlay[] = getValidSubset( settings.overlays, Object.values(Overlay), - [] + [], ); const studies: Study[] = getValidSubset( settings.studies, Object.values(Study), - [Study.VOLUME] + [Study.VOLUME], ); // find the study size @@ -122,6 +131,7 @@ export const useCandlesChartSettings = () => { }); return { + chartlib: settings.chartlib, interval, chartType, overlays, @@ -132,5 +142,6 @@ export const useCandlesChartSettings = () => { setStudies: settings.setStudies, setOverlays: settings.setOverlays, setStudySizes: settings.setStudySizes, + setChartlib: settings.setChartlib, }; }; diff --git a/apps/trading/components/trading-view-container/index.ts b/apps/trading/components/trading-view-container/index.ts new file mode 100644 index 000000000..97f6bf9f5 --- /dev/null +++ b/apps/trading/components/trading-view-container/index.ts @@ -0,0 +1 @@ +export { TradingViewContainer } from './trading-view-container'; diff --git a/apps/trading/components/trading-view-container/trading-view-container.tsx b/apps/trading/components/trading-view-container/trading-view-container.tsx new file mode 100644 index 000000000..eb2f4f1ba --- /dev/null +++ b/apps/trading/components/trading-view-container/trading-view-container.tsx @@ -0,0 +1,5 @@ +import { TradingView } from '@vegaprotocol/trading-view'; + +export const TradingViewContainer = ({ marketId }: { marketId: string }) => { + return ; +}; diff --git a/apps/trading/pages/index.page.tsx b/apps/trading/pages/index.page.tsx index 3278d530b..2a4476b7c 100644 --- a/apps/trading/pages/index.page.tsx +++ b/apps/trading/pages/index.page.tsx @@ -1,5 +1,7 @@ import Head from 'next/head'; import { ClientRouter } from './client-router'; +import Script from 'next/script'; +import { useState } from 'react'; /** * Next only handles this single index page, react-router takes over after the page @@ -7,6 +9,7 @@ import { ClientRouter } from './client-router'; * have to serve a static site via next export */ export default function Index() { + const [isScriptReady, setIsScriptReady] = useState(false); return ( <> @@ -27,7 +30,14 @@ export default function Index() { - +