feat(trading): trading view (#5348)
This commit is contained in:
parent
0796f2b31f
commit
f178b85846
@ -25,3 +25,6 @@ NX_ICEBERG_ORDERS=true
|
|||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
NX_METAMASK_SNAPS=true
|
NX_METAMASK_SNAPS=true
|
||||||
NX_REFERRALS=true
|
NX_REFERRALS=true
|
||||||
|
|
||||||
|
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
||||||
|
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
||||||
|
@ -28,3 +28,6 @@ NX_REFERRALS=true
|
|||||||
|
|
||||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||||
|
|
||||||
|
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
||||||
|
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
||||||
|
@ -62,10 +62,10 @@ const MainGrid = memo(
|
|||||||
id="chart"
|
id="chart"
|
||||||
overflowHidden
|
overflowHidden
|
||||||
name={t('Chart')}
|
name={t('Chart')}
|
||||||
menu={<TradingViews.candles.menu />}
|
menu={<TradingViews.chart.menu />}
|
||||||
>
|
>
|
||||||
<ErrorBoundary feature="chart">
|
<ErrorBoundary feature="chart">
|
||||||
<TradingViews.candles.component marketId={marketId} />
|
<TradingViews.chart.component marketId={marketId} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="depth" name={t('Depth')}>
|
<Tab id="depth" name={t('Depth')}>
|
||||||
|
@ -23,7 +23,7 @@ interface TradePanelsProps {
|
|||||||
|
|
||||||
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||||
const [view, setView] = useState<TradingView>('candles');
|
const [view, setView] = useState<TradingView>('chart');
|
||||||
|
|
||||||
const renderView = () => {
|
const renderView = () => {
|
||||||
const Component = TradingViews[view].component;
|
const Component = TradingViews[view].component;
|
||||||
@ -50,7 +50,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
|||||||
const Menu = viewCfg.menu;
|
const Menu = viewCfg.menu;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
|
<div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
|
||||||
<Menu />
|
<Menu />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -149,7 +149,7 @@ const useViewLabel = (view: TradingView) => {
|
|||||||
const t = useT();
|
const t = useT();
|
||||||
|
|
||||||
const labels = {
|
const labels = {
|
||||||
candles: t('Candles'),
|
chart: t('Chart'),
|
||||||
depth: t('Depth'),
|
depth: t('Depth'),
|
||||||
liquidity: t('Liquidity'),
|
liquidity: t('Liquidity'),
|
||||||
funding: t('Funding'),
|
funding: t('Funding'),
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||||
import {
|
|
||||||
CandlesChartContainer,
|
|
||||||
CandlesMenu,
|
|
||||||
} from '@vegaprotocol/candles-chart';
|
|
||||||
import { Filter, OpenOrdersMenu } from '@vegaprotocol/orders';
|
import { Filter, OpenOrdersMenu } from '@vegaprotocol/orders';
|
||||||
import { TradesContainer } from '../../components/trades-container';
|
import { TradesContainer } from '../../components/trades-container';
|
||||||
import { OrderbookContainer } from '../../components/orderbook-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 { StopOrdersContainer } from '../../components/stop-orders-container';
|
||||||
import { AccountsMenu } from '../../components/accounts-menu';
|
import { AccountsMenu } from '../../components/accounts-menu';
|
||||||
import { PositionsMenu } from '../../components/positions-menu';
|
import { PositionsMenu } from '../../components/positions-menu';
|
||||||
|
import { ChartContainer, ChartMenu } from '../../components/chart-container';
|
||||||
|
|
||||||
export type TradingView = keyof typeof TradingViews;
|
export type TradingView = keyof typeof TradingViews;
|
||||||
|
|
||||||
export const TradingViews = {
|
export const TradingViews = {
|
||||||
candles: {
|
chart: {
|
||||||
component: CandlesChartContainer,
|
component: ChartContainer,
|
||||||
menu: CandlesMenu,
|
menu: ChartMenu,
|
||||||
},
|
},
|
||||||
depth: {
|
depth: {
|
||||||
component: DepthChartContainer,
|
component: DepthChartContainer,
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { ChartContainer } from './chart-container';
|
||||||
|
import { useChartSettingsStore } from './use-chart-settings';
|
||||||
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/candles-chart', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/candles-chart'),
|
||||||
|
CandlesChartContainer: ({ marketId }: { marketId: string }) => (
|
||||||
|
<div data-testid="pennant">{marketId}</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/trading-view', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/trading-view'),
|
||||||
|
TradingViewContainer: ({ marketId }: { marketId: string }) => (
|
||||||
|
<div data-testid="tradingview">{marketId}</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ChartContainer', () => {
|
||||||
|
it('renders pennant if no library path is set', () => {
|
||||||
|
useChartSettingsStore.setState({
|
||||||
|
chartlib: 'tradingview',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEnvironment.setState({
|
||||||
|
CHARTING_LIBRARY_PATH: undefined,
|
||||||
|
CHARTING_LIBRARY_HASH: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketId = 'market-id';
|
||||||
|
|
||||||
|
render(<ChartContainer marketId={marketId} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('pennant')).toHaveTextContent(marketId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders trading view if library path is set', () => {
|
||||||
|
useChartSettingsStore.setState({
|
||||||
|
chartlib: 'tradingview',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEnvironment.setState({
|
||||||
|
CHARTING_LIBRARY_PATH: 'dummy-path',
|
||||||
|
CHARTING_LIBRARY_HASH: 'hash',
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketId = 'market-id';
|
||||||
|
|
||||||
|
render(<ChartContainer marketId={marketId} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('tradingview')).toHaveTextContent(marketId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders pennant chart if stored in settings', () => {
|
||||||
|
useChartSettingsStore.setState({
|
||||||
|
chartlib: 'pennant',
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketId = 'market-id';
|
||||||
|
|
||||||
|
render(<ChartContainer marketId={marketId} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('pennant')).toHaveTextContent(marketId);
|
||||||
|
});
|
||||||
|
});
|
120
apps/trading/components/chart-container/chart-container.tsx
Normal file
120
apps/trading/components/chart-container/chart-container.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import invert from 'lodash/invert';
|
||||||
|
import { type Interval } from '@vegaprotocol/types';
|
||||||
|
import {
|
||||||
|
TradingViewContainer,
|
||||||
|
ALLOWED_TRADINGVIEW_HOSTNAMES,
|
||||||
|
TRADINGVIEW_INTERVAL_MAP,
|
||||||
|
} from '@vegaprotocol/trading-view';
|
||||||
|
import {
|
||||||
|
CandlesChartContainer,
|
||||||
|
PENNANT_INTERVAL_MAP,
|
||||||
|
} from '@vegaprotocol/candles-chart';
|
||||||
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
|
import { useChartSettings, STUDY_SIZE } from './use-chart-settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders either the pennant chart or the tradingview chart
|
||||||
|
*/
|
||||||
|
export const ChartContainer = ({ marketId }: { marketId: string }) => {
|
||||||
|
const { CHARTING_LIBRARY_PATH, CHARTING_LIBRARY_HASH } = useEnvironment();
|
||||||
|
|
||||||
|
const {
|
||||||
|
chartlib,
|
||||||
|
interval,
|
||||||
|
chartType,
|
||||||
|
overlays,
|
||||||
|
studies,
|
||||||
|
studySizes,
|
||||||
|
tradingViewStudies,
|
||||||
|
setInterval,
|
||||||
|
setStudies,
|
||||||
|
setStudySizes,
|
||||||
|
setOverlays,
|
||||||
|
setTradingViewStudies,
|
||||||
|
} = useChartSettings();
|
||||||
|
|
||||||
|
const pennantChart = (
|
||||||
|
<CandlesChartContainer
|
||||||
|
marketId={marketId}
|
||||||
|
interval={toPennantInterval(interval)}
|
||||||
|
chartType={chartType}
|
||||||
|
overlays={overlays}
|
||||||
|
studies={studies}
|
||||||
|
studySizes={studySizes}
|
||||||
|
setStudySizes={setStudySizes}
|
||||||
|
setStudies={setStudies}
|
||||||
|
setOverlays={setOverlays}
|
||||||
|
defaultStudySize={STUDY_SIZE}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ALLOWED_TRADINGVIEW_HOSTNAMES.includes(window.location.hostname)) {
|
||||||
|
return pennantChart;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CHARTING_LIBRARY_PATH || !CHARTING_LIBRARY_HASH) {
|
||||||
|
return pennantChart;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (chartlib) {
|
||||||
|
case 'tradingview': {
|
||||||
|
return (
|
||||||
|
<TradingViewContainer
|
||||||
|
libraryPath={CHARTING_LIBRARY_PATH}
|
||||||
|
libraryHash={CHARTING_LIBRARY_HASH}
|
||||||
|
marketId={marketId}
|
||||||
|
interval={toTradingViewResolution(interval)}
|
||||||
|
studies={tradingViewStudies}
|
||||||
|
onIntervalChange={(newInterval) => {
|
||||||
|
setInterval(fromTradingViewResolution(newInterval));
|
||||||
|
}}
|
||||||
|
onAutoSaveNeeded={(data: { studies: string[] }) => {
|
||||||
|
setTradingViewStudies(data.studies);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'pennant': {
|
||||||
|
return pennantChart;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error('invalid chart lib');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTradingViewResolution = (interval: Interval) => {
|
||||||
|
const resolution = TRADINGVIEW_INTERVAL_MAP[interval];
|
||||||
|
|
||||||
|
if (!resolution) {
|
||||||
|
throw new Error(
|
||||||
|
`failed to convert interval: ${interval} to valid resolution`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolution;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromTradingViewResolution = (resolution: string) => {
|
||||||
|
const interval = invert(TRADINGVIEW_INTERVAL_MAP)[resolution];
|
||||||
|
|
||||||
|
if (!interval) {
|
||||||
|
throw new Error(
|
||||||
|
`failed to convert resolution: ${resolution} to valid interval`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return interval as Interval;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toPennantInterval = (interval: Interval) => {
|
||||||
|
const pennantInterval = PENNANT_INTERVAL_MAP[interval];
|
||||||
|
|
||||||
|
if (!pennantInterval) {
|
||||||
|
throw new Error(
|
||||||
|
`failed to convert interval: ${interval} to valid pennant interval`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pennantInterval;
|
||||||
|
};
|
140
apps/trading/components/chart-container/chart-menu.spec.tsx
Normal file
140
apps/trading/components/chart-container/chart-menu.spec.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { render, screen, within } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { ChartMenu } from './chart-menu';
|
||||||
|
import {
|
||||||
|
useChartSettingsStore,
|
||||||
|
DEFAULT_CHART_SETTINGS,
|
||||||
|
} from './use-chart-settings';
|
||||||
|
import { Overlay, Study, overlayLabels, studyLabels } from 'pennant';
|
||||||
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
|
describe('ChartMenu', () => {
|
||||||
|
it('doesnt show trading view option if library path undefined', () => {
|
||||||
|
useEnvironment.setState({ CHARTING_LIBRARY_PATH: undefined });
|
||||||
|
|
||||||
|
render(<ChartMenu />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('button', { name: 'TradingView' })
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('button', { name: 'Vega chart' })
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can switch between charts if library path', async () => {
|
||||||
|
useEnvironment.setState({ CHARTING_LIBRARY_PATH: 'dummy' });
|
||||||
|
|
||||||
|
render(<ChartMenu />);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'TradingView' }));
|
||||||
|
expect(useChartSettingsStore.getState().chartlib).toEqual('tradingview');
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'Vega chart' }));
|
||||||
|
expect(useChartSettingsStore.getState().chartlib).toEqual('pennant');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tradingview', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useEnvironment.setState({ CHARTING_LIBRARY_PATH: 'dummy-path' });
|
||||||
|
|
||||||
|
// clear store each time to avoid conditional testing of defaults
|
||||||
|
useChartSettingsStore.setState({
|
||||||
|
chartlib: 'tradingview',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only shows chartlib switch and attribution', () => {
|
||||||
|
render(<ChartMenu />);
|
||||||
|
|
||||||
|
const buttons = screen.getAllByRole('button');
|
||||||
|
expect(buttons).toHaveLength(1);
|
||||||
|
expect(buttons[0]).toHaveTextContent('Vega chart');
|
||||||
|
|
||||||
|
expect(screen.getByText('Chart by')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pennant', () => {
|
||||||
|
const openDropdown = async () => {
|
||||||
|
await userEvent.click(
|
||||||
|
screen.getByRole('button', {
|
||||||
|
name: 'Indicators',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// clear store each time to avoid conditional testing of defaults
|
||||||
|
useChartSettingsStore.setState({
|
||||||
|
chartlib: 'pennant',
|
||||||
|
overlays: [],
|
||||||
|
studies: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(Object.values(Overlay))('can set %s overlay', async (overlay) => {
|
||||||
|
render(<ChartMenu />);
|
||||||
|
|
||||||
|
await openDropdown();
|
||||||
|
|
||||||
|
const menu = within(await screen.findByRole('menu'));
|
||||||
|
|
||||||
|
await userEvent.click(menu.getByText(overlayLabels[overlay as Overlay]));
|
||||||
|
|
||||||
|
// re-open the dropdown
|
||||||
|
await openDropdown();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(overlayLabels[overlay as Overlay])
|
||||||
|
).toHaveAttribute('data-state', 'checked');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(Object.values(Study))('can set %s study', async (study) => {
|
||||||
|
render(<ChartMenu />);
|
||||||
|
|
||||||
|
await openDropdown();
|
||||||
|
|
||||||
|
const menu = within(await screen.findByRole('menu'));
|
||||||
|
|
||||||
|
await userEvent.click(menu.getByText(studyLabels[study as Study]));
|
||||||
|
|
||||||
|
// re-open the dropdown
|
||||||
|
await openDropdown();
|
||||||
|
|
||||||
|
expect(screen.getByText(studyLabels[study as Study])).toHaveAttribute(
|
||||||
|
'data-state',
|
||||||
|
'checked'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render with the correct default studies and overlays', async () => {
|
||||||
|
useChartSettingsStore.setState({
|
||||||
|
...DEFAULT_CHART_SETTINGS,
|
||||||
|
chartlib: 'pennant',
|
||||||
|
});
|
||||||
|
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
expect(menu.getByText(studyLabels.macd)).toHaveAttribute(
|
||||||
|
'data-state',
|
||||||
|
'checked'
|
||||||
|
);
|
||||||
|
expect(menu.getByText(overlayLabels.movingAverage)).toHaveAttribute(
|
||||||
|
'data-state',
|
||||||
|
'checked'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,14 +1,12 @@
|
|||||||
import 'pennant/dist/style.css';
|
|
||||||
import {
|
import {
|
||||||
ChartType,
|
ChartType,
|
||||||
Interval,
|
|
||||||
Overlay,
|
Overlay,
|
||||||
Study,
|
Study,
|
||||||
chartTypeLabels,
|
chartTypeLabels,
|
||||||
intervalLabels,
|
|
||||||
overlayLabels,
|
overlayLabels,
|
||||||
studyLabels,
|
studyLabels,
|
||||||
} from 'pennant';
|
} from 'pennant';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
TradingButton,
|
TradingButton,
|
||||||
TradingDropdown,
|
TradingDropdown,
|
||||||
@ -20,10 +18,21 @@ import {
|
|||||||
TradingDropdownTrigger,
|
TradingDropdownTrigger,
|
||||||
Icon,
|
Icon,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { type IconName } from '@blueprintjs/icons';
|
import { Interval } from '@vegaprotocol/types';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
import { useCandlesChartSettings } from './use-candles-chart-settings';
|
import { ALLOWED_TRADINGVIEW_HOSTNAMES } from '@vegaprotocol/trading-view';
|
||||||
import { useT } from './use-t';
|
import { IconNames, type IconName } from '@blueprintjs/icons';
|
||||||
|
import { useChartSettings } from './use-chart-settings';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
|
||||||
|
const INTERVALS = [
|
||||||
|
Interval.INTERVAL_I1M,
|
||||||
|
Interval.INTERVAL_I5M,
|
||||||
|
Interval.INTERVAL_I15M,
|
||||||
|
Interval.INTERVAL_I1H,
|
||||||
|
Interval.INTERVAL_I6H,
|
||||||
|
Interval.INTERVAL_I1D,
|
||||||
|
];
|
||||||
|
|
||||||
const chartTypeIcon = new Map<ChartType, IconName>([
|
const chartTypeIcon = new Map<ChartType, IconName>([
|
||||||
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
|
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
|
||||||
@ -32,30 +41,46 @@ const chartTypeIcon = new Map<ChartType, IconName>([
|
|||||||
[ChartType.OHLC, IconNames.WATERFALL_CHART],
|
[ChartType.OHLC, IconNames.WATERFALL_CHART],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const CandlesMenu = () => {
|
export const ChartMenu = () => {
|
||||||
|
const { CHARTING_LIBRARY_PATH } = useEnvironment();
|
||||||
const {
|
const {
|
||||||
|
chartlib,
|
||||||
interval,
|
interval,
|
||||||
chartType,
|
chartType,
|
||||||
studies,
|
studies,
|
||||||
overlays,
|
overlays,
|
||||||
|
setChartlib,
|
||||||
setInterval,
|
setInterval,
|
||||||
setType,
|
setType,
|
||||||
setStudies,
|
setStudies,
|
||||||
setOverlays,
|
setOverlays,
|
||||||
} = useCandlesChartSettings();
|
} = useChartSettings();
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const triggerClasses = 'text-xs';
|
|
||||||
const contentAlign = 'end';
|
const contentAlign = 'end';
|
||||||
|
const triggerClasses = 'text-xs';
|
||||||
const triggerButtonProps = { size: 'extra-small' } as const;
|
const triggerButtonProps = { size: 'extra-small' } as const;
|
||||||
|
|
||||||
return (
|
const isPennant = chartlib === 'pennant';
|
||||||
|
const commonMenuItems = (
|
||||||
|
<TradingButton
|
||||||
|
onClick={() => {
|
||||||
|
setChartlib(isPennant ? 'tradingview' : 'pennant');
|
||||||
|
}}
|
||||||
|
size="extra-small"
|
||||||
|
>
|
||||||
|
{isPennant ? 'TradingView' : t('Vega chart')}
|
||||||
|
</TradingButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
const pennantMenuItems = (
|
||||||
<>
|
<>
|
||||||
<TradingDropdown
|
<TradingDropdown
|
||||||
trigger={
|
trigger={
|
||||||
<TradingDropdownTrigger className={triggerClasses}>
|
<TradingDropdownTrigger className={triggerClasses}>
|
||||||
<TradingButton {...triggerButtonProps}>
|
<TradingButton {...triggerButtonProps}>
|
||||||
{t('Interval: {{interval}}', {
|
{t('Interval: {{interval}}', {
|
||||||
interval: intervalLabels[interval],
|
interval: t(interval),
|
||||||
})}
|
})}
|
||||||
</TradingButton>
|
</TradingButton>
|
||||||
</TradingDropdownTrigger>
|
</TradingDropdownTrigger>
|
||||||
@ -68,13 +93,13 @@ export const CandlesMenu = () => {
|
|||||||
setInterval(value as Interval);
|
setInterval(value as Interval);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Object.values(Interval).map((timeInterval) => (
|
{INTERVALS.map((timeInterval) => (
|
||||||
<TradingDropdownRadioItem
|
<TradingDropdownRadioItem
|
||||||
key={timeInterval}
|
key={timeInterval}
|
||||||
inset
|
inset
|
||||||
value={timeInterval}
|
value={timeInterval}
|
||||||
>
|
>
|
||||||
{intervalLabels[timeInterval]}
|
{t(timeInterval)}
|
||||||
<TradingDropdownItemIndicator />
|
<TradingDropdownItemIndicator />
|
||||||
</TradingDropdownRadioItem>
|
</TradingDropdownRadioItem>
|
||||||
))}
|
))}
|
||||||
@ -158,4 +183,50 @@ export const CandlesMenu = () => {
|
|||||||
</TradingDropdown>
|
</TradingDropdown>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tradingViewMenuItems = (
|
||||||
|
<p className="text-xs mr-2 whitespace-nowrap">
|
||||||
|
<Trans
|
||||||
|
i18nKey="Chart by <0>TradingView</0>"
|
||||||
|
components={[
|
||||||
|
// eslint-disable-next-line
|
||||||
|
<a
|
||||||
|
className="underline"
|
||||||
|
target="_blank"
|
||||||
|
href="https://www.tradingview.com"
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ALLOWED_TRADINGVIEW_HOSTNAMES.includes(window.location.hostname)) {
|
||||||
|
return pennantMenuItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CHARTING_LIBRARY_PATH) {
|
||||||
|
return pennantMenuItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (chartlib) {
|
||||||
|
case 'tradingview': {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{tradingViewMenuItems}
|
||||||
|
{commonMenuItems}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'pennant': {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{pennantMenuItems}
|
||||||
|
{commonMenuItems}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error('invalid chart lib');
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
2
apps/trading/components/chart-container/index.ts
Normal file
2
apps/trading/components/chart-container/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { ChartContainer } from './chart-container';
|
||||||
|
export { ChartMenu } from './chart-menu';
|
@ -1,18 +1,23 @@
|
|||||||
import { getValidItem, getValidSubset } from '@vegaprotocol/react-helpers';
|
import { ChartType, Overlay, Study } from 'pennant';
|
||||||
import { ChartType, Interval, Study } from 'pennant';
|
|
||||||
import { Overlay } from 'pennant';
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
import { immer } from 'zustand/middleware/immer';
|
import { immer } from 'zustand/middleware/immer';
|
||||||
|
import { Interval } from '@vegaprotocol/types';
|
||||||
|
import { getValidItem, getValidSubset } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
type StudySizes = { [S in Study]?: number };
|
type StudySizes = { [S in Study]?: number };
|
||||||
|
export type Chartlib = 'pennant' | 'tradingview';
|
||||||
|
|
||||||
interface StoredSettings {
|
interface StoredSettings {
|
||||||
|
chartlib: Chartlib;
|
||||||
|
// For interval we use the enum from @vegaprotocol/types, this is to make mapping between different
|
||||||
|
// chart types easier and more consistent
|
||||||
interval: Interval;
|
interval: Interval;
|
||||||
type: ChartType;
|
type: ChartType;
|
||||||
overlays: Overlay[];
|
overlays: Overlay[];
|
||||||
studies: Study[];
|
studies: Study[];
|
||||||
studySizes: StudySizes;
|
studySizes: StudySizes;
|
||||||
|
tradingViewStudies: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STUDY_SIZE = 90;
|
export const STUDY_SIZE = 90;
|
||||||
@ -25,20 +30,24 @@ const STUDY_ORDER: Study[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_CHART_SETTINGS = {
|
export const DEFAULT_CHART_SETTINGS = {
|
||||||
interval: Interval.I15M,
|
chartlib: 'pennant' as const,
|
||||||
|
interval: Interval.INTERVAL_I15M,
|
||||||
type: ChartType.CANDLE,
|
type: ChartType.CANDLE,
|
||||||
overlays: [Overlay.MOVING_AVERAGE],
|
overlays: [Overlay.MOVING_AVERAGE],
|
||||||
studies: [Study.MACD, Study.VOLUME],
|
studies: [Study.MACD, Study.VOLUME],
|
||||||
studySizes: {},
|
studySizes: {},
|
||||||
|
tradingViewStudies: ['Volume'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCandlesChartSettingsStore = create<
|
export const useChartSettingsStore = create<
|
||||||
StoredSettings & {
|
StoredSettings & {
|
||||||
setType: (type: ChartType) => void;
|
setType: (type: ChartType) => void;
|
||||||
setInterval: (interval: Interval) => void;
|
setInterval: (interval: Interval) => void;
|
||||||
setOverlays: (overlays?: Overlay[]) => void;
|
setOverlays: (overlays?: Overlay[]) => void;
|
||||||
setStudies: (studies?: Study[]) => void;
|
setStudies: (studies?: Study[]) => void;
|
||||||
setStudySizes: (sizes: number[]) => void;
|
setStudySizes: (sizes: number[]) => void;
|
||||||
|
setChartlib: (lib: Chartlib) => void;
|
||||||
|
setTradingViewStudies: (studies: string[]) => void;
|
||||||
}
|
}
|
||||||
>()(
|
>()(
|
||||||
persist(
|
persist(
|
||||||
@ -81,6 +90,16 @@ export const useCandlesChartSettingsStore = create<
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setChartlib: (lib) => {
|
||||||
|
set((state) => {
|
||||||
|
state.chartlib = lib;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setTradingViewStudies: (studies: string[]) => {
|
||||||
|
set((state) => {
|
||||||
|
state.tradingViewStudies = studies;
|
||||||
|
});
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
name: 'vega_candles_chart_store',
|
name: 'vega_candles_chart_store',
|
||||||
@ -88,13 +107,13 @@ export const useCandlesChartSettingsStore = create<
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useCandlesChartSettings = () => {
|
export const useChartSettings = () => {
|
||||||
const settings = useCandlesChartSettingsStore();
|
const settings = useChartSettingsStore();
|
||||||
|
|
||||||
const interval: Interval = getValidItem(
|
const interval: Interval = getValidItem(
|
||||||
settings.interval,
|
settings.interval,
|
||||||
Object.values(Interval),
|
Object.values(Interval),
|
||||||
Interval.I15M
|
Interval.INTERVAL_I15M
|
||||||
);
|
);
|
||||||
|
|
||||||
const chartType: ChartType = getValidItem(
|
const chartType: ChartType = getValidItem(
|
||||||
@ -122,15 +141,19 @@ export const useCandlesChartSettings = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
chartlib: settings.chartlib,
|
||||||
interval,
|
interval,
|
||||||
chartType,
|
chartType,
|
||||||
overlays,
|
overlays,
|
||||||
studies,
|
studies,
|
||||||
studySizes,
|
studySizes,
|
||||||
|
tradingViewStudies: settings.tradingViewStudies,
|
||||||
setInterval: settings.setInterval,
|
setInterval: settings.setInterval,
|
||||||
setType: settings.setType,
|
setType: settings.setType,
|
||||||
setStudies: settings.setStudies,
|
setStudies: settings.setStudies,
|
||||||
setOverlays: settings.setOverlays,
|
setOverlays: settings.setOverlays,
|
||||||
setStudySizes: settings.setStudySizes,
|
setStudySizes: settings.setStudySizes,
|
||||||
|
setChartlib: settings.setChartlib,
|
||||||
|
setTradingViewStudies: settings.setTradingViewStudies,
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -4,9 +4,18 @@ export default {
|
|||||||
preset: '../../jest.preset.js',
|
preset: '../../jest.preset.js',
|
||||||
transform: {
|
transform: {
|
||||||
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
||||||
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
|
'^.+\\.[tj]sx?$': [
|
||||||
|
'babel-jest',
|
||||||
|
{
|
||||||
|
presets: ['@nx/next/babel'],
|
||||||
|
// required for pennant to work in jest, due to having untranspiled exports
|
||||||
|
plugins: [['@babel/plugin-proposal-private-methods']],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||||
coverageDirectory: '../../coverage/apps/trading',
|
coverageDirectory: '../../coverage/apps/trading',
|
||||||
setupFilesAfterEnv: ['./setup-tests.ts'],
|
setupFilesAfterEnv: ['./setup-tests.ts'],
|
||||||
|
// dont ignore pennant from transpilation
|
||||||
|
transformIgnorePatterns: ['<rootDir>/node_modules/pennant'],
|
||||||
};
|
};
|
||||||
|
@ -75,6 +75,7 @@ i18n
|
|||||||
'positions',
|
'positions',
|
||||||
'trades',
|
'trades',
|
||||||
'trading',
|
'trading',
|
||||||
|
'trading-view',
|
||||||
'ui-toolkit',
|
'ui-toolkit',
|
||||||
'utils',
|
'utils',
|
||||||
'wallet',
|
'wallet',
|
||||||
|
@ -1,42 +1,51 @@
|
|||||||
import 'pennant/dist/style.css';
|
import 'pennant/dist/style.css';
|
||||||
import { CandlestickChart } from 'pennant';
|
import {
|
||||||
|
CandlestickChart,
|
||||||
|
type Overlay,
|
||||||
|
type ChartType,
|
||||||
|
type Interval,
|
||||||
|
type Study,
|
||||||
|
} from 'pennant';
|
||||||
import { VegaDataSource } from './data-source';
|
import { VegaDataSource } from './data-source';
|
||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import {
|
|
||||||
STUDY_SIZE,
|
|
||||||
useCandlesChartSettings,
|
|
||||||
} from './use-candles-chart-settings';
|
|
||||||
import { useT } from './use-t';
|
import { useT } from './use-t';
|
||||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
export type CandlesChartContainerProps = {
|
export type CandlesChartContainerProps = {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
|
interval: Interval;
|
||||||
|
chartType: ChartType;
|
||||||
|
overlays: Overlay[];
|
||||||
|
studies: Study[];
|
||||||
|
studySizes: number[];
|
||||||
|
defaultStudySize: number;
|
||||||
|
setStudies: (studies?: Study[]) => void;
|
||||||
|
setStudySizes: (sizes: number[]) => void;
|
||||||
|
setOverlays: (overlays?: Overlay[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CANDLES_TO_WIDTH_FACTOR = 0.2;
|
const CANDLES_TO_WIDTH_FACTOR = 0.2;
|
||||||
|
|
||||||
export const CandlesChartContainer = ({
|
export const CandlesChartContainer = ({
|
||||||
marketId,
|
marketId,
|
||||||
}: CandlesChartContainerProps) => {
|
|
||||||
const client = useApolloClient();
|
|
||||||
const { pubKey } = useVegaWallet();
|
|
||||||
const { theme } = useThemeSwitcher();
|
|
||||||
const t = useT();
|
|
||||||
|
|
||||||
const {
|
|
||||||
interval,
|
interval,
|
||||||
chartType,
|
chartType,
|
||||||
overlays,
|
overlays,
|
||||||
studies,
|
studies,
|
||||||
studySizes,
|
studySizes,
|
||||||
|
defaultStudySize,
|
||||||
setStudies,
|
setStudies,
|
||||||
setStudySizes,
|
setStudySizes,
|
||||||
setOverlays,
|
setOverlays,
|
||||||
} = useCandlesChartSettings();
|
}: CandlesChartContainerProps) => {
|
||||||
|
const client = useApolloClient();
|
||||||
|
const { pubKey } = useVegaWallet();
|
||||||
|
const { theme } = useThemeSwitcher();
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
const handlePaneChange = useMemo(
|
const handlePaneChange = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -69,7 +78,7 @@ export const CandlesChartContainer = ({
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
initialNumCandlesToDisplay: candlesCount,
|
initialNumCandlesToDisplay: candlesCount,
|
||||||
studySize: STUDY_SIZE,
|
studySize: defaultStudySize,
|
||||||
studySizes,
|
studySizes,
|
||||||
}}
|
}}
|
||||||
interval={interval}
|
interval={interval}
|
||||||
|
@ -1,85 +0,0 @@
|
|||||||
import { render, screen, within } from '@testing-library/react';
|
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import { CandlesMenu } from './candles-menu';
|
|
||||||
import {
|
|
||||||
useCandlesChartSettingsStore,
|
|
||||||
DEFAULT_CHART_SETTINGS,
|
|
||||||
} from './use-candles-chart-settings';
|
|
||||||
import { Overlay, Study, overlayLabels, studyLabels } from 'pennant';
|
|
||||||
|
|
||||||
describe('CandlesMenu', () => {
|
|
||||||
const openDropdown = async () => {
|
|
||||||
await userEvent.click(
|
|
||||||
screen.getByRole('button', {
|
|
||||||
name: 'Indicators',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// clear store each time to avoid conditional testing of defaults
|
|
||||||
useCandlesChartSettingsStore.setState({ overlays: [], studies: [] });
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(Object.values(Overlay))('can set %s overlay', async (overlay) => {
|
|
||||||
render(<CandlesMenu />);
|
|
||||||
|
|
||||||
await openDropdown();
|
|
||||||
|
|
||||||
const menu = within(await screen.findByRole('menu'));
|
|
||||||
|
|
||||||
await userEvent.click(menu.getByText(overlayLabels[overlay as Overlay]));
|
|
||||||
|
|
||||||
// re-open the dropdown
|
|
||||||
await openDropdown();
|
|
||||||
|
|
||||||
expect(screen.getByText(overlayLabels[overlay as Overlay])).toHaveAttribute(
|
|
||||||
'data-state',
|
|
||||||
'checked'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(Object.values(Study))('can set %s study', async (study) => {
|
|
||||||
render(<CandlesMenu />);
|
|
||||||
|
|
||||||
await openDropdown();
|
|
||||||
|
|
||||||
const menu = within(await screen.findByRole('menu'));
|
|
||||||
|
|
||||||
await userEvent.click(menu.getByText(studyLabels[study as Study]));
|
|
||||||
|
|
||||||
// re-open the dropdown
|
|
||||||
await openDropdown();
|
|
||||||
|
|
||||||
expect(screen.getByText(studyLabels[study as Study])).toHaveAttribute(
|
|
||||||
'data-state',
|
|
||||||
'checked'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render with the correct default studies and overlays', async () => {
|
|
||||||
useCandlesChartSettingsStore.setState(DEFAULT_CHART_SETTINGS);
|
|
||||||
|
|
||||||
render(<CandlesMenu />);
|
|
||||||
|
|
||||||
await userEvent.click(
|
|
||||||
screen.getByRole('button', {
|
|
||||||
name: 'Indicators',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const menu = within(await screen.findByRole('menu'));
|
|
||||||
|
|
||||||
expect(menu.getByText(studyLabels.volume)).toHaveAttribute(
|
|
||||||
'data-state',
|
|
||||||
'checked'
|
|
||||||
);
|
|
||||||
expect(menu.getByText(studyLabels.macd)).toHaveAttribute(
|
|
||||||
'data-state',
|
|
||||||
'checked'
|
|
||||||
);
|
|
||||||
expect(menu.getByText(overlayLabels.movingAverage)).toHaveAttribute(
|
|
||||||
'data-state',
|
|
||||||
'checked'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
12
libs/candles-chart/src/lib/constants.ts
Normal file
12
libs/candles-chart/src/lib/constants.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Interval as PennantInterval } from 'pennant';
|
||||||
|
import { Interval } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
export const PENNANT_INTERVAL_MAP = {
|
||||||
|
[Interval.INTERVAL_BLOCK]: undefined, // TODO: handle block tick
|
||||||
|
[Interval.INTERVAL_I1M]: PennantInterval.I1M,
|
||||||
|
[Interval.INTERVAL_I5M]: PennantInterval.I5M,
|
||||||
|
[Interval.INTERVAL_I15M]: PennantInterval.I15M,
|
||||||
|
[Interval.INTERVAL_I1H]: PennantInterval.I1H,
|
||||||
|
[Interval.INTERVAL_I6H]: PennantInterval.I6H,
|
||||||
|
[Interval.INTERVAL_I1D]: PennantInterval.I1D,
|
||||||
|
} as const;
|
@ -1,5 +1,5 @@
|
|||||||
export * from './__generated__/Candles';
|
export * from './__generated__/Candles';
|
||||||
export * from './__generated__/Chart';
|
export * from './__generated__/Chart';
|
||||||
export * from './candles-chart';
|
export * from './candles-chart';
|
||||||
export * from './candles-menu';
|
export { PENNANT_INTERVAL_MAP } from './constants';
|
||||||
export * from './data-source';
|
export * from './data-source';
|
||||||
|
@ -154,6 +154,7 @@ const compileEnvVars = () => {
|
|||||||
'VEGA_ENV',
|
'VEGA_ENV',
|
||||||
process.env['NX_VEGA_ENV']
|
process.env['NX_VEGA_ENV']
|
||||||
) as Networks;
|
) as Networks;
|
||||||
|
|
||||||
const env: Environment = {
|
const env: Environment = {
|
||||||
VEGA_URL: windowOrDefault('VEGA_URL', process.env['NX_VEGA_URL']),
|
VEGA_URL: windowOrDefault('VEGA_URL', process.env['NX_VEGA_URL']),
|
||||||
VEGA_ENV,
|
VEGA_ENV,
|
||||||
@ -253,6 +254,14 @@ const compileEnvVars = () => {
|
|||||||
'NX_MOZILLA_EXTENSION_URL',
|
'NX_MOZILLA_EXTENSION_URL',
|
||||||
process.env['NX_MOZILLA_EXTENSION_URL']
|
process.env['NX_MOZILLA_EXTENSION_URL']
|
||||||
),
|
),
|
||||||
|
CHARTING_LIBRARY_PATH: windowOrDefault(
|
||||||
|
'NX_CHARTING_LIBRARY_PATH',
|
||||||
|
process.env['NX_CHARTING_LIBRARY_PATH']
|
||||||
|
),
|
||||||
|
CHARTING_LIBRARY_HASH: windowOrDefault(
|
||||||
|
'NX_CHARTING_LIBRARY_HASH',
|
||||||
|
process.env['NX_CHARTING_LIBRARY_HASH']
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return env;
|
return env;
|
||||||
@ -360,6 +369,7 @@ export const compileFeatureFlags = (refresh = false): FeatureFlags => {
|
|||||||
) as string
|
) as string
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const EXPLORER_FLAGS = {
|
const EXPLORER_FLAGS = {
|
||||||
EXPLORER_ASSETS: TRUTHY.includes(
|
EXPLORER_ASSETS: TRUTHY.includes(
|
||||||
windowOrDefault(
|
windowOrDefault(
|
||||||
@ -416,6 +426,7 @@ export const compileFeatureFlags = (refresh = false): FeatureFlags => {
|
|||||||
) as string
|
) as string
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const GOVERNANCE_FLAGS = {
|
const GOVERNANCE_FLAGS = {
|
||||||
GOVERNANCE_NETWORK_DOWN: TRUTHY.includes(
|
GOVERNANCE_NETWORK_DOWN: TRUTHY.includes(
|
||||||
windowOrDefault(
|
windowOrDefault(
|
||||||
|
@ -60,6 +60,8 @@ export const envSchema = z
|
|||||||
TENDERMINT_WEBSOCKET_URL: z.optional(z.string()),
|
TENDERMINT_WEBSOCKET_URL: z.optional(z.string()),
|
||||||
CHROME_EXTENSION_URL: z.optional(z.string()),
|
CHROME_EXTENSION_URL: z.optional(z.string()),
|
||||||
MOZILLA_EXTENSION_URL: z.optional(z.string()),
|
MOZILLA_EXTENSION_URL: z.optional(z.string()),
|
||||||
|
CHARTING_LIBRARY_PATH: z.optional(z.string()),
|
||||||
|
CHARTING_LIBRARY_HASH: z.optional(z.string()),
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
@ -11,6 +11,7 @@ import en_fills from './locales/en/fills.json';
|
|||||||
import en_funding_payments from './locales/en/funding-payments.json';
|
import en_funding_payments from './locales/en/funding-payments.json';
|
||||||
import en_governance from './locales/en/governance.json';
|
import en_governance from './locales/en/governance.json';
|
||||||
import en_trading from './locales/en/trading.json';
|
import en_trading from './locales/en/trading.json';
|
||||||
|
import en_trading_view from './locales/en/trading-view.json';
|
||||||
import en_markets from './locales/en/markets.json';
|
import en_markets from './locales/en/markets.json';
|
||||||
import en_web3 from './locales/en/web3.json';
|
import en_web3 from './locales/en/web3.json';
|
||||||
import en_proposals from './locales/en/proposals.json';
|
import en_proposals from './locales/en/proposals.json';
|
||||||
@ -32,6 +33,7 @@ export const locales = {
|
|||||||
'funding-payments': en_funding_payments,
|
'funding-payments': en_funding_payments,
|
||||||
governance: en_governance,
|
governance: en_governance,
|
||||||
trading: en_trading,
|
trading: en_trading,
|
||||||
|
trading_view: en_trading_view,
|
||||||
markets: en_markets,
|
markets: en_markets,
|
||||||
web3: en_web3,
|
web3: en_web3,
|
||||||
positions: en_positions,
|
positions: en_positions,
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
{
|
{
|
||||||
"Indicators": "Indicators",
|
|
||||||
"Interval: {{interval}}": "Interval: {{interval}}",
|
|
||||||
"No open orders": "No open orders"
|
"No open orders": "No open orders"
|
||||||
}
|
}
|
||||||
|
4
libs/i18n/src/locales/en/trading-view.json
Normal file
4
libs/i18n/src/locales/en/trading-view.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"Failed to initialize Trading view": "Failed to initialize Trading view",
|
||||||
|
"Loading Trading View": "Loading Trading View"
|
||||||
|
}
|
@ -26,9 +26,10 @@
|
|||||||
"Best offer": "Best offer",
|
"Best offer": "Best offer",
|
||||||
"Browse": "Browse",
|
"Browse": "Browse",
|
||||||
"By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>": "By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>",
|
"By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>": "By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>",
|
||||||
"Candles": "Candles",
|
"Chart": "Chart",
|
||||||
"Change (24h)": "Change (24h)",
|
"Change (24h)": "Change (24h)",
|
||||||
"Chart": "Chart",
|
"Chart": "Chart",
|
||||||
|
"Chart by <0>TradingView</0>": "Chart by <0>TradingView</0>",
|
||||||
"checkOutProposalsAndVote": "Check out the terms of the proposals and vote:",
|
"checkOutProposalsAndVote": "Check out the terms of the proposals and vote:",
|
||||||
"checkOutProposalsAndVote_one": "Check out the terms of the proposal and vote:",
|
"checkOutProposalsAndVote_one": "Check out the terms of the proposal and vote:",
|
||||||
"checkOutProposalsAndVote_other": "Check out the terms of the proposals and vote:",
|
"checkOutProposalsAndVote_other": "Check out the terms of the proposals and vote:",
|
||||||
@ -120,7 +121,15 @@
|
|||||||
"Improve vega console": "Improve vega console",
|
"Improve vega console": "Improve vega console",
|
||||||
"Inactive": "Inactive",
|
"Inactive": "Inactive",
|
||||||
"Index Price": "Index Price",
|
"Index Price": "Index Price",
|
||||||
|
"Indicators": "Indicators",
|
||||||
"Infrastructure": "Infrastructure",
|
"Infrastructure": "Infrastructure",
|
||||||
|
"Interval: {{interval}}": "Interval: {{interval}}",
|
||||||
|
"INTERVAL_I1M": "1m",
|
||||||
|
"INTERVAL_I5M": "5m",
|
||||||
|
"INTERVAL_I15M": "15m",
|
||||||
|
"INTERVAL_I1H": "1H",
|
||||||
|
"INTERVAL_I6H": "6H",
|
||||||
|
"INTERVAL_I1D": "1D",
|
||||||
"Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.": "Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.",
|
"Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.": "Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.",
|
||||||
"Learn about providing liquidity": "Learn about providing liquidity",
|
"Learn about providing liquidity": "Learn about providing liquidity",
|
||||||
"Learn more": "Learn more",
|
"Learn more": "Learn more",
|
||||||
@ -192,6 +201,7 @@
|
|||||||
"pastEpochs": "Past {{count}} epochs",
|
"pastEpochs": "Past {{count}} epochs",
|
||||||
"pastEpochs_one": "Past {{count}} epoch",
|
"pastEpochs_one": "Past {{count}} epoch",
|
||||||
"pastEpochs_other": "Past {{count}} epochs",
|
"pastEpochs_other": "Past {{count}} epochs",
|
||||||
|
"Pennant": "Pennant",
|
||||||
"Perpetuals": "Perpetuals",
|
"Perpetuals": "Perpetuals",
|
||||||
"Please choose another market from the <0>market list</0>": "Please choose another market from the <0>market list</0>",
|
"Please choose another market from the <0>market list</0>": "Please choose another market from the <0>market list</0>",
|
||||||
"Please connect Vega wallet": "Please connect Vega wallet",
|
"Please connect Vega wallet": "Please connect Vega wallet",
|
||||||
@ -291,6 +301,7 @@
|
|||||||
"Trader": "Trader",
|
"Trader": "Trader",
|
||||||
"Trades": "Trades",
|
"Trades": "Trades",
|
||||||
"Trading": "Trading",
|
"Trading": "Trading",
|
||||||
|
"TradingView": "TradingView",
|
||||||
"Trading has been terminated as a result of the product definition": "Trading has been terminated as a result of the product definition",
|
"Trading has been terminated as a result of the product definition": "Trading has been terminated as a result of the product definition",
|
||||||
"Trading mode": "Trading mode",
|
"Trading mode": "Trading mode",
|
||||||
"Trading on Market {{name}} may stop on {{date}}. There is open proposal to close this market.": "Trading on Market {{name}} may stop on {{date}}. There is open proposal to close this market.",
|
"Trading on Market {{name}} may stop on {{date}}. There is open proposal to close this market.": "Trading on Market {{name}} may stop on {{date}}. There is open proposal to close this market.",
|
||||||
@ -302,6 +313,7 @@
|
|||||||
"totalCommission_other": "Total commission (last {{count}} epochs)",
|
"totalCommission_other": "Total commission (last {{count}} epochs)",
|
||||||
"Unknown": "Unknown",
|
"Unknown": "Unknown",
|
||||||
"Unknown settlement date": "Unknown settlement date",
|
"Unknown settlement date": "Unknown settlement date",
|
||||||
|
"Vega chart": "Vega chart",
|
||||||
"Vega Reward pot": "Vega Reward pot",
|
"Vega Reward pot": "Vega Reward pot",
|
||||||
"Vega Wallet <0>full featured<0>": "Vega Wallet <0>full featured<0>",
|
"Vega Wallet <0>full featured<0>": "Vega Wallet <0>full featured<0>",
|
||||||
"Vesting": "Vesting",
|
"Vesting": "Vesting",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
export * from './use-copy-timeout';
|
||||||
export * from './use-fetch';
|
export * from './use-fetch';
|
||||||
export * from './use-local-storage';
|
export * from './use-local-storage';
|
||||||
export * from './use-mutation-observer';
|
export * from './use-mutation-observer';
|
||||||
@ -11,4 +12,4 @@ export * from './use-theme-switcher';
|
|||||||
export * from './use-storybook-theme-observer';
|
export * from './use-storybook-theme-observer';
|
||||||
export * from './use-yesterday';
|
export * from './use-yesterday';
|
||||||
export * from './use-previous';
|
export * from './use-previous';
|
||||||
export * from './use-copy-timeout';
|
export { useScript } from './use-script';
|
||||||
|
18
libs/react-helpers/src/hooks/use-script.spec.ts
Normal file
18
libs/react-helpers/src/hooks/use-script.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
|
import { useScript } from './use-script';
|
||||||
|
|
||||||
|
describe('useScript', () => {
|
||||||
|
it('appends a script to the body', async () => {
|
||||||
|
const url = 'http://localhost:8080/foo.js';
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useScript(url, 'integrity-hash'));
|
||||||
|
|
||||||
|
expect(result.current).toBe('loading');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const script = document.body.getElementsByTagName('script')[0];
|
||||||
|
expect(script).toBeInTheDocument();
|
||||||
|
expect(script).toHaveAttribute('src', url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
57
libs/react-helpers/src/hooks/use-script.ts
Normal file
57
libs/react-helpers/src/hooks/use-script.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
type State = 'idle' | 'loading' | 'ready' | 'error';
|
||||||
|
|
||||||
|
// keep record of loaded script state, so if the component re-mounts but the script
|
||||||
|
// was already appended its not re-appended and will have the correct state
|
||||||
|
type Url = string;
|
||||||
|
const cache: Record<Url, State> = {};
|
||||||
|
|
||||||
|
export const useScript = (url: string, integrity: string) => {
|
||||||
|
// track state of the script as it loads or possibly fails
|
||||||
|
const [state, setState] = useState<State>(url ? 'loading' : 'idle');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!url) {
|
||||||
|
setState('idle');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the integrity hash of the script as an identifier
|
||||||
|
let script = document.getElementById(integrity) as HTMLScriptElement | null;
|
||||||
|
|
||||||
|
if (script) {
|
||||||
|
// script already on the page
|
||||||
|
setState(cache[url]);
|
||||||
|
} else {
|
||||||
|
// script not found, create and append script
|
||||||
|
script = document.createElement('script');
|
||||||
|
script.id = integrity;
|
||||||
|
script.src = url;
|
||||||
|
script.async = true;
|
||||||
|
script.crossOrigin = 'anonymous'; // make sure sri is respected with cross origin request
|
||||||
|
script.integrity = `sha256-${integrity}`;
|
||||||
|
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup/teardown listeners to notify component when script has loaded
|
||||||
|
const _setState = (event: Event) => {
|
||||||
|
const result = event.type === 'load' ? 'ready' : 'error';
|
||||||
|
setState(result);
|
||||||
|
cache[url] = result;
|
||||||
|
};
|
||||||
|
|
||||||
|
script.addEventListener('load', _setState);
|
||||||
|
script.addEventListener('error', _setState);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (script) {
|
||||||
|
script.removeEventListener('load', _setState);
|
||||||
|
script.removeEventListener('error', _setState);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [url, integrity]);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
12
libs/trading-view/.babelrc
Normal file
12
libs/trading-view/.babelrc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@nx/react/babel",
|
||||||
|
{
|
||||||
|
"runtime": "automatic",
|
||||||
|
"useBuiltIns": "usage"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"plugins": []
|
||||||
|
}
|
18
libs/trading-view/.eslintrc.json
Normal file
18
libs/trading-view/.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
||||||
|
"ignorePatterns": ["!**/*", "__generated__"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
7
libs/trading-view/README.md
Normal file
7
libs/trading-view/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# trading-view
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test trading-view` to execute the unit tests via [Jest](https://jestjs.io).
|
12
libs/trading-view/jest.config.ts
Normal file
12
libs/trading-view/jest.config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
export default {
|
||||||
|
displayName: 'trading-view',
|
||||||
|
preset: '../../jest.preset.js',
|
||||||
|
transform: {
|
||||||
|
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
||||||
|
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
|
||||||
|
},
|
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||||
|
coverageDirectory: '../../coverage/libs/trading-view',
|
||||||
|
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||||
|
};
|
4
libs/trading-view/package.json
Normal file
4
libs/trading-view/package.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "trading-view",
|
||||||
|
"version": "0.0.1"
|
||||||
|
}
|
43
libs/trading-view/project.json
Normal file
43
libs/trading-view/project.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "trading-view",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "libs/trading-view/src",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nx/eslint:lint",
|
||||||
|
"outputs": ["{options.outputFile}"],
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["libs/trading-view/**/*.{ts,tsx,js,jsx}"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"executor": "@nx/rollup:rollup",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/libs/trading-view",
|
||||||
|
"tsConfig": "libs/trading-view/tsconfig.lib.json",
|
||||||
|
"project": "libs/trading-view/package.json",
|
||||||
|
"entryFile": "libs/trading-view/src/index.ts",
|
||||||
|
"external": ["react", "react-dom", "react/jsx-runtime"],
|
||||||
|
"rollupConfig": "@nx/react/plugins/bundle-rollup",
|
||||||
|
"compiler": "babel",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "libs/trading-view/README.md",
|
||||||
|
"input": ".",
|
||||||
|
"output": "."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nx/jest:jest",
|
||||||
|
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "libs/trading-view/jest.config.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
libs/trading-view/src/index.ts
Normal file
5
libs/trading-view/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { TradingViewContainer } from './lib/trading-view-container';
|
||||||
|
export {
|
||||||
|
ALLOWED_TRADINGVIEW_HOSTNAMES,
|
||||||
|
TRADINGVIEW_INTERVAL_MAP,
|
||||||
|
} from './lib/constants';
|
40
libs/trading-view/src/lib/Bars.graphql
Normal file
40
libs/trading-view/src/lib/Bars.graphql
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
fragment Bar on Candle {
|
||||||
|
periodStart
|
||||||
|
lastUpdateInPeriod
|
||||||
|
high
|
||||||
|
low
|
||||||
|
open
|
||||||
|
close
|
||||||
|
volume
|
||||||
|
}
|
||||||
|
|
||||||
|
query GetBars(
|
||||||
|
$marketId: ID!
|
||||||
|
$interval: Interval!
|
||||||
|
$since: String!
|
||||||
|
$to: String
|
||||||
|
) {
|
||||||
|
market(id: $marketId) {
|
||||||
|
id
|
||||||
|
decimalPlaces
|
||||||
|
positionDecimalPlaces
|
||||||
|
candlesConnection(
|
||||||
|
interval: $interval
|
||||||
|
since: $since
|
||||||
|
to: $to
|
||||||
|
pagination: { last: 5000 }
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Bar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription LastBar($marketId: ID!, $interval: Interval!) {
|
||||||
|
candles(marketId: $marketId, interval: $interval) {
|
||||||
|
...Bar
|
||||||
|
}
|
||||||
|
}
|
24
libs/trading-view/src/lib/Symbol.graphql
Normal file
24
libs/trading-view/src/lib/Symbol.graphql
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
query Symbol($marketId: ID!) {
|
||||||
|
market(id: $marketId) {
|
||||||
|
id
|
||||||
|
decimalPlaces
|
||||||
|
positionDecimalPlaces
|
||||||
|
tradableInstrument {
|
||||||
|
instrument {
|
||||||
|
code
|
||||||
|
name
|
||||||
|
metadata {
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
product {
|
||||||
|
... on Future {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
... on Perpetual {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
119
libs/trading-view/src/lib/__generated__/Bars.ts
generated
Normal file
119
libs/trading-view/src/lib/__generated__/Bars.ts
generated
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
|
const defaultOptions = {} as const;
|
||||||
|
export type BarFragment = { __typename?: 'Candle', periodStart: any, lastUpdateInPeriod: any, high: string, low: string, open: string, close: string, volume: string };
|
||||||
|
|
||||||
|
export type GetBarsQueryVariables = Types.Exact<{
|
||||||
|
marketId: Types.Scalars['ID'];
|
||||||
|
interval: Types.Interval;
|
||||||
|
since: Types.Scalars['String'];
|
||||||
|
to?: Types.InputMaybe<Types.Scalars['String']>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type GetBarsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, candlesConnection?: { __typename?: 'CandleDataConnection', edges?: Array<{ __typename?: 'CandleEdge', node: { __typename?: 'Candle', periodStart: any, lastUpdateInPeriod: any, high: string, low: string, open: string, close: string, volume: string } } | null> | null } | null } | null };
|
||||||
|
|
||||||
|
export type LastBarSubscriptionVariables = Types.Exact<{
|
||||||
|
marketId: Types.Scalars['ID'];
|
||||||
|
interval: Types.Interval;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type LastBarSubscription = { __typename?: 'Subscription', candles: { __typename?: 'Candle', periodStart: any, lastUpdateInPeriod: any, high: string, low: string, open: string, close: string, volume: string } };
|
||||||
|
|
||||||
|
export const BarFragmentDoc = gql`
|
||||||
|
fragment Bar on Candle {
|
||||||
|
periodStart
|
||||||
|
lastUpdateInPeriod
|
||||||
|
high
|
||||||
|
low
|
||||||
|
open
|
||||||
|
close
|
||||||
|
volume
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export const GetBarsDocument = gql`
|
||||||
|
query GetBars($marketId: ID!, $interval: Interval!, $since: String!, $to: String) {
|
||||||
|
market(id: $marketId) {
|
||||||
|
id
|
||||||
|
decimalPlaces
|
||||||
|
positionDecimalPlaces
|
||||||
|
candlesConnection(
|
||||||
|
interval: $interval
|
||||||
|
since: $since
|
||||||
|
to: $to
|
||||||
|
pagination: {last: 5000}
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Bar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${BarFragmentDoc}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useGetBarsQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useGetBarsQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useGetBarsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useGetBarsQuery({
|
||||||
|
* variables: {
|
||||||
|
* marketId: // value for 'marketId'
|
||||||
|
* interval: // value for 'interval'
|
||||||
|
* since: // value for 'since'
|
||||||
|
* to: // value for 'to'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useGetBarsQuery(baseOptions: Apollo.QueryHookOptions<GetBarsQuery, GetBarsQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<GetBarsQuery, GetBarsQueryVariables>(GetBarsDocument, options);
|
||||||
|
}
|
||||||
|
export function useGetBarsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetBarsQuery, GetBarsQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<GetBarsQuery, GetBarsQueryVariables>(GetBarsDocument, options);
|
||||||
|
}
|
||||||
|
export type GetBarsQueryHookResult = ReturnType<typeof useGetBarsQuery>;
|
||||||
|
export type GetBarsLazyQueryHookResult = ReturnType<typeof useGetBarsLazyQuery>;
|
||||||
|
export type GetBarsQueryResult = Apollo.QueryResult<GetBarsQuery, GetBarsQueryVariables>;
|
||||||
|
export const LastBarDocument = gql`
|
||||||
|
subscription LastBar($marketId: ID!, $interval: Interval!) {
|
||||||
|
candles(marketId: $marketId, interval: $interval) {
|
||||||
|
...Bar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${BarFragmentDoc}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useLastBarSubscription__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useLastBarSubscription` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useLastBarSubscription` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useLastBarSubscription({
|
||||||
|
* variables: {
|
||||||
|
* marketId: // value for 'marketId'
|
||||||
|
* interval: // value for 'interval'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useLastBarSubscription(baseOptions: Apollo.SubscriptionHookOptions<LastBarSubscription, LastBarSubscriptionVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useSubscription<LastBarSubscription, LastBarSubscriptionVariables>(LastBarDocument, options);
|
||||||
|
}
|
||||||
|
export type LastBarSubscriptionHookResult = ReturnType<typeof useLastBarSubscription>;
|
||||||
|
export type LastBarSubscriptionResult = Apollo.SubscriptionResult<LastBarSubscription>;
|
67
libs/trading-view/src/lib/__generated__/Symbol.ts
generated
Normal file
67
libs/trading-view/src/lib/__generated__/Symbol.ts
generated
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
|
const defaultOptions = {} as const;
|
||||||
|
export type SymbolQueryVariables = Types.Exact<{
|
||||||
|
marketId: Types.Scalars['ID'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type SymbolQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', code: string, name: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename: 'Future' } | { __typename: 'Perpetual' } | { __typename?: 'Spot' } } } } | null };
|
||||||
|
|
||||||
|
|
||||||
|
export const SymbolDocument = gql`
|
||||||
|
query Symbol($marketId: ID!) {
|
||||||
|
market(id: $marketId) {
|
||||||
|
id
|
||||||
|
decimalPlaces
|
||||||
|
positionDecimalPlaces
|
||||||
|
tradableInstrument {
|
||||||
|
instrument {
|
||||||
|
code
|
||||||
|
name
|
||||||
|
metadata {
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
product {
|
||||||
|
... on Future {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
... on Perpetual {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useSymbolQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useSymbolQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useSymbolQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useSymbolQuery({
|
||||||
|
* variables: {
|
||||||
|
* marketId: // value for 'marketId'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useSymbolQuery(baseOptions: Apollo.QueryHookOptions<SymbolQuery, SymbolQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<SymbolQuery, SymbolQueryVariables>(SymbolDocument, options);
|
||||||
|
}
|
||||||
|
export function useSymbolLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SymbolQuery, SymbolQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<SymbolQuery, SymbolQueryVariables>(SymbolDocument, options);
|
||||||
|
}
|
||||||
|
export type SymbolQueryHookResult = ReturnType<typeof useSymbolQuery>;
|
||||||
|
export type SymbolLazyQueryHookResult = ReturnType<typeof useSymbolLazyQuery>;
|
||||||
|
export type SymbolQueryResult = Apollo.QueryResult<SymbolQuery, SymbolQueryVariables>;
|
24
libs/trading-view/src/lib/constants.ts
Normal file
24
libs/trading-view/src/lib/constants.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Interval } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
export const ALLOWED_TRADINGVIEW_HOSTNAMES = [
|
||||||
|
'localhost',
|
||||||
|
'vegafairground.eth.limo',
|
||||||
|
'vegafairground.eth',
|
||||||
|
'vegaprotocol.eth',
|
||||||
|
'vegaprotocol.eth.limo',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CHARTING_LIBRARY_FILE = 'charting_library.standalone.js';
|
||||||
|
|
||||||
|
export const TRADINGVIEW_INTERVAL_MAP = {
|
||||||
|
[Interval.INTERVAL_BLOCK]: undefined, // TODO: handle block tick
|
||||||
|
[Interval.INTERVAL_I1M]: '1',
|
||||||
|
[Interval.INTERVAL_I5M]: '5',
|
||||||
|
[Interval.INTERVAL_I15M]: '15',
|
||||||
|
[Interval.INTERVAL_I1H]: '60',
|
||||||
|
[Interval.INTERVAL_I6H]: '360',
|
||||||
|
[Interval.INTERVAL_I1D]: '1D',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ResolutionRecord = typeof TRADINGVIEW_INTERVAL_MAP;
|
||||||
|
export type ResolutionString = ResolutionRecord[keyof ResolutionRecord];
|
56
libs/trading-view/src/lib/trading-view-container.spec.tsx
Normal file
56
libs/trading-view/src/lib/trading-view-container.spec.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { TradingViewContainer } from './trading-view-container';
|
||||||
|
import * as useScriptModule from '@vegaprotocol/react-helpers';
|
||||||
|
import { CHARTING_LIBRARY_FILE } from './constants';
|
||||||
|
|
||||||
|
jest.mock('./trading-view', () => ({
|
||||||
|
TradingView: ({ marketId }: { marketId: string }) => (
|
||||||
|
<div data-testid="trading-view">{marketId}</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('TradingView', () => {
|
||||||
|
const props = {
|
||||||
|
libraryPath: 'foo',
|
||||||
|
libraryHash: 'hash',
|
||||||
|
marketId: 'marketId',
|
||||||
|
};
|
||||||
|
const renderComponent = () => render(<TradingViewContainer {...props} />);
|
||||||
|
|
||||||
|
it.each(['loading', 'idle'])(
|
||||||
|
'renders loading state when script is %s',
|
||||||
|
(state: string) => {
|
||||||
|
const spyOnScript = jest
|
||||||
|
.spyOn(useScriptModule, 'useScript')
|
||||||
|
.mockReturnValue(state as 'idle' | 'loading');
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(screen.getByText('Loading Trading View')).toBeInTheDocument();
|
||||||
|
expect(spyOnScript).toHaveBeenCalledWith(
|
||||||
|
props.libraryPath + CHARTING_LIBRARY_FILE,
|
||||||
|
props.libraryHash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('renders error state if script fails to load', () => {
|
||||||
|
jest.spyOn(useScriptModule, 'useScript').mockReturnValue('error');
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText('Failed to initialize Trading view')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders TradingView if script loads successfully', () => {
|
||||||
|
jest.spyOn(useScriptModule, 'useScript').mockReturnValue('ready');
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('trading-view')).toHaveTextContent(
|
||||||
|
props.marketId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
56
libs/trading-view/src/lib/trading-view-container.tsx
Normal file
56
libs/trading-view/src/lib/trading-view-container.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useScript } from '@vegaprotocol/react-helpers';
|
||||||
|
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useT } from './use-t';
|
||||||
|
import { TradingView, type OnAutoSaveNeededCallback } from './trading-view';
|
||||||
|
import { CHARTING_LIBRARY_FILE, type ResolutionString } from './constants';
|
||||||
|
|
||||||
|
export const TradingViewContainer = ({
|
||||||
|
libraryPath,
|
||||||
|
libraryHash,
|
||||||
|
marketId,
|
||||||
|
interval,
|
||||||
|
studies,
|
||||||
|
onIntervalChange,
|
||||||
|
onAutoSaveNeeded,
|
||||||
|
}: {
|
||||||
|
libraryPath: string;
|
||||||
|
libraryHash: string;
|
||||||
|
marketId: string;
|
||||||
|
interval: ResolutionString;
|
||||||
|
studies: string[];
|
||||||
|
onIntervalChange: (interval: string) => void;
|
||||||
|
onAutoSaveNeeded: OnAutoSaveNeededCallback;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const scriptState = useScript(
|
||||||
|
libraryPath + CHARTING_LIBRARY_FILE,
|
||||||
|
libraryHash
|
||||||
|
);
|
||||||
|
|
||||||
|
if (scriptState === 'loading' || scriptState === 'idle') {
|
||||||
|
return (
|
||||||
|
<Splash>
|
||||||
|
<p>{t('Loading Trading View')}</p>
|
||||||
|
</Splash>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scriptState === 'error') {
|
||||||
|
return (
|
||||||
|
<Splash>
|
||||||
|
<p>{t('Failed to initialize Trading view')}</p>
|
||||||
|
</Splash>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TradingView
|
||||||
|
libraryPath={libraryPath}
|
||||||
|
marketId={marketId}
|
||||||
|
interval={interval}
|
||||||
|
studies={studies}
|
||||||
|
onIntervalChange={onIntervalChange}
|
||||||
|
onAutoSaveNeeded={onAutoSaveNeeded}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
131
libs/trading-view/src/lib/trading-view.tsx
Normal file
131
libs/trading-view/src/lib/trading-view.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
useScreenDimensions,
|
||||||
|
useThemeSwitcher,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import { useLanguage } from './use-t';
|
||||||
|
import { useDatafeed } from './use-datafeed';
|
||||||
|
import { type ResolutionString } from './constants';
|
||||||
|
|
||||||
|
export type OnAutoSaveNeededCallback = (data: { studies: string[] }) => void;
|
||||||
|
|
||||||
|
export const TradingView = ({
|
||||||
|
marketId,
|
||||||
|
libraryPath,
|
||||||
|
interval,
|
||||||
|
studies,
|
||||||
|
onIntervalChange,
|
||||||
|
onAutoSaveNeeded,
|
||||||
|
}: {
|
||||||
|
marketId: string;
|
||||||
|
libraryPath: string;
|
||||||
|
interval: ResolutionString;
|
||||||
|
studies: string[];
|
||||||
|
onIntervalChange: (interval: string) => void;
|
||||||
|
onAutoSaveNeeded: OnAutoSaveNeededCallback;
|
||||||
|
}) => {
|
||||||
|
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 datafeed = useDatafeed();
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
const disableOnSmallScreens = isMobile ? ['left_toolbar'] : [];
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
const asOverlay = study === 'Volume';
|
||||||
|
activeChart.createStudy(study, asOverlay);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 programmitcally 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, doen separately
|
||||||
|
// to avoid full chart reload
|
||||||
|
useEffect(() => {
|
||||||
|
if (!widgetRef.current || !widgetRef.current._ready) return;
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
});
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
return <div ref={chartContainerRef} className="w-full h-full" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOverrides = (theme: 'dark' | 'light') => {
|
||||||
|
return {
|
||||||
|
// colors set here, trading view lets the user set a color
|
||||||
|
'paneProperties.background': theme === 'dark' ? '#05060C' : '#fff',
|
||||||
|
'paneProperties.backgroundType': 'solid',
|
||||||
|
};
|
||||||
|
};
|
298
libs/trading-view/src/lib/use-datafeed.ts
Normal file
298
libs/trading-view/src/lib/use-datafeed.ts
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
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,
|
||||||
|
type BarFragment,
|
||||||
|
type GetBarsQuery,
|
||||||
|
type GetBarsQueryVariables,
|
||||||
|
type LastBarSubscription,
|
||||||
|
type LastBarSubscriptionVariables,
|
||||||
|
} from './__generated__/Bars';
|
||||||
|
import { Interval } from '@vegaprotocol/types';
|
||||||
|
import {
|
||||||
|
SymbolDocument,
|
||||||
|
type SymbolQuery,
|
||||||
|
type SymbolQueryVariables,
|
||||||
|
} from './__generated__/Symbol';
|
||||||
|
import { getMarketExpiryDate, toBigNum } from '@vegaprotocol/utils';
|
||||||
|
|
||||||
|
const EXCHANGE = 'VEGA';
|
||||||
|
|
||||||
|
const resolutionMap: Record<string, Interval> = {
|
||||||
|
'1T': Interval.INTERVAL_BLOCK,
|
||||||
|
'1': Interval.INTERVAL_I1M,
|
||||||
|
'5': Interval.INTERVAL_I5M,
|
||||||
|
'15': Interval.INTERVAL_I15M,
|
||||||
|
'60': Interval.INTERVAL_I1H,
|
||||||
|
'360': Interval.INTERVAL_I6H,
|
||||||
|
'1D': Interval.INTERVAL_I1D,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const supportedResolutions = Object.keys(resolutionMap);
|
||||||
|
|
||||||
|
const configurationData = {
|
||||||
|
// only showing Vega ofc
|
||||||
|
exchanges: [EXCHANGE],
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
export const useDatafeed = () => {
|
||||||
|
const hasHistory = useRef(false);
|
||||||
|
const subRef = useRef<Subscription>();
|
||||||
|
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));
|
||||||
|
},
|
||||||
|
|
||||||
|
searchSymbols: () => {
|
||||||
|
/* no op, we handle finding markets in app */
|
||||||
|
},
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const result = await client.query<SymbolQuery, SymbolQueryVariables>({
|
||||||
|
query: SymbolDocument,
|
||||||
|
variables: {
|
||||||
|
marketId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.data.market) {
|
||||||
|
onResolveErrorCallback('Cannot resolve symbol: market not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const market = result.data.market;
|
||||||
|
const instrument = market.tradableInstrument.instrument;
|
||||||
|
const productType = instrument.product.__typename;
|
||||||
|
|
||||||
|
if (!productType) {
|
||||||
|
onResolveErrorCallback(
|
||||||
|
'Cannot resolve symbol: invalid product type'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let type = 'undefined'; // https://www.tradingview.com/charting-library-docs/latest/api/modules/Charting_Library#symboltype
|
||||||
|
if (productType === 'Future' || productType === 'Perpetual') {
|
||||||
|
type = 'futures';
|
||||||
|
} else if (productType === 'Spot') {
|
||||||
|
type = 'spot';
|
||||||
|
}
|
||||||
|
|
||||||
|
const expirationDate = getMarketExpiryDate(instrument.metadata.tags);
|
||||||
|
const expirationTimestamp = expirationDate
|
||||||
|
? Math.floor(expirationDate.getTime() / 1000)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// @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,
|
||||||
|
full_name: `${EXCHANGE}:${instrument.code}`,
|
||||||
|
description: instrument.name,
|
||||||
|
listed_exchange: EXCHANGE,
|
||||||
|
expired: productType === 'Perpetual' ? false : true,
|
||||||
|
expirationDate: expirationTimestamp,
|
||||||
|
|
||||||
|
// @ts-ignore cant import types as chartin_library is external
|
||||||
|
format: 'price' as SeriesFormat,
|
||||||
|
type,
|
||||||
|
session: '24x7',
|
||||||
|
timezone: 'Etc/UTC',
|
||||||
|
exchange: EXCHANGE,
|
||||||
|
minmov: 1,
|
||||||
|
pricescale: Number('1' + '0'.repeat(market.decimalPlaces)), // for number of decimal places
|
||||||
|
visible_plots_set: 'ohlc',
|
||||||
|
volume_precision: market.positionDecimalPlaces,
|
||||||
|
data_status: 'streaming',
|
||||||
|
delay: 1000, // around 1 block time
|
||||||
|
has_intraday: true, // required for less than 1 day interval
|
||||||
|
has_empty_bars: true, // library will generate bars if there are gaps, useful for auctions
|
||||||
|
has_ticks: false, // switch to true when enabling block intervals
|
||||||
|
|
||||||
|
// @ts-ignore required for data conversion
|
||||||
|
vegaDecimalPlaces: market.decimalPlaces,
|
||||||
|
// @ts-ignore required for data conversion
|
||||||
|
vegaPositionDecimalPlaces: market.positionDecimalPlaces,
|
||||||
|
};
|
||||||
|
|
||||||
|
onSymbolResolvedCallback(symbolInfo);
|
||||||
|
} catch (err) {
|
||||||
|
onResolveErrorCallback('Cannot resolve symbol');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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) {
|
||||||
|
onErrorCallback('No symbol.ticker');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await client.query<
|
||||||
|
GetBarsQuery,
|
||||||
|
GetBarsQueryVariables
|
||||||
|
>({
|
||||||
|
query: GetBarsDocument,
|
||||||
|
variables: {
|
||||||
|
marketId: symbolInfo.ticker,
|
||||||
|
since: unixTimestampToDate(periodParams.from).toISOString(),
|
||||||
|
to: unixTimestampToDate(periodParams.to).toISOString(),
|
||||||
|
interval: resolutionMap[resolution],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const candleEdges = compact(
|
||||||
|
result.data.market?.candlesConnection?.edges
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!candleEdges.length) {
|
||||||
|
onHistoryCallback([], { noData: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bars = candleEdges.map((edge) => {
|
||||||
|
return prepareBar(
|
||||||
|
edge.node,
|
||||||
|
// @ts-ignore added in resolveSymbol
|
||||||
|
symbolInfo.vegaDecimalPlaces,
|
||||||
|
// @ts-ignore added in resolveSymbol
|
||||||
|
symbolInfo.vegaPositionDecimalPlaces
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
hasHistory.current = true;
|
||||||
|
|
||||||
|
onHistoryCallback(bars, { noData: false });
|
||||||
|
} catch (err) {
|
||||||
|
onErrorCallback(
|
||||||
|
err instanceof Error ? err.message : 'Failed to get bars'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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) {
|
||||||
|
throw new Error('No symbolInfo.ticker');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dont start the subscription if there is no candle history. This protects against a
|
||||||
|
// problem where drawing on the chart throws an error if there is no prior history, instead
|
||||||
|
// no you'll just get the no data message
|
||||||
|
if (!hasHistory.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
subRef.current = client
|
||||||
|
.subscribe<LastBarSubscription, LastBarSubscriptionVariables>({
|
||||||
|
query: LastBarDocument,
|
||||||
|
variables: {
|
||||||
|
marketId: symbolInfo.ticker,
|
||||||
|
interval: resolutionMap[resolution],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.subscribe(({ data }) => {
|
||||||
|
if (data) {
|
||||||
|
const bar = prepareBar(
|
||||||
|
data.candles,
|
||||||
|
// @ts-ignore added in resolveSymbol
|
||||||
|
symbolInfo.vegaDecimalPlaces,
|
||||||
|
// @ts-ignore added in resolveSymbol
|
||||||
|
symbolInfo.vegaPositionDecimalPlaces
|
||||||
|
);
|
||||||
|
|
||||||
|
onTick(bar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We only have one active subscription no need to use the uid provided by unsubscribeBars
|
||||||
|
*/
|
||||||
|
unsubscribeBars: () => {
|
||||||
|
if (subRef.current) {
|
||||||
|
subRef.current.unsubscribe();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return feed;
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (subRef.current) {
|
||||||
|
subRef.current.unsubscribe();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return datafeed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const prepareBar = (
|
||||||
|
bar: BarFragment,
|
||||||
|
decimalPlaces: number,
|
||||||
|
positionDecimalPlaces: number
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
time: new Date(bar.periodStart).getTime(),
|
||||||
|
low: toBigNum(bar.low, decimalPlaces).toNumber(),
|
||||||
|
high: toBigNum(bar.high, decimalPlaces).toNumber(),
|
||||||
|
open: toBigNum(bar.open, decimalPlaces).toNumber(),
|
||||||
|
close: toBigNum(bar.close, decimalPlaces).toNumber(),
|
||||||
|
volume: toBigNum(bar.volume, positionDecimalPlaces).toNumber(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const unixTimestampToDate = (timestamp: number) => {
|
||||||
|
return new Date(timestamp * 1000);
|
||||||
|
};
|
4
libs/trading-view/src/lib/use-t.ts
Normal file
4
libs/trading-view/src/lib/use-t.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
export const ns = 'trading-view';
|
||||||
|
export const useT = () => useTranslation(ns).t;
|
||||||
|
export const useLanguage = () => useTranslation(ns).i18n.language;
|
15
libs/trading-view/src/setup-tests.ts
Normal file
15
libs/trading-view/src/setup-tests.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
|
import { locales } from '@vegaprotocol/i18n';
|
||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
// Set up i18n instance so that components have the correct default
|
||||||
|
// en translations
|
||||||
|
i18n.use(initReactI18next).init({
|
||||||
|
// we init with resources
|
||||||
|
resources: locales,
|
||||||
|
fallbackLng: 'en',
|
||||||
|
ns: ['trading-view'],
|
||||||
|
defaultNS: 'trading-view',
|
||||||
|
});
|
20
libs/trading-view/tsconfig.json
Normal file
20
libs/trading-view/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": false,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extends": "../../tsconfig.base.json"
|
||||||
|
}
|
24
libs/trading-view/tsconfig.lib.json
Normal file
24
libs/trading-view/tsconfig.lib.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
|
||||||
|
"@nx/react/typings/cssmodule.d.ts",
|
||||||
|
"@nx/react/typings/image.d.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"jest.config.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.test.jsx"
|
||||||
|
],
|
||||||
|
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
20
libs/trading-view/tsconfig.spec.json
Normal file
20
libs/trading-view/tsconfig.spec.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"module": "commonjs",
|
||||||
|
"types": ["jest", "node", "@testing-library/jest-dom"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"jest.config.ts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
@ -89,7 +89,6 @@ export const Tabs = ({
|
|||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className={classNames('flex-1 p-1', {
|
className={classNames('flex-1 p-1', {
|
||||||
'bg-vega-clight-700 dark:bg-vega-cdark-700': wrapped,
|
'bg-vega-clight-700 dark:bg-vega-cdark-700': wrapped,
|
||||||
'': wrapped,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{Children.map(children, (child) => {
|
{Children.map(children, (child) => {
|
||||||
@ -97,7 +96,7 @@ export const Tabs = ({
|
|||||||
return (
|
return (
|
||||||
<TabsPrimitive.Content
|
<TabsPrimitive.Content
|
||||||
value={child.props.id}
|
value={child.props.id}
|
||||||
className={classNames('flex flex-nowrap gap-1', {
|
className={classNames('flex items-center flex-nowrap gap-1', {
|
||||||
'justify-end': !wrapped,
|
'justify-end': !wrapped,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -146,6 +146,7 @@
|
|||||||
"@storybook/react-webpack5": "7.5.3",
|
"@storybook/react-webpack5": "7.5.3",
|
||||||
"@svgr/rollup": "^8.0.1",
|
"@svgr/rollup": "^8.0.1",
|
||||||
"@svgr/webpack": "^6.1.2",
|
"@svgr/webpack": "^6.1.2",
|
||||||
|
"@swc-node/register": "~1.6.7",
|
||||||
"@swc/cli": "^0.1.62",
|
"@swc/cli": "^0.1.62",
|
||||||
"@swc/core": "~1.3.85",
|
"@swc/core": "~1.3.85",
|
||||||
"@swc/jest": "0.2.20",
|
"@swc/jest": "0.2.20",
|
||||||
@ -161,9 +162,9 @@
|
|||||||
"@types/lodash": "^4.14.171",
|
"@types/lodash": "^4.14.171",
|
||||||
"@types/node": "18.14.2",
|
"@types/node": "18.14.2",
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
"@types/react": "18.2.24",
|
"@types/react": "18.2.33",
|
||||||
"@types/react-copy-to-clipboard": "5.0.7",
|
"@types/react-copy-to-clipboard": "5.0.7",
|
||||||
"@types/react-dom": "18.2.9",
|
"@types/react-dom": "18.2.14",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-syntax-highlighter": "^15.5.5",
|
"@types/react-syntax-highlighter": "^15.5.5",
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
# Chart
|
# Chart
|
||||||
|
|
||||||
## Display options
|
## Chart lib type
|
||||||
|
|
||||||
|
- **Must** be able to view the Vega chart by default
|
||||||
|
- **Must** be able to switch to the TradingView chart
|
||||||
|
- **Must** have the interval persisted between chart types
|
||||||
|
|
||||||
|
## Pennant chart
|
||||||
|
|
||||||
|
### Display options
|
||||||
|
|
||||||
- **Must** be able to change time interval from a list of intervals (<a name="6007-CHAR-001" href="#6007-CHAR-001">6007-CHAR-001</a>)
|
- **Must** be able to change time interval from a list of intervals (<a name="6007-CHAR-001" href="#6007-CHAR-001">6007-CHAR-001</a>)
|
||||||
- 1m
|
- 1m
|
||||||
@ -36,7 +44,7 @@
|
|||||||
- **Must** be able to add multiple overlays at the same time (<a name="6007-CHAR-008" href="#6007-CHAR-008">6007-CHAR-008</a>)
|
- **Must** be able to add multiple overlays at the same time (<a name="6007-CHAR-008" href="#6007-CHAR-008">6007-CHAR-008</a>)
|
||||||
- **Must** be able to close any overlays selected (<a name="6007-CHAR-009" href="#6007-CHAR-009">6007-CHAR-009</a>)
|
- **Must** be able to close any overlays selected (<a name="6007-CHAR-009" href="#6007-CHAR-009">6007-CHAR-009</a>)
|
||||||
|
|
||||||
## Price and Time
|
### Price and Time
|
||||||
|
|
||||||
- **Must** see details of price from where my mouse cursor is on the chart(<a name="6007-CHAR-010" href="#6007-CHAR-010">6007-CHAR-010</a>)
|
- **Must** see details of price from where my mouse cursor is on the chart(<a name="6007-CHAR-010" href="#6007-CHAR-010">6007-CHAR-010</a>)
|
||||||
|
|
||||||
@ -54,13 +62,13 @@
|
|||||||
- **Must** y axis shows price range(<a name="6007-CHAR-014" href="#6007-CHAR-014">6007-CHAR-014</a>)
|
- **Must** y axis shows price range(<a name="6007-CHAR-014" href="#6007-CHAR-014">6007-CHAR-014</a>)
|
||||||
- **Must** show the last price line on the Y axis (<a name="6007-CHAR-015" href="#6007-CHAR-015">6007-CHAR-015</a>)
|
- **Must** show the last price line on the Y axis (<a name="6007-CHAR-015" href="#6007-CHAR-015">6007-CHAR-015</a>)
|
||||||
|
|
||||||
# Display Types
|
### Display Types
|
||||||
|
|
||||||
## Mountain
|
#### Mountain
|
||||||
|
|
||||||
- **Must** show area line chart with the line being at the last price (<a name="6007-CHAR-016" href="#6007-CHAR-016">6007-CHAR-016</a>)
|
- **Must** show area line chart with the line being at the last price (<a name="6007-CHAR-016" href="#6007-CHAR-016">6007-CHAR-016</a>)
|
||||||
|
|
||||||
## Candlestick
|
#### Candlestick
|
||||||
|
|
||||||
- **Must** body is green if the close is higher than the open (<a name="6007-CHAR-017" href="#6007-CHAR-017">6007-CHAR-017</a>)
|
- **Must** body is green if the close is higher than the open (<a name="6007-CHAR-017" href="#6007-CHAR-017">6007-CHAR-017</a>)
|
||||||
- **Must** body is red if the close is lower than the open (<a name="6007-CHAR-018" href="#6007-CHAR-018">6007-CHAR-018</a>)
|
- **Must** body is red if the close is lower than the open (<a name="6007-CHAR-018" href="#6007-CHAR-018">6007-CHAR-018</a>)
|
||||||
@ -69,11 +77,11 @@
|
|||||||
- **Must** show low price (<a name="6007-CHAR-021" href="#6007-CHAR-021">6007-CHAR-021</a>)
|
- **Must** show low price (<a name="6007-CHAR-021" href="#6007-CHAR-021">6007-CHAR-021</a>)
|
||||||
- **Must** show close price (<a name="6007-CHAR-022" href="#6007-CHAR-022">6007-CHAR-022</a>)
|
- **Must** show close price (<a name="6007-CHAR-022" href="#6007-CHAR-022">6007-CHAR-022</a>)
|
||||||
|
|
||||||
## Line
|
#### Line
|
||||||
|
|
||||||
- **Must** show line on the chart with the line being the the last price (<a name="6007-CHAR-023" href="#6007-CHAR-023">6007-CHAR-023</a>)
|
- **Must** show line on the chart with the line being the the last price (<a name="6007-CHAR-023" href="#6007-CHAR-023">6007-CHAR-023</a>)
|
||||||
|
|
||||||
## OHLC
|
#### OHLC
|
||||||
|
|
||||||
- **Must** show open price (<a name="6007-CHAR-024" href="#6007-CHAR-024">6007-CHAR-024</a>)
|
- **Must** show open price (<a name="6007-CHAR-024" href="#6007-CHAR-024">6007-CHAR-024</a>)
|
||||||
- **Must** show high price (<a name="6007-CHAR-025" href="#6007-CHAR-025">6007-CHAR-025</a>)
|
- **Must** show high price (<a name="6007-CHAR-025" href="#6007-CHAR-025">6007-CHAR-025</a>)
|
||||||
@ -82,63 +90,68 @@
|
|||||||
- **Must** show in green if the close is higher than the open (<a name="6007-CHAR-028" href="#6007-CHAR-028">6007-CHAR-028</a>)
|
- **Must** show in green if the close is higher than the open (<a name="6007-CHAR-028" href="#6007-CHAR-028">6007-CHAR-028</a>)
|
||||||
- **Must** show in red if the close is lower than the open (<a name="6007-CHAR-029" href="#6007-CHAR-029">6007-CHAR-029</a>)
|
- **Must** show in red if the close is lower than the open (<a name="6007-CHAR-029" href="#6007-CHAR-029">6007-CHAR-029</a>)
|
||||||
|
|
||||||
# Overlays
|
### Overlays
|
||||||
|
|
||||||
## Bollinger bands
|
#### Bollinger bands
|
||||||
|
|
||||||
- **Must** show upper band (<a name="6007-CHAR-030" href="#6007-CHAR-030">6007-CHAR-030</a>)
|
- **Must** show upper band (<a name="6007-CHAR-030" href="#6007-CHAR-030">6007-CHAR-030</a>)
|
||||||
- **Must** show lower band (<a name="6007-CHAR-031" href="#6007-CHAR-031">6007-CHAR-031</a>)
|
- **Must** show lower band (<a name="6007-CHAR-031" href="#6007-CHAR-031">6007-CHAR-031</a>)
|
||||||
- **Must** show band values at time of cursor position (<a name="6007-CHAR-032" href="#6007-CHAR-032">6007-CHAR-032</a>)
|
- **Must** show band values at time of cursor position (<a name="6007-CHAR-032" href="#6007-CHAR-032">6007-CHAR-032</a>)
|
||||||
|
|
||||||
## Envelope
|
#### Envelope
|
||||||
|
|
||||||
- **Must** show upper line (<a name="6007-CHAR-033" href="#6007-CHAR-033">6007-CHAR-033</a>)
|
- **Must** show upper line (<a name="6007-CHAR-033" href="#6007-CHAR-033">6007-CHAR-033</a>)
|
||||||
- **Must** show lower line (<a name="6007-CHAR-034" href="#6007-CHAR-034">6007-CHAR-034</a>)
|
- **Must** show lower line (<a name="6007-CHAR-034" href="#6007-CHAR-034">6007-CHAR-034</a>)
|
||||||
- **Must** show line values at time of cursor position (<a name="6007-CHAR-035" href="#6007-CHAR-035">6007-CHAR-035</a>)
|
- **Must** show line values at time of cursor position (<a name="6007-CHAR-035" href="#6007-CHAR-035">6007-CHAR-035</a>)
|
||||||
|
|
||||||
## EMA
|
#### EMA
|
||||||
|
|
||||||
- **Must** show line (<a name="6007-CHAR-036" href="#6007-CHAR-036">6007-CHAR-036</a>)
|
- **Must** show line (<a name="6007-CHAR-036" href="#6007-CHAR-036">6007-CHAR-036</a>)
|
||||||
- **Must** show line value at time of cursor position (<a name="6007-CHAR-037" href="#6007-CHAR-037">6007-CHAR-037</a>)
|
- **Must** show line value at time of cursor position (<a name="6007-CHAR-037" href="#6007-CHAR-037">6007-CHAR-037</a>)
|
||||||
|
|
||||||
## Moving Average
|
#### Moving Average
|
||||||
|
|
||||||
- **Must** show line (<a name="6007-CHAR-038" href="#6007-CHAR-038">6007-CHAR-038</a>)
|
- **Must** show line (<a name="6007-CHAR-038" href="#6007-CHAR-038">6007-CHAR-038</a>)
|
||||||
- **Must** show line value at time of cursor position (<a name="6007-CHAR-039" href="#6007-CHAR-039">6007-CHAR-039</a>)
|
- **Must** show line value at time of cursor position (<a name="6007-CHAR-039" href="#6007-CHAR-039">6007-CHAR-039</a>)
|
||||||
|
|
||||||
## Price monitoring bounds
|
#### Price monitoring bounds
|
||||||
|
|
||||||
- **Must** show min line (<a name="6007-CHAR-040" href="#6007-CHAR-040">6007-CHAR-040</a>)
|
- **Must** show min line (<a name="6007-CHAR-040" href="#6007-CHAR-040">6007-CHAR-040</a>)
|
||||||
- **Must** show max line (<a name="6007-CHAR-041" href="#6007-CHAR-041">6007-CHAR-041</a>)
|
- **Must** show max line (<a name="6007-CHAR-041" href="#6007-CHAR-041">6007-CHAR-041</a>)
|
||||||
- **Must** show reference line (<a name="6007-CHAR-042" href="#6007-CHAR-042">6007-CHAR-042</a>)
|
- **Must** show reference line (<a name="6007-CHAR-042" href="#6007-CHAR-042">6007-CHAR-042</a>)
|
||||||
- **Must** show line values at time of cursor position (<a name="6007-CHAR-043" href="#6007-CHAR-043">6007-CHAR-043</a>)
|
- **Must** show line values at time of cursor position (<a name="6007-CHAR-043" href="#6007-CHAR-043">6007-CHAR-043</a>)
|
||||||
|
|
||||||
# Studies
|
### Studies
|
||||||
|
|
||||||
## Eldar-ray
|
#### Eldar-ray
|
||||||
|
|
||||||
- **Must** show bear power line (<a name="6007-CHAR-044" href="#6007-CHAR-044">6007-CHAR-044</a>)
|
- **Must** show bear power line (<a name="6007-CHAR-044" href="#6007-CHAR-044">6007-CHAR-044</a>)
|
||||||
- **Must** show bull power line (<a name="6007-CHAR-045" href="#6007-CHAR-045">6007-CHAR-045</a>)
|
- **Must** show bull power line (<a name="6007-CHAR-045" href="#6007-CHAR-045">6007-CHAR-045</a>)
|
||||||
- **Must** show line values at time of cursor position (<a name="6007-CHAR-046" href="#6007-CHAR-046">6007-CHAR-046</a>)
|
- **Must** show line values at time of cursor position (<a name="6007-CHAR-046" href="#6007-CHAR-046">6007-CHAR-046</a>)
|
||||||
|
|
||||||
## Force index
|
#### Force index
|
||||||
|
|
||||||
- **Must** show force line (<a name="6007-CHAR-047" href="#6007-CHAR-047">6007-CHAR-047</a>)
|
- **Must** show force line (<a name="6007-CHAR-047" href="#6007-CHAR-047">6007-CHAR-047</a>)
|
||||||
- **Must** show line value at time of cursor position (<a name="6007-CHAR-048" href="#6007-CHAR-048">6007-CHAR-048</a>)
|
- **Must** show line value at time of cursor position (<a name="6007-CHAR-048" href="#6007-CHAR-048">6007-CHAR-048</a>)
|
||||||
|
|
||||||
## MACD
|
#### MACD
|
||||||
|
|
||||||
- **Must** show MACD line (<a name="6007-CHAR-049" href="#6007-CHAR-049">6007-CHAR-049</a>)
|
- **Must** show MACD line (<a name="6007-CHAR-049" href="#6007-CHAR-049">6007-CHAR-049</a>)
|
||||||
- **Must** show signal line (<a name="6007-CHAR-050" href="#6007-CHAR-050">6007-CHAR-050</a>)
|
- **Must** show signal line (<a name="6007-CHAR-050" href="#6007-CHAR-050">6007-CHAR-050</a>)
|
||||||
- **Must** show histogram (<a name="6007-CHAR-051" href="#6007-CHAR-051">6007-CHAR-051</a>)
|
- **Must** show histogram (<a name="6007-CHAR-051" href="#6007-CHAR-051">6007-CHAR-051</a>)
|
||||||
- **Must** show line values at time of cursor position (<a name="6007-CHAR-052" href="#6007-CHAR-052">6007-CHAR-052</a>)
|
- **Must** show line values at time of cursor position (<a name="6007-CHAR-052" href="#6007-CHAR-052">6007-CHAR-052</a>)
|
||||||
|
|
||||||
## RSI
|
#### RSI
|
||||||
|
|
||||||
- **Must** show RSI line (<a name="6007-CHAR-053" href="#6007-CHAR-053">6007-CHAR-053</a>)
|
- **Must** show RSI line (<a name="6007-CHAR-053" href="#6007-CHAR-053">6007-CHAR-053</a>)
|
||||||
- **Must** show line value at time of cursor position (<a name="6007-CHAR-054" href="#6007-CHAR-054">6007-CHAR-054</a>)
|
- **Must** show line value at time of cursor position (<a name="6007-CHAR-054" href="#6007-CHAR-054">6007-CHAR-054</a>)
|
||||||
|
|
||||||
## Volume
|
#### Volume
|
||||||
|
|
||||||
- **Must** show volume bars (<a name="6007-CHAR-055" href="#6007-CHAR-055">6007-CHAR-055</a>)
|
- **Must** show volume bars (<a name="6007-CHAR-055" href="#6007-CHAR-055">6007-CHAR-055</a>)
|
||||||
- **Must** show bar value at time of cursor position (<a name="6007-CHAR-056" href="#6007-CHAR-056">6007-CHAR-056</a>)
|
- **Must** show bar value at time of cursor position (<a name="6007-CHAR-056" href="#6007-CHAR-056">6007-CHAR-056</a>)
|
||||||
|
|
||||||
|
## TradingView
|
||||||
|
|
||||||
|
- **Must** persist interval in chart settings
|
||||||
|
- **Must** must show an attribution to trading view
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
],
|
],
|
||||||
"@vegaprotocol/tendermint": ["libs/tendermint/src/index.ts"],
|
"@vegaprotocol/tendermint": ["libs/tendermint/src/index.ts"],
|
||||||
"@vegaprotocol/trades": ["libs/trades/src/index.ts"],
|
"@vegaprotocol/trades": ["libs/trades/src/index.ts"],
|
||||||
|
"@vegaprotocol/trading-view": ["libs/trading-view/src/index.ts"],
|
||||||
"@vegaprotocol/types": ["libs/types/src/index.ts"],
|
"@vegaprotocol/types": ["libs/types/src/index.ts"],
|
||||||
"@vegaprotocol/ui-toolkit": ["libs/ui-toolkit/src/index.ts"],
|
"@vegaprotocol/ui-toolkit": ["libs/ui-toolkit/src/index.ts"],
|
||||||
"@vegaprotocol/utils": ["libs/utils/src/index.ts"],
|
"@vegaprotocol/utils": ["libs/utils/src/index.ts"],
|
||||||
|
45
yarn.lock
45
yarn.lock
@ -6643,6 +6643,31 @@
|
|||||||
"@svgr/plugin-jsx" "8.1.0"
|
"@svgr/plugin-jsx" "8.1.0"
|
||||||
"@svgr/plugin-svgo" "8.1.0"
|
"@svgr/plugin-svgo" "8.1.0"
|
||||||
|
|
||||||
|
"@swc-node/core@^1.10.6":
|
||||||
|
version "1.10.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc-node/core/-/core-1.10.6.tgz#5c2af68bd4c9c8f5d91178a724af341a4402f5b6"
|
||||||
|
integrity sha512-lDIi/rPosmKIknWzvs2/Fi9zWRtbkx8OJ9pQaevhsoGzJSal8Pd315k1W5AIrnknfdAB4HqRN12fk6AhqnrEEw==
|
||||||
|
|
||||||
|
"@swc-node/register@~1.6.7":
|
||||||
|
version "1.6.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc-node/register/-/register-1.6.8.tgz#4e2402b42ae5b538d5041e0c4d86d9c3c8d5b323"
|
||||||
|
integrity sha512-74ijy7J9CWr1Z88yO+ykXphV29giCrSpANQPQRooE0bObpkTO1g4RzQovIfbIaniBiGDDVsYwDoQ3FIrCE8HcQ==
|
||||||
|
dependencies:
|
||||||
|
"@swc-node/core" "^1.10.6"
|
||||||
|
"@swc-node/sourcemap-support" "^0.3.0"
|
||||||
|
colorette "^2.0.19"
|
||||||
|
debug "^4.3.4"
|
||||||
|
pirates "^4.0.5"
|
||||||
|
tslib "^2.5.0"
|
||||||
|
|
||||||
|
"@swc-node/sourcemap-support@^0.3.0":
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@swc-node/sourcemap-support/-/sourcemap-support-0.3.0.tgz#e8a0d139bd3e8db39635f63fde43dbe6c39237cc"
|
||||||
|
integrity sha512-gqBJSmJMWomZFxlppaKea7NeAqFrDrrS0RMt24No92M3nJWcyI9YKGEQKl+EyJqZ5gh6w1s0cTklMHMzRwA1NA==
|
||||||
|
dependencies:
|
||||||
|
source-map-support "^0.5.21"
|
||||||
|
tslib "^2.5.0"
|
||||||
|
|
||||||
"@swc/cli@^0.1.62":
|
"@swc/cli@^0.1.62":
|
||||||
version "0.1.62"
|
version "0.1.62"
|
||||||
resolved "https://registry.yarnpkg.com/@swc/cli/-/cli-0.1.62.tgz#6442fde2fcf75175a300fb4fcf30f8c60bbb3ab3"
|
resolved "https://registry.yarnpkg.com/@swc/cli/-/cli-0.1.62.tgz#6442fde2fcf75175a300fb4fcf30f8c60bbb3ab3"
|
||||||
@ -7415,10 +7440,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-dom@18.2.9":
|
"@types/react-dom@18.2.14":
|
||||||
version "18.2.9"
|
version "18.2.14"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.9.tgz#c4ce3c7c91a134e1bff58692aa2d2f2f4029c38b"
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.14.tgz#c01ba40e5bb57fc1dc41569bb3ccdb19eab1c539"
|
||||||
integrity sha512-6nNhVzZ9joQ6F7lozrASuQKC0Kf6ArYMU+DqA2ZrUbB+d+9lC6ZLn1GxiEBI1edmAwvTULtuJ6uPZpv3XudwUg==
|
integrity sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
@ -7484,10 +7509,10 @@
|
|||||||
"@types/scheduler" "*"
|
"@types/scheduler" "*"
|
||||||
csstype "^3.0.2"
|
csstype "^3.0.2"
|
||||||
|
|
||||||
"@types/react@18.2.24":
|
"@types/react@18.2.33":
|
||||||
version "18.2.24"
|
version "18.2.33"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.24.tgz#3c7d68c02e0205a472f04abe4a0c1df35d995c05"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.33.tgz#055356243dc4350a9ee6c6a2c07c5cae12e38877"
|
||||||
integrity sha512-Ee0Jt4sbJxMu1iDcetZEIKQr99J1Zfb6D4F3qfUWoR1JpInkY1Wdg4WwCyBjL257D0+jGqSl1twBjV8iCaC0Aw==
|
integrity sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/prop-types" "*"
|
"@types/prop-types" "*"
|
||||||
"@types/scheduler" "*"
|
"@types/scheduler" "*"
|
||||||
@ -10228,7 +10253,7 @@ colorette@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
|
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
|
||||||
integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
|
integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
|
||||||
|
|
||||||
colorette@^2.0.10, colorette@^2.0.16, colorette@^2.0.20:
|
colorette@^2.0.10, colorette@^2.0.16, colorette@^2.0.19, colorette@^2.0.20:
|
||||||
version "2.0.20"
|
version "2.0.20"
|
||||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
||||||
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
|
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
|
||||||
@ -20355,7 +20380,7 @@ source-map-support@0.5.19:
|
|||||||
buffer-from "^1.0.0"
|
buffer-from "^1.0.0"
|
||||||
source-map "^0.6.0"
|
source-map "^0.6.0"
|
||||||
|
|
||||||
source-map-support@^0.5.16, source-map-support@~0.5.20:
|
source-map-support@^0.5.16, source-map-support@^0.5.21, source-map-support@~0.5.20:
|
||||||
version "0.5.21"
|
version "0.5.21"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
|
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
|
||||||
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
|
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
|
||||||
|
Loading…
Reference in New Issue
Block a user