chore(trading): store all chart state (#5627)
Co-authored-by: bwallacee <ben@vega.xyz>
This commit is contained in:
parent
f22a3bc2d2
commit
0660eda334
@ -25,12 +25,12 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
|
||||
overlays,
|
||||
studies,
|
||||
studySizes,
|
||||
tradingViewStudies,
|
||||
setInterval,
|
||||
setStudies,
|
||||
setStudySizes,
|
||||
setOverlays,
|
||||
setTradingViewStudies,
|
||||
state,
|
||||
setState,
|
||||
} = useChartSettings();
|
||||
|
||||
const pennantChart = (
|
||||
@ -64,13 +64,13 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
|
||||
libraryHash={CHARTING_LIBRARY_HASH}
|
||||
marketId={marketId}
|
||||
interval={toTradingViewResolution(interval)}
|
||||
studies={tradingViewStudies}
|
||||
onIntervalChange={(newInterval) => {
|
||||
setInterval(fromTradingViewResolution(newInterval));
|
||||
}}
|
||||
onAutoSaveNeeded={(data: { studies: string[] }) => {
|
||||
setTradingViewStudies(data.studies);
|
||||
onAutoSaveNeeded={(data) => {
|
||||
setState(data);
|
||||
}}
|
||||
state={state}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -27,11 +27,11 @@ describe('ChartMenu', () => {
|
||||
|
||||
render(<ChartMenu />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Vega chart' }));
|
||||
expect(useChartSettingsStore.getState().chartlib).toEqual('pennant');
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'TradingView' }));
|
||||
await userEvent.click(screen.getByTestId('chartlib-toggle-button'));
|
||||
expect(useChartSettingsStore.getState().chartlib).toEqual('tradingview');
|
||||
|
||||
await userEvent.click(screen.getByTestId('chartlib-toggle-button'));
|
||||
expect(useChartSettingsStore.getState().chartlib).toEqual('pennant');
|
||||
});
|
||||
|
||||
describe('tradingview', () => {
|
||||
|
@ -68,6 +68,7 @@ export const ChartMenu = () => {
|
||||
setChartlib(isPennant ? 'tradingview' : 'pennant');
|
||||
}}
|
||||
size="extra-small"
|
||||
testId="chartlib-toggle-button"
|
||||
>
|
||||
{isPennant ? 'TradingView' : t('Vega chart')}
|
||||
</TradingButton>
|
||||
|
@ -9,6 +9,7 @@ type StudySizes = { [S in Study]?: number };
|
||||
export type Chartlib = 'pennant' | 'tradingview';
|
||||
|
||||
interface StoredSettings {
|
||||
state: object | undefined; // Don't see a better type provided from TradingView type definitions
|
||||
chartlib: Chartlib;
|
||||
// For interval we use the enum from @vegaprotocol/types, this is to make mapping between different
|
||||
// chart types easier and more consistent
|
||||
@ -17,7 +18,6 @@ interface StoredSettings {
|
||||
overlays: Overlay[];
|
||||
studies: Study[];
|
||||
studySizes: StudySizes;
|
||||
tradingViewStudies: string[];
|
||||
}
|
||||
|
||||
export const STUDY_SIZE = 90;
|
||||
@ -30,13 +30,13 @@ const STUDY_ORDER: Study[] = [
|
||||
];
|
||||
|
||||
export const DEFAULT_CHART_SETTINGS = {
|
||||
chartlib: 'tradingview' as const,
|
||||
state: undefined,
|
||||
chartlib: 'pennant' as const,
|
||||
interval: Interval.INTERVAL_I15M,
|
||||
type: ChartType.CANDLE,
|
||||
overlays: [Overlay.MOVING_AVERAGE],
|
||||
studies: [Study.MACD, Study.VOLUME],
|
||||
studySizes: {},
|
||||
tradingViewStudies: ['Volume'],
|
||||
};
|
||||
|
||||
export const useChartSettingsStore = create<
|
||||
@ -47,7 +47,7 @@ export const useChartSettingsStore = create<
|
||||
setStudies: (studies?: Study[]) => void;
|
||||
setStudySizes: (sizes: number[]) => void;
|
||||
setChartlib: (lib: Chartlib) => void;
|
||||
setTradingViewStudies: (studies: string[]) => void;
|
||||
setState: (state: object) => void;
|
||||
}
|
||||
>()(
|
||||
persist(
|
||||
@ -95,10 +95,8 @@ export const useChartSettingsStore = create<
|
||||
state.chartlib = lib;
|
||||
});
|
||||
},
|
||||
setTradingViewStudies: (studies: string[]) => {
|
||||
set((state) => {
|
||||
state.tradingViewStudies = studies;
|
||||
});
|
||||
setState: (state) => {
|
||||
set({ state });
|
||||
},
|
||||
})),
|
||||
{
|
||||
@ -147,13 +145,13 @@ export const useChartSettings = () => {
|
||||
overlays,
|
||||
studies,
|
||||
studySizes,
|
||||
tradingViewStudies: settings.tradingViewStudies,
|
||||
setInterval: settings.setInterval,
|
||||
setType: settings.setType,
|
||||
setStudies: settings.setStudies,
|
||||
setOverlays: settings.setOverlays,
|
||||
setStudySizes: settings.setStudySizes,
|
||||
setChartlib: settings.setChartlib,
|
||||
setTradingViewStudies: settings.setTradingViewStudies,
|
||||
state: settings.state,
|
||||
setState: settings.setState,
|
||||
};
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*", "__generated__"],
|
||||
"ignorePatterns": ["!**/*", "__generated__", "charting-library.d.ts"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
|
19258
libs/trading-view/src/charting-library.d.ts
vendored
Normal file
19258
libs/trading-view/src/charting-library.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,17 +9,17 @@ export const TradingViewContainer = ({
|
||||
libraryHash,
|
||||
marketId,
|
||||
interval,
|
||||
studies,
|
||||
onIntervalChange,
|
||||
onAutoSaveNeeded,
|
||||
state,
|
||||
}: {
|
||||
libraryPath: string;
|
||||
libraryHash: string;
|
||||
marketId: string;
|
||||
interval: ResolutionString;
|
||||
studies: string[];
|
||||
onIntervalChange: (interval: string) => void;
|
||||
onAutoSaveNeeded: OnAutoSaveNeededCallback;
|
||||
state: object | undefined;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const scriptState = useScript(
|
||||
@ -48,9 +48,9 @@ export const TradingViewContainer = ({
|
||||
libraryPath={libraryPath}
|
||||
marketId={marketId}
|
||||
interval={interval}
|
||||
studies={studies}
|
||||
onIntervalChange={onIntervalChange}
|
||||
onAutoSaveNeeded={onAutoSaveNeeded}
|
||||
state={state}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,127 +1,167 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import {
|
||||
usePrevious,
|
||||
useScreenDimensions,
|
||||
useThemeSwitcher,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useLanguage } from './use-t';
|
||||
import { useDatafeed } from './use-datafeed';
|
||||
import { type ResolutionString } from './constants';
|
||||
import {
|
||||
type ChartingLibraryFeatureset,
|
||||
type LanguageCode,
|
||||
type ChartingLibraryWidgetOptions,
|
||||
type IChartingLibraryWidget,
|
||||
type ChartPropertiesOverrides,
|
||||
type ResolutionString as TVResolutionString,
|
||||
} from '../charting-library';
|
||||
|
||||
export type OnAutoSaveNeededCallback = (data: { studies: string[] }) => void;
|
||||
const noop = () => {};
|
||||
|
||||
export type OnAutoSaveNeededCallback = (data: object) => void;
|
||||
|
||||
export const TradingView = ({
|
||||
marketId,
|
||||
libraryPath,
|
||||
interval,
|
||||
studies,
|
||||
onIntervalChange,
|
||||
onAutoSaveNeeded,
|
||||
state,
|
||||
}: {
|
||||
marketId: string;
|
||||
libraryPath: string;
|
||||
interval: ResolutionString;
|
||||
studies: string[];
|
||||
onIntervalChange: (interval: string) => void;
|
||||
onAutoSaveNeeded: OnAutoSaveNeededCallback;
|
||||
state: object | undefined;
|
||||
}) => {
|
||||
const { isMobile } = useScreenDimensions();
|
||||
const { theme } = useThemeSwitcher();
|
||||
const language = useLanguage();
|
||||
const chartContainerRef =
|
||||
useRef<HTMLDivElement>() as React.MutableRefObject<HTMLInputElement>;
|
||||
// Cant get types as charting_library is externally loaded
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const widgetRef = useRef<any>();
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||
const widgetRef = useRef<IChartingLibraryWidget>();
|
||||
|
||||
const datafeed = useDatafeed();
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const disableOnSmallScreens = isMobile ? ['left_toolbar'] : [];
|
||||
const prevMarketId = usePrevious(marketId);
|
||||
const prevTheme = usePrevious(theme);
|
||||
|
||||
const overrides = getOverrides(theme);
|
||||
|
||||
const widgetOptions = {
|
||||
symbol: marketId,
|
||||
datafeed,
|
||||
interval: interval,
|
||||
container: chartContainerRef.current,
|
||||
library_path: libraryPath,
|
||||
custom_css_url: 'vega_styles.css',
|
||||
// Trading view accepts just 'en' rather than 'en-US' which is what react-i18next provides
|
||||
// https://www.tradingview.com/charting-library-docs/latest/core_concepts/Localization?_highlight=language#supported-languages
|
||||
locale: language.split('-')[0],
|
||||
enabled_features: ['tick_resolution'],
|
||||
disabled_features: [
|
||||
'header_symbol_search',
|
||||
'header_compare',
|
||||
'show_object_tree',
|
||||
'timeframes_toolbar',
|
||||
...disableOnSmallScreens,
|
||||
],
|
||||
fullscreen: false,
|
||||
autosize: true,
|
||||
theme,
|
||||
overrides,
|
||||
loading_screen: {
|
||||
backgroundColor: overrides['paneProperties.background'],
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore parent component loads TradingView onto window obj
|
||||
widgetRef.current = new window.TradingView.widget(widgetOptions);
|
||||
|
||||
widgetRef.current.onChartReady(() => {
|
||||
widgetRef.current.applyOverrides(getOverrides(theme));
|
||||
|
||||
widgetRef.current.subscribe('onAutoSaveNeeded', () => {
|
||||
const studies = widgetRef.current
|
||||
.activeChart()
|
||||
.getAllStudies()
|
||||
.map((s: { id: string; name: string }) => s.name);
|
||||
onAutoSaveNeeded({ studies });
|
||||
});
|
||||
|
||||
const activeChart = widgetRef.current.activeChart();
|
||||
|
||||
// Show volume study by default, second bool arg adds it as a overlay on top of the chart
|
||||
studies.forEach((study) => {
|
||||
activeChart.createStudy(study);
|
||||
});
|
||||
|
||||
// Subscribe to interval changes so it can be persisted in chart settings
|
||||
activeChart.onIntervalChanged().subscribe(null, onIntervalChange);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (!widgetRef.current) return;
|
||||
widgetRef.current.remove();
|
||||
};
|
||||
},
|
||||
|
||||
// No theme in deps to avoid full chart reload when the theme changes
|
||||
// Instead the theme is changed programmatically in a separate useEffect
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[datafeed, marketId, language, libraryPath, isMobile]
|
||||
);
|
||||
|
||||
// Update the trading view theme every time the app theme updates, done separately
|
||||
// to avoid full chart reload
|
||||
useEffect(() => {
|
||||
if (!widgetRef.current || !widgetRef.current._ready) return;
|
||||
// Widget already created
|
||||
if (widgetRef.current !== undefined) {
|
||||
// Update the symbol if changed
|
||||
if (marketId !== prevMarketId) {
|
||||
widgetRef.current.setSymbol(
|
||||
marketId,
|
||||
(interval ? interval : '15') as TVResolutionString,
|
||||
noop
|
||||
);
|
||||
}
|
||||
|
||||
// Calling changeTheme will reset the default dark/light background to the TV default
|
||||
// so we need to re-apply the pane bg override. A promise is also required
|
||||
// https://github.com/tradingview/charting_library/issues/6546#issuecomment-1139517908
|
||||
widgetRef.current.changeTheme(theme).then(() => {
|
||||
widgetRef.current.applyOverrides(getOverrides(theme));
|
||||
// Update theme theme if changed
|
||||
if (theme !== prevTheme) {
|
||||
widgetRef.current.changeTheme(theme).then(() => {
|
||||
if (!widgetRef.current) return;
|
||||
widgetRef.current.applyOverrides(getOverrides(theme));
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chartContainerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create widget
|
||||
const overrides = getOverrides(theme);
|
||||
|
||||
const disabledOnSmallScreens: ChartingLibraryFeatureset[] = isMobile
|
||||
? ['left_toolbar']
|
||||
: [];
|
||||
const disabledFeatures: ChartingLibraryFeatureset[] = [
|
||||
'header_symbol_search',
|
||||
'header_compare',
|
||||
'show_object_tree',
|
||||
'timeframes_toolbar',
|
||||
...disabledOnSmallScreens,
|
||||
];
|
||||
|
||||
const widgetOptions: ChartingLibraryWidgetOptions = {
|
||||
symbol: marketId,
|
||||
datafeed,
|
||||
interval: interval as TVResolutionString,
|
||||
container: chartContainerRef.current,
|
||||
library_path: libraryPath,
|
||||
custom_css_url: 'vega_styles.css',
|
||||
// Trading view accepts just 'en' rather than 'en-US' which is what react-i18next provides
|
||||
// https://www.tradingview.com/charting-library-docs/latest/core_concepts/Localization?_highlight=language#supported-languages
|
||||
locale: language.split('-')[0] as LanguageCode,
|
||||
enabled_features: ['tick_resolution'],
|
||||
disabled_features: disabledFeatures,
|
||||
fullscreen: false,
|
||||
autosize: true,
|
||||
theme,
|
||||
overrides,
|
||||
loading_screen: {
|
||||
backgroundColor: overrides['paneProperties.background'],
|
||||
},
|
||||
auto_save_delay: 1,
|
||||
saved_data: state,
|
||||
};
|
||||
|
||||
widgetRef.current = new window.TradingView.widget(widgetOptions);
|
||||
|
||||
widgetRef.current.onChartReady(() => {
|
||||
if (!widgetRef.current) return;
|
||||
|
||||
const activeChart = widgetRef.current.activeChart();
|
||||
|
||||
if (!state) {
|
||||
// If chart has loaded with no state, create a volume study
|
||||
activeChart.createStudy('Volume');
|
||||
}
|
||||
|
||||
// Subscribe to interval changes so it can be persisted in chart settings
|
||||
activeChart.onIntervalChanged().subscribe(null, onIntervalChange);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
widgetRef.current.subscribe('onAutoSaveNeeded', () => {
|
||||
if (!widgetRef.current) return;
|
||||
|
||||
widgetRef.current.save((newState) => {
|
||||
onAutoSaveNeeded(newState);
|
||||
});
|
||||
});
|
||||
}, [
|
||||
state,
|
||||
datafeed,
|
||||
interval,
|
||||
prevTheme,
|
||||
prevMarketId,
|
||||
marketId,
|
||||
theme,
|
||||
language,
|
||||
libraryPath,
|
||||
isMobile,
|
||||
onAutoSaveNeeded,
|
||||
onIntervalChange,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (!widgetRef.current) return;
|
||||
widgetRef.current.remove();
|
||||
widgetRef.current = undefined;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div ref={chartContainerRef} className="w-full h-full" />;
|
||||
};
|
||||
|
||||
const getOverrides = (theme: 'dark' | 'light') => {
|
||||
const getOverrides = (
|
||||
theme: 'dark' | 'light'
|
||||
): Partial<ChartPropertiesOverrides> => {
|
||||
return {
|
||||
// colors set here, trading view lets the user set a color
|
||||
'paneProperties.background': theme === 'dark' ? '#05060C' : '#fff',
|
||||
|
@ -2,15 +2,6 @@ import { useEffect, useMemo, useRef } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { type Subscription } from 'zen-observable-ts';
|
||||
/*
|
||||
* TODO: figure out how we can get the chart types
|
||||
import {
|
||||
type LibrarySymbolInfo,
|
||||
type IBasicDataFeed,
|
||||
type ResolutionString,
|
||||
type SeriesFormat,
|
||||
} from '../charting_library/charting_library';
|
||||
*/
|
||||
import {
|
||||
GetBarsDocument,
|
||||
LastBarDocument,
|
||||
@ -27,6 +18,12 @@ import {
|
||||
type SymbolQueryVariables,
|
||||
} from './__generated__/Symbol';
|
||||
import { getMarketExpiryDate, toBigNum } from '@vegaprotocol/utils';
|
||||
import {
|
||||
type IBasicDataFeed,
|
||||
type DatafeedConfiguration,
|
||||
type LibrarySymbolInfo,
|
||||
type ResolutionString,
|
||||
} from '../charting-library';
|
||||
|
||||
const EXCHANGE = 'VEGA';
|
||||
|
||||
@ -42,12 +39,8 @@ const resolutionMap: Record<string, Interval> = {
|
||||
|
||||
const supportedResolutions = Object.keys(resolutionMap);
|
||||
|
||||
const configurationData = {
|
||||
// only showing Vega ofc
|
||||
exchanges: [EXCHANGE],
|
||||
|
||||
const configurationData: DatafeedConfiguration = {
|
||||
// Represents the resolutions for bars supported by your datafeed
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
supported_resolutions: supportedResolutions as ResolutionString[],
|
||||
} as const;
|
||||
|
||||
@ -57,9 +50,7 @@ export const useDatafeed = () => {
|
||||
const client = useApolloClient();
|
||||
|
||||
const datafeed = useMemo(() => {
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
const feed: IBasicDataFeed = {
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
onReady: (callback) => {
|
||||
setTimeout(() => callback(configurationData));
|
||||
},
|
||||
@ -69,11 +60,8 @@ export const useDatafeed = () => {
|
||||
},
|
||||
|
||||
resolveSymbol: async (
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
marketId,
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
onSymbolResolvedCallback,
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
onResolveErrorCallback
|
||||
) => {
|
||||
try {
|
||||
@ -110,9 +98,8 @@ export const useDatafeed = () => {
|
||||
const expirationDate = getMarketExpiryDate(instrument.metadata.tags);
|
||||
const expirationTimestamp = expirationDate
|
||||
? Math.floor(expirationDate.getTime() / 1000)
|
||||
: null;
|
||||
: undefined;
|
||||
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
const symbolInfo: LibrarySymbolInfo = {
|
||||
ticker: market.id, // use ticker as our unique identifier so that code/name can be used for name/description
|
||||
name: instrument.code,
|
||||
@ -120,10 +107,9 @@ export const useDatafeed = () => {
|
||||
description: instrument.name,
|
||||
listed_exchange: EXCHANGE,
|
||||
expired: productType === 'Perpetual' ? false : true,
|
||||
expirationDate: expirationTimestamp,
|
||||
expiration_date: expirationTimestamp,
|
||||
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
format: 'price' as SeriesFormat,
|
||||
format: 'price',
|
||||
type,
|
||||
session: '24x7',
|
||||
timezone: 'Etc/UTC',
|
||||
@ -151,15 +137,10 @@ export const useDatafeed = () => {
|
||||
},
|
||||
|
||||
getBars: async (
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
symbolInfo,
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
resolution,
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
periodParams,
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
onHistoryCallback,
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
onErrorCallback
|
||||
) => {
|
||||
if (!symbolInfo.ticker) {
|
||||
@ -211,13 +192,9 @@ export const useDatafeed = () => {
|
||||
},
|
||||
|
||||
subscribeBars: (
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
symbolInfo,
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
resolution,
|
||||
// @ts-ignore cant import types as chartin_library is external
|
||||
onTick
|
||||
|
||||
// subscriberUID, // chart will subscribe and unsbuscribe when the parent market of the page changes so we don't need to use subscriberUID as of now
|
||||
) => {
|
||||
if (!symbolInfo.ticker) {
|
||||
|
@ -16,6 +16,7 @@ type TradingButtonProps = {
|
||||
subLabel?: ReactNode;
|
||||
fill?: boolean;
|
||||
minimal?: boolean;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
const getClassName = (
|
||||
@ -120,6 +121,7 @@ export const TradingButton = forwardRef<
|
||||
className,
|
||||
subLabel,
|
||||
fill,
|
||||
testId,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@ -132,6 +134,7 @@ export const TradingButton = forwardRef<
|
||||
{ size, subLabel, intent, fill, minimal },
|
||||
className
|
||||
)}
|
||||
data-testid={testId}
|
||||
{...props}
|
||||
>
|
||||
<Content icon={icon} subLabel={subLabel} children={children} />
|
||||
|
Loading…
Reference in New Issue
Block a user