feat: add datafeed hook for trading view
This commit is contained in:
@@ -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,
|
||||
|
||||
+12
-12
@@ -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';
|
||||
+17
-6
@@ -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} />;
|
||||
};
|
||||
@@ -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 />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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; */
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,5 +1,4 @@
|
||||
export * from './__generated__/Candles';
|
||||
export * from './__generated__/Chart';
|
||||
export * from './candles-chart';
|
||||
export * from './candles-menu';
|
||||
export * from './data-source';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user