feat: add datafeed hook for trading view

This commit is contained in:
Matthew Russell
2023-12-12 14:18:28 -08:00
parent 65d8699027
commit fdb0d03ec0
16 changed files with 680 additions and 215 deletions
@@ -192,7 +192,7 @@ const MainGrid = memo(
</ResizableGridPanel>
</ResizableGrid>
);
}
},
);
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 (
@@ -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,
@@ -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(<CandlesMenu />);
render(<ChartMenu />);
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(<CandlesMenu />);
render(<ChartMenu />);
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(<CandlesMenu />);
render(<ChartMenu />);
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',
);
});
});
@@ -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, IconName>([
[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 <TradingViewContainer marketId={marketId} />;
}
case 'pennant': {
return (
<CandlesChartContainer
marketId={marketId}
interval={interval}
chartType={chartType}
overlays={overlays}
studies={studies}
studySizes={studySizes}
setStudySizes={setStudySizes}
setStudies={setStudies}
setOverlays={setOverlays}
defaultStudySize={STUDY_SIZE}
/>
);
}
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 = (
<TradingDropdown
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>{chartlib}</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent align={contentAlign}>
<TradingDropdownRadioGroup
value={chartlib}
onValueChange={(value) => {
// @ts-ignore need to cast to Chartlib
setChartlib(value);
}}
>
<TradingDropdownRadioItem inset value="tradingview">
Tradingview
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
<TradingDropdownRadioItem inset value="pennant">
Pennant
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
</TradingDropdownRadioGroup>
</TradingDropdownContent>
</TradingDropdown>
);
switch (chartlib) {
case 'tradingview': {
return <>{chartlibDropdown}</>;
}
case 'pennant': {
return (
<>
{chartlibDropdown}
<TradingDropdown
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
{t('Interval: {{interval}}', {
interval: intervalLabels[interval],
})}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent align={contentAlign}>
<TradingDropdownRadioGroup
value={interval}
onValueChange={(value) => {
setInterval(value as Interval);
}}
>
{Object.values(Interval).map((timeInterval) => (
<TradingDropdownRadioItem
key={timeInterval}
inset
value={timeInterval}
>
{intervalLabels[timeInterval]}
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
))}
</TradingDropdownRadioGroup>
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
<Icon name={chartTypeIcon.get(chartType) as IconName} />
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent align={contentAlign}>
<TradingDropdownRadioGroup
value={chartType}
onValueChange={(value) => {
setType(value as ChartType);
}}
>
{Object.values(ChartType).map((type) => (
<TradingDropdownRadioItem key={type} inset value={type}>
{chartTypeLabels[type]}
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
))}
</TradingDropdownRadioGroup>
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
{t('Indicators')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent align={contentAlign}>
{Object.values(Overlay).map((overlay) => (
<TradingDropdownCheckboxItem
key={overlay}
checked={overlays.includes(overlay)}
onCheckedChange={() => {
const newOverlays = [...overlays];
const index = overlays.findIndex(
(item) => item === overlay,
);
index !== -1
? newOverlays.splice(index, 1)
: newOverlays.push(overlay);
setOverlays(newOverlays);
}}
>
{overlayLabels[overlay]}
<TradingDropdownItemIndicator />
</TradingDropdownCheckboxItem>
))}
{Object.values(Study).map((study) => (
<TradingDropdownCheckboxItem
key={study}
checked={studies.includes(study)}
onCheckedChange={() => {
const newStudies = [...studies];
const index = studies.findIndex((item) => item === study);
index !== -1
? newStudies.splice(index, 1)
: newStudies.push(study);
setStudies(newStudies);
}}
>
{studyLabels[study]}
<TradingDropdownItemIndicator />
</TradingDropdownCheckboxItem>
))}
</TradingDropdownContent>
</TradingDropdown>
</>
);
}
default: {
throw new Error('invalid chart lib');
}
}
};
@@ -0,0 +1 @@
export { ChartContainer, ChartMenu } from './chart-container';
@@ -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,
};
};
@@ -0,0 +1 @@
export { TradingViewContainer } from './trading-view-container';
@@ -0,0 +1,5 @@
import { TradingView } from '@vegaprotocol/trading-view';
export const TradingViewContainer = ({ marketId }: { marketId: string }) => {
return <TradingView marketId={marketId} />;
};
+11 -1
View File
@@ -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 (
<>
<Head>
@@ -27,7 +30,14 @@ export default function Index() {
<meta name="twitter:image:alt" content="VEGA logo" />
<meta name="twitter:site" content="@vegaprotocol" />
</Head>
<ClientRouter />
<Script
src="/bundle.js"
strategy="lazyOnload"
onReady={() => {
setIsScriptReady(true);
}}
/>
{isScriptReady && <ClientRouter />}
</>
);
}
+68
View File
@@ -248,3 +248,71 @@ input::-webkit-inner-spin-button {
input[type='number'] {
-moz-appearance: textfield;
}
/* TRADING VIEW */
:root:not(.theme-dark) {
--tv-color-platform-background: #fff;
--tv-color-pane-background: #fff;
/* --tv-color-toolbar-button-background-hover: rgb(244, 143, 177); */
/* --tv-color-toolbar-button-background-expanded: rgb(244, 143, 177); */
/* --tv-color-toolbar-button-background-active: rgb(249, 185, 233); */
/* --tv-color-toolbar-button-background-active-hover: rgb(244, 143, 177); */
/* --tv-color-toolbar-button-text: rgb(136, 24, 79); */
/* --tv-color-toolbar-button-text-hover: rgb(74, 20, 140); */
/* --tv-color-toolbar-button-text-active: red; */
/* --tv-color-toolbar-button-text-active-hover: red; */
/* --tv-color-item-active-text: rgb(6, 6, 255); */
/* --tv-color-toolbar-toggle-button-background-active: red; */
/* --tv-color-toolbar-toggle-button-background-active-hover: magenta; */
/* --tv-color-toolbar-divider-background: rgb(251, 223, 244); */
/* --tv-color-toolbar-save-layout-loader: rgb(106, 109, 120); */
/* --tv-color-popup-background: rgb(241, 188, 225); */
/* --tv-color-popup-element-text: rgb(136, 24, 79); */
/* --tv-color-popup-element-text-hover: rgb(74, 20, 140); */
/* --tv-color-popup-element-background-hover: rgb(244, 143, 177); */
/* --tv-color-popup-element-divider-background: rgb(251, 223, 244); */
/* --tv-color-popup-element-secondary-text: rgb(74, 20, 140); */
/* --tv-color-popup-element-hint-text: rgb(74, 20, 140); */
/* --tv-color-popup-element-text-active: rgb(6, 6, 255); */
/* --tv-color-popup-element-background-active: red; */
/* --tv-color-popup-element-toolbox-text: rgb(136, 24, 79); */
/* --tv-color-popup-element-toolbox-text-hover: rgb(74, 20, 140); */
/* --tv-color-popup-element-toolbox-text-active-hover: rgb(74, 20, 140); */
/* --tv-color-popup-element-toolbox-background-hover: rgb(222, 89, 132); */
/* --tv-color-popup-element-toolbox-background-active-hover: magenta; */
}
.theme-dark:root {
--tv-color-platform-background: #000;
--tv-color-pane-background: #000;
/* --tv-color-toolbar-button-background-hover: rgb(244, 143, 177); */
/* --tv-color-toolbar-button-background-expanded: rgb(244, 143, 177); */
/* --tv-color-toolbar-button-background-active: rgb(249, 185, 233); */
/* --tv-color-toolbar-button-background-active-hover: rgb(244, 143, 177); */
/* --tv-color-toolbar-button-text: rgb(136, 24, 79); */
/* --tv-color-toolbar-button-text-hover: rgb(74, 20, 140); */
/* --tv-color-toolbar-button-text-active: red; */
/* --tv-color-toolbar-button-text-active-hover: red; */
/* --tv-color-item-active-text: rgb(6, 255, 6); */
/* --tv-color-toolbar-toggle-button-background-active: red; */
/* --tv-color-toolbar-toggle-button-background-active-hover: magenta; */
/* --tv-color-toolbar-divider-background: rgb(251, 223, 244); */
/* --tv-color-toolbar-save-layout-loader: rgb(134, 137, 147); */
/* --tv-color-popup-background: rgb(241, 188, 225); */
/* --tv-color-popup-element-text: rgb(136, 24, 79); */
/* --tv-color-popup-element-text-hover: rgb(74, 20, 140); */
/* --tv-color-popup-element-background-hover: rgb(244, 143, 177); */
/* --tv-color-popup-element-divider-background: rgb(251, 223, 244); */
/* --tv-color-popup-element-secondary-text: rgb(74, 20, 140); */
/* --tv-color-popup-element-hint-text: rgb(74, 20, 140); */
/* --tv-color-popup-element-text-active: rgb(6, 6, 255); */
/* --tv-color-popup-element-background-active: red; */
/* --tv-color-popup-element-toolbox-text: rgb(136, 24, 79); */
/* --tv-color-popup-element-toolbox-text-hover: rgb(74, 20, 140); */
/* --tv-color-popup-element-toolbox-text-active-hover: rgb(74, 20, 140); */
/* --tv-color-popup-element-toolbox-background-hover: rgb(222, 89, 132); */
/* --tv-color-popup-element-toolbox-background-active-hover: magenta; */
}
+20 -17
View File
@@ -6,45 +6,48 @@ import { useMemo } from 'react';
import debounce from 'lodash/debounce';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
STUDY_SIZE,
useCandlesChartSettings,
} from './use-candles-chart-settings';
import { useT } from './use-t';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
export type CandlesChartContainerProps = {
marketId: string;
interval: any;
chartType: any;
overlays: any;
studies: any;
studySizes: any;
defaultStudySize: number;
setStudies: any;
setStudySizes: any;
setOverlays: any;
};
const CANDLES_TO_WIDTH_FACTOR = 0.2;
export const CandlesChartContainer = ({
marketId,
interval,
chartType,
overlays,
studies,
studySizes,
defaultStudySize,
setStudies,
setStudySizes,
setOverlays,
}: CandlesChartContainerProps) => {
const client = useApolloClient();
const { pubKey } = useVegaWallet();
const { theme } = useThemeSwitcher();
const t = useT();
const {
interval,
chartType,
overlays,
studies,
studySizes,
setStudies,
setStudySizes,
setOverlays,
} = useCandlesChartSettings();
const handlePaneChange = useMemo(
() =>
debounce((sizes: number[]) => {
// first number is main pain, which is greedy so we don't store it
setStudySizes(sizes.filter((_, i) => i !== 0));
}, 300),
[setStudySizes]
[setStudySizes],
);
const dataSource = useMemo(() => {
@@ -69,7 +72,7 @@ export const CandlesChartContainer = ({
</span>
),
initialNumCandlesToDisplay: candlesCount,
studySize: STUDY_SIZE,
studySize: defaultStudySize,
studySizes,
}}
interval={interval}
-161
View File
@@ -1,161 +0,0 @@
import 'pennant/dist/style.css';
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 { useCandlesChartSettings } from './use-candles-chart-settings';
import { useT } from './use-t';
const chartTypeIcon = new Map<ChartType, IconName>([
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
[ChartType.CANDLE, IconNames.WATERFALL_CHART],
[ChartType.LINE, IconNames.TIMELINE_LINE_CHART],
[ChartType.OHLC, IconNames.WATERFALL_CHART],
]);
export const CandlesMenu = () => {
const {
interval,
chartType,
studies,
overlays,
setInterval,
setType,
setStudies,
setOverlays,
} = useCandlesChartSettings();
const t = useT();
const triggerClasses = 'text-xs';
const contentAlign = 'end';
const triggerButtonProps = { size: 'extra-small' } as const;
return (
<>
<TradingDropdown
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
{t('Interval: {{interval}}', {
interval: intervalLabels[interval],
})}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent align={contentAlign}>
<TradingDropdownRadioGroup
value={interval}
onValueChange={(value) => {
setInterval(value as Interval);
}}
>
{Object.values(Interval).map((timeInterval) => (
<TradingDropdownRadioItem
key={timeInterval}
inset
value={timeInterval}
>
{intervalLabels[timeInterval]}
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
))}
</TradingDropdownRadioGroup>
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
<Icon name={chartTypeIcon.get(chartType) as IconName} />
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent align={contentAlign}>
<TradingDropdownRadioGroup
value={chartType}
onValueChange={(value) => {
setType(value as ChartType);
}}
>
{Object.values(ChartType).map((type) => (
<TradingDropdownRadioItem key={type} inset value={type}>
{chartTypeLabels[type]}
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
))}
</TradingDropdownRadioGroup>
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
{t('Indicators')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent align={contentAlign}>
{Object.values(Overlay).map((overlay) => (
<TradingDropdownCheckboxItem
key={overlay}
checked={overlays.includes(overlay)}
onCheckedChange={() => {
const newOverlays = [...overlays];
const index = overlays.findIndex((item) => item === overlay);
index !== -1
? newOverlays.splice(index, 1)
: newOverlays.push(overlay);
setOverlays(newOverlays);
}}
>
{overlayLabels[overlay]}
<TradingDropdownItemIndicator />
</TradingDropdownCheckboxItem>
))}
{Object.values(Study).map((study) => (
<TradingDropdownCheckboxItem
key={study}
checked={studies.includes(study)}
onCheckedChange={() => {
const newStudies = [...studies];
const index = studies.findIndex((item) => item === study);
index !== -1
? newStudies.splice(index, 1)
: newStudies.push(study);
setStudies(newStudies);
}}
>
{studyLabels[study]}
<TradingDropdownItemIndicator />
</TradingDropdownCheckboxItem>
))}
</TradingDropdownContent>
</TradingDropdown>
</>
);
};
-1
View File
@@ -1,5 +1,4 @@
export * from './__generated__/Candles';
export * from './__generated__/Chart';
export * from './candles-chart';
export * from './candles-menu';
export * from './data-source';
+214
View File
@@ -0,0 +1,214 @@
import { useEffect, useMemo } from 'react';
import {
type LibrarySymbolInfo,
type IBasicDataFeed,
type ResolutionString,
type SeriesFormat,
} from '../charting_library/charting_library';
type Candle = {
start: string;
lastUpdate: string;
high: string;
low: string;
open: string;
close: string;
volume: string;
notional: string;
};
/* eslint-disable no-console */
const url = 'https://api.n07.testnet.vega.xyz/api/v2';
const websocketUrl = 'wss://api.n07.testnet.vega.xyz/api/v2/stream/candle/data';
let socket: WebSocket;
const configurationData = {
// only showing Vega ofc
exchanges: undefined,
// Represents the resolutions for bars supported by your datafeed
supported_resolutions: ['1'] as ResolutionString[],
} as const;
const resolutionMap: Record<ResolutionString, (marketId: string) => string> = {
// @ts-ignore something something
'1': (marketId: string) => `trades_candle_1_minute_${marketId}`,
};
export const useDataFeed = (marketId: string) => {
const datafeed = useMemo(() => {
const feed: IBasicDataFeed = {
onReady: (callback) => {
console.log('[onReady]: Method call');
setTimeout(() => callback(configurationData));
},
searchSymbols: () => {
console.log('[searchSymbols]: Method call');
},
resolveSymbol: async (
marketId,
onSymbolResolvedCallback,
onResolveErrorCallback,
) => {
console.log('[resolveSymbol]: Method call', marketId);
try {
const data = await request('market/' + marketId);
const symbolInfo: LibrarySymbolInfo = {
ticker: data.market.id,
name: data.market.tradableInstrument.instrument.code,
full_name: data.market.tradableInstrument.instrument.code,
listed_exchange: 'vega',
format: 'price' as SeriesFormat,
description: data.market.tradableInstrument.instrument.name,
type: 'futures',
session: '24x7',
timezone: 'Etc/UTC',
exchange: '',
minmov: 1,
pricescale: 100,
visible_plots_set: 'ohlc',
supported_resolutions: ['1'] as ResolutionString[],
volume_precision: data.market.positionDecimalPlaces,
data_status: 'pulsed',
delay: 1000,
has_intraday: true, // required for less than 1 day interval
};
onSymbolResolvedCallback(symbolInfo);
} catch (err) {
onResolveErrorCallback('Cannot resolve symbol');
}
},
getBars: async (
symbolInfo,
resolution,
periodParams,
onHistoryCallback,
onErrorCallback,
) => {
console.log('HERE', resolution, symbolInfo);
if (!symbolInfo.ticker) {
onErrorCallback('No symbol.ticker');
return;
}
try {
const candleId = resolutionMap[resolution](symbolInfo.ticker);
console.log(candleId);
const data = await request(`candle?candleId=${candleId}`);
if (!data.candles.edges.length) {
onHistoryCallback([], { noData: true });
return;
}
const bars = data.candles.edges
.map((e: { node: Candle }) => {
return prepareBar(e.node);
})
.reverse();
console.log(bars);
onHistoryCallback(bars, { noData: false });
} catch (err) {
console.log('Cannot getBars', err);
onErrorCallback(err as Error);
}
},
subscribeBars: (
symbolInfo,
resolution,
onTick,
subscriberUID,
onResetCacheNeededCallback,
) => {
console.log(
'[subscribeBars]: Method call with subscriberUID:',
subscriberUID,
);
if (!symbolInfo.ticker) {
console.error('No symbolInfo.ticker');
return;
}
const candleId = resolutionMap[resolution](symbolInfo.ticker);
socket = new WebSocket(`${websocketUrl}?candleId=${candleId}`);
socket.onopen = (event) => {
console.log('open', event);
};
socket.onmessage = (event) => {
console.log('message', event);
try {
// TODO: update or append to candle set
const data = JSON.parse(event.data);
const bar = prepareBar(data.result.candle);
onTick(bar);
} catch (err) {
console.error(err);
}
};
socket.onerror = (event) => {
console.log('error', event);
};
socket.onclose = (event) => {
console.log('close', event);
};
},
unsubscribeBars: (subscriberUID) => {
console.log(
'[unsubscribeBars]: Method call with subscriberUID:',
subscriberUID,
);
if (socket) {
socket.close();
}
},
};
return feed;
}, []);
useEffect(() => {
return () => {
if (socket) {
socket.close();
}
};
}, []);
return datafeed;
};
const prepareBar = (candle: Candle) => {
return {
time: Number(candle.start.slice(0, -6)), // trim to milliseconds
low: Number(candle.low),
high: Number(candle.high),
open: Number(candle.open),
close: Number(candle.close),
volume: Number(candle.volume),
};
};
const request = async (path: string) => {
try {
const res = await fetch(`${url}/${path}`);
const json = await res.json();
return json;
console.log(json);
} catch (err) {
console.error(err);
}
};
+71 -8
View File
@@ -1,9 +1,72 @@
export function TradingView() {
return (
<div>
<h1>Welcome to TradingView!</h1>
</div>
);
}
import { useEffect, useRef } from 'react';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import {
type ChartingLibraryWidgetOptions,
type LanguageCode,
type ResolutionString,
widget,
} from '../charting_library';
import { useDataFeed } from './datafeed';
export default TradingView;
export const TradingView = ({ marketId }: { marketId: string }) => {
const { theme } = useThemeSwitcher();
const chartContainerRef =
useRef<HTMLDivElement>() as React.MutableRefObject<HTMLInputElement>;
const datafeed = useDataFeed(marketId);
useEffect(() => {
const widgetOptions: ChartingLibraryWidgetOptions = {
symbol: marketId,
// BEWARE: no trailing slash is expected in feed URL
// eslint-disable-next-line
datafeed,
interval: '1' as ResolutionString,
container: chartContainerRef.current,
library_path: '/charting_library/',
locale: 'en' as LanguageCode,
disabled_features: [
'use_localstorage_for_settings',
'header_symbol_search',
'header_compare',
],
enabled_features: ['study_templates'],
charts_storage_url: 'https://saveload.tradingview.com',
charts_storage_api_version: '1.1',
client_id: 'tradingview.com',
user_id: 'public_user_id',
fullscreen: false,
autosize: true,
theme,
};
const tvWidget = new widget(widgetOptions);
// Add a custom button
tvWidget.onChartReady(() => {
tvWidget.headerReady().then(() => {
const button = tvWidget.createButton();
button.setAttribute('title', 'Click to show a notification popup');
button.classList.add('apply-common-tooltip');
button.addEventListener('click', () =>
tvWidget.showNoticeDialog({
title: 'Notification',
body: 'TradingView Charting Library API works correctly',
callback: () => {
console.log('Noticed!');
},
}),
);
button.innerHTML = 'Check API';
});
});
return () => {
tvWidget.remove();
};
}, [theme, datafeed, marketId]);
return <div ref={chartContainerRef} className="w-full h-full" />;
};
+1 -1
View File
@@ -57,7 +57,7 @@
"@vegaprotocol/wallet": ["libs/wallet/src/index.ts"],
"@vegaprotocol/web3": ["libs/web3/src/index.ts"],
"@vegaprotocol/withdraws": ["libs/withdraws/src/index.ts"],
"trading-view": ["libs/trading-view/src/index.ts"]
"@vegaprotocol/trading-view": ["libs/trading-view/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"],