fix(trading): market selector volume update (#3989)

This commit is contained in:
m.ray 2023-06-07 04:45:52 +03:00 committed by GitHub
parent 878bed9c7a
commit aae5c44fa4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 271 additions and 117 deletions

View File

@ -57,7 +57,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
it('market volume displayed', () => {
cy.getByTestId(marketTitle).contains('Market volume').click();
validateMarketDataRow(0, '24 Hour Volume', 'Unknown');
validateMarketDataRow(1, 'Open Interest', '-');
validateMarketDataRow(2, 'Best Bid Volume', '1');
validateMarketDataRow(3, 'Best Offer Volume', '3');

View File

@ -40,26 +40,26 @@ describe('markets selector', { tags: '@smoke' }, () => {
{
code: 'SOLUSD',
markPrice: '84.41XYZalpha',
change: '+200.00%',
vol: '324h vol',
change: '',
vol: '0.0024h vol',
},
{
code: 'ETHBTC.QM21',
markPrice: '46,126.90058tBTC',
change: '+200.00%',
vol: '324h vol',
change: '',
vol: '0.0024h vol',
},
{
code: 'BTCUSD.MF21',
markPrice: '46,126.90058tDAI',
change: '+200.00%',
vol: '324h vol',
change: '',
vol: '0.0024h vol',
},
{
code: 'AAPL.MF21',
markPrice: '46,126.90058tUSDC',
change: '+200.00%',
vol: '324h vol',
change: '',
vol: '0.0024h vol',
},
];
cy.getByTestId(list)
@ -80,7 +80,7 @@ describe('markets selector', { tags: '@smoke' }, () => {
market.change
);
// 6001-MARK-025
expect(item.find('[data-testid="sparkline-svg"]')).to.exist;
expect(item.find('[data-testid="sparkline-svg"]')).to.not.exist;
});
});

View File

@ -6,25 +6,32 @@ import { MemoryRouter } from 'react-router-dom';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type {
MarketCandlesQuery,
MarketCandlesQueryVariables,
MarketDataUpdateFieldsFragment,
MarketDataUpdateSubscription,
} from '@vegaprotocol/markets';
import { MarketCandlesDocument } from '@vegaprotocol/markets';
import { MarketDataUpdateDocument } from '@vegaprotocol/markets';
import {
AuctionTrigger,
Interval,
MarketState,
MarketTradingMode,
} from '@vegaprotocol/types';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { subDays } from 'date-fns';
describe('MarketSelectorItem', () => {
const yesterday = new Date();
yesterday.setHours(yesterday.getHours() - 20);
const market = createMarketFragment({
id: 'market-0',
decimalPlaces: 2,
// @ts-ignore fragment doesn't contain candles
candles: [
{ close: '5', volume: '50' },
{ close: '10', volume: '50' },
{ close: '5', volume: '50', periodStart: yesterday.toISOString() },
{ close: '10', volume: '50', periodStart: yesterday.toISOString() },
],
tradableInstrument: {
instrument: {
@ -36,6 +43,7 @@ describe('MarketSelectorItem', () => {
},
},
});
const marketData: MarketDataUpdateFieldsFragment = {
__typename: 'ObservableMarketData',
marketId: market.id,
@ -63,26 +71,32 @@ describe('MarketSelectorItem', () => {
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
priceMonitoringBounds: null,
};
const mock: MockedResponse<MarketDataUpdateSubscription> = {
request: {
query: MarketDataUpdateDocument,
variables: {
marketId: market.id,
},
const candles = [
{
open: '5',
close: '5',
high: '5',
low: '5',
volume: '50',
periodStart: yesterday.toISOString(),
},
result: {
data: {
marketsData: [marketData],
},
{
open: '10',
close: '10',
high: '10',
low: '10',
volume: '50',
periodStart: yesterday.toISOString(),
},
};
];
const mockOnSelect = jest.fn();
const renderJsx = () => {
const renderJsx = (mocks: MockedResponse[]) => {
return render(
<MemoryRouter>
<MockedProvider mocks={[mock]}>
<MockedProvider mocks={mocks}>
<MarketSelectorItem
market={market}
currentMarketId={market.id}
@ -94,11 +108,66 @@ describe('MarketSelectorItem', () => {
);
};
let dateSpy: jest.SpyInstance;
const ts = 1685577600000; // 2023-06-01
beforeAll(() => {
dateSpy = jest.spyOn(Date, 'now').mockImplementation(() => ts);
});
afterAll(() => {
dateSpy.mockRestore();
});
it('renders market information', async () => {
const symbol =
market.tradableInstrument.instrument.product.settlementAsset.symbol;
renderJsx();
const mock: MockedResponse<MarketDataUpdateSubscription> = {
request: {
query: MarketDataUpdateDocument,
variables: {
marketId: market.id,
},
},
result: {
data: {
marketsData: [marketData],
},
},
};
const since = subDays(Date.now(), 5).toISOString();
const variables: MarketCandlesQueryVariables = {
marketId: market.id,
interval: Interval.INTERVAL_I1H,
since,
};
const mockCandles: MockedResponse<MarketCandlesQuery> = {
request: {
query: MarketCandlesDocument,
variables,
},
result: {
data: {
marketsConnection: {
edges: [
{
node: {
candlesConnection: {
edges: candles.map((c) => ({
node: c,
})),
},
},
},
],
},
},
},
};
renderJsx([mock, mockCandles]);
const link = screen.getByRole('link');
// link renders and is styled
@ -106,18 +175,17 @@ describe('MarketSelectorItem', () => {
expect(link).toHaveClass('ring-1');
expect(screen.getByTitle('24h vol')).toHaveTextContent('100');
expect(screen.getByTitle('24h vol')).toHaveTextContent('0.00');
expect(screen.getByTitle(symbol)).toHaveTextContent('-');
// candles are loaded immediately
expect(screen.getByTestId('market-item-change')).toHaveTextContent(
'+100.00%'
);
await waitFor(() => {
expect(screen.getByTitle('24h vol')).toHaveTextContent('100');
expect(screen.getByTitle(symbol)).toHaveTextContent(
addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
);
expect(screen.getByTestId('market-item-change')).toHaveTextContent(
'+100.00%'
);
});
await userEvent.click(link);

View File

@ -1,4 +1,4 @@
import type { CSSProperties } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import {
@ -8,6 +8,7 @@ import {
} from '@vegaprotocol/utils';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
import { calcCandleVolume } from '@vegaprotocol/markets';
import { useCandles } from '@vegaprotocol/markets';
import { useMarketDataUpdateSubscription } from '@vegaprotocol/markets';
import { Sparkline } from '@vegaprotocol/ui-toolkit';
import {
@ -80,8 +81,9 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
: '';
const instrument = market.tradableInstrument.instrument;
const { oneDayCandles } = useCandles({ marketId: market.id });
const vol = market.candles ? calcCandleVolume(market.candles) : '0';
const vol = oneDayCandles ? calcCandleVolume(oneDayCandles) : '0';
const volume =
vol && vol !== '0'
? addDecimalsFormatNumber(vol, market.positionDecimalPlaces)
@ -111,20 +113,20 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
value={price}
label={instrument.product.settlementAsset.symbol}
/>
<div className="relative">
{market.candles && (
<PriceChange candles={market.candles.map((c) => c.close)} />
<div className="relative text-xs p-1">
{oneDayCandles && (
<PriceChange candles={oneDayCandles.map((c) => c.close)} />
)}
<div
// absolute so height is not larger than price change value
className="absolute right-0 bottom-0 w-[120px]"
>
{market.candles && (
{oneDayCandles && (
<Sparkline
width={120}
height={20}
data={market.candles.filter(Boolean).map((c) => Number(c.close))}
data={oneDayCandles.map((c) => Number(c.close))}
/>
)}
</div>
@ -133,7 +135,13 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
);
};
const DataRow = ({ value, label }: { value: string; label: string }) => {
const DataRow = ({
value,
label,
}: {
value: string | ReactNode;
label: string;
}) => {
return (
<div
className="text-ellipsis whitespace-nowrap overflow-hidden leading-tight"

View File

@ -53,7 +53,7 @@ export const useMarketSelectorList = ({
});
if (sort === Sort.None) {
// Sort by market state primarilly and AtoZ secondarilly
// Sort by market state primarily and AtoZ secondarily
return orderBy(
markets,
[

View File

@ -1,15 +1,8 @@
import type { RefObject } from 'react';
import { useInView } from 'react-intersection-observer';
import { isNumeric } from '@vegaprotocol/utils';
import { useFiveDaysAgo, useYesterday } from '@vegaprotocol/react-helpers';
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
import { PriceChangeCell } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import type { CandleClose } from '@vegaprotocol/types';
import { marketCandlesProvider } from '../../market-candles-provider';
import type { MarketCandlesFieldsFragment } from '../../__generated__/market-candles';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useCandles } from '../../hooks/use-candles';
interface Props {
marketId?: string;
@ -17,38 +10,16 @@ interface Props {
initialValue?: string[];
isHeader?: boolean;
noUpdate?: boolean;
inViewRoot?: RefObject<Element>;
}
export const Last24hPriceChange = ({
marketId,
decimalPlaces,
initialValue,
inViewRoot,
}: Props) => {
const [ref, inView] = useInView({ root: inViewRoot?.current });
const fiveDaysAgo = useFiveDaysAgo();
const yesterday = useYesterday();
const { data, error } = useThrottledDataProvider({
dataProvider: marketCandlesProvider,
variables: {
marketId: marketId || '',
interval: Schema.Interval.INTERVAL_I1H,
since: new Date(fiveDaysAgo).toISOString(),
},
skip: !marketId || !inView,
const { oneDayCandles, error, fiveDaysCandles } = useCandles({
marketId,
});
const fiveDaysCandles = data?.filter((candle) => Boolean(candle));
const candles = fiveDaysCandles?.filter((candle) =>
isCandleLessThan24hOld(candle, yesterday)
);
const oneDayCandles =
candles
?.map((candle) => candle?.close)
.filter((c): c is CandleClose => c !== null) || initialValue;
if (
fiveDaysCandles &&
fiveDaysCandles.length > 0 &&
@ -68,30 +39,18 @@ export const Last24hPriceChange = ({
</span>
}
>
<span ref={ref}>{t('Unknown')} </span>
<span>-</span>
</Tooltip>
);
}
if (error || !isNumeric(decimalPlaces)) {
return <span ref={ref}>-</span>;
return <span>-</span>;
}
return (
<PriceChangeCell
candles={oneDayCandles || []}
candles={oneDayCandles?.map((c) => c.close) || initialValue || []}
decimalPlaces={decimalPlaces}
ref={ref}
/>
);
};
export const isCandleLessThan24hOld = (
candle: MarketCandlesFieldsFragment | undefined,
yesterday: number
) => {
if (!candle?.open) {
return false;
}
const candleDate = new Date(candle.close);
return candleDate > new Date(yesterday);
};

View File

@ -1,20 +1,13 @@
import type { RefObject } from 'react';
import { useInView } from 'react-intersection-observer';
import { marketCandlesProvider } from '../../market-candles-provider';
import { calcCandleVolume } from '../../market-utils';
import { addDecimalsFormatNumber, isNumeric } from '@vegaprotocol/utils';
import { useFiveDaysAgo, useYesterday } from '@vegaprotocol/react-helpers';
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
import * as Schema from '@vegaprotocol/types';
import { isCandleLessThan24hOld } from '../last-24h-price-change';
import { t } from '@vegaprotocol/i18n';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { useCandles } from '../../hooks';
interface Props {
marketId?: string;
positionDecimalPlaces?: number;
formatDecimals?: number;
inViewRoot?: RefObject<Element>;
initialValue?: string;
}
@ -22,29 +15,12 @@ export const Last24hVolume = ({
marketId,
positionDecimalPlaces,
formatDecimals,
inViewRoot,
initialValue,
}: Props) => {
const yesterday = useYesterday();
const fiveDaysAgo = useFiveDaysAgo();
const [ref, inView] = useInView({ root: inViewRoot?.current });
const { data } = useThrottledDataProvider({
dataProvider: marketCandlesProvider,
variables: {
marketId: marketId || '',
interval: Schema.Interval.INTERVAL_I1H,
since: new Date(fiveDaysAgo).toISOString(),
},
skip: !(inView && marketId),
const { oneDayCandles, fiveDaysCandles } = useCandles({
marketId,
});
const fiveDaysCandles = data?.filter((candle) => Boolean(candle));
const oneDayCandles = fiveDaysCandles?.filter((candle) =>
isCandleLessThan24hOld(candle, yesterday)
);
if (
fiveDaysCandles &&
fiveDaysCandles.length > 0 &&
@ -72,20 +48,21 @@ export const Last24hVolume = ({
</div>
}
>
<span ref={ref}>{t('Unknown')} </span>
<span>-</span>
</Tooltip>
);
}
const candleVolume = oneDayCandles
? calcCandleVolume(oneDayCandles)
: initialValue;
return (
<Tooltip
description={t(
'The total number of contracts traded in the last 24 hours.'
)}
>
<span ref={ref}>
<span>
{candleVolume && isNumeric(positionDecimalPlaces)
? addDecimalsFormatNumber(
candleVolume,

View File

@ -2,3 +2,4 @@ export * from './use-market-oracle';
export * from './use-oracle-markets';
export * from './use-oracle-proofs';
export * from './use-oracle-spec-binding-data';
export * from './use-candles';

View File

@ -0,0 +1,105 @@
import { renderHook } from '@testing-library/react';
import { useCandles } from './use-candles';
const today = new Date();
const fiveDaysAgo = new Date();
fiveDaysAgo.setDate(today.getDate() - 5);
const mockData = [
{
high: '6293819',
low: '6263737',
open: '6266893',
close: '6293819',
volume: '72447',
periodStart: today.toISOString(),
__typename: 'Candle',
},
null,
{
high: '6309988',
low: '6296335',
open: '6307451',
close: '6296335',
volume: '73657',
periodStart: today.toISOString(),
__typename: 'Candle',
},
{
high: '6315153',
low: '6294001',
open: '6296335',
close: '6315152',
volume: '89395',
periodStart: today.toISOString(),
__typename: 'Candle',
},
{
high: '6309988',
low: '6296335',
open: '6307451',
close: '6296335',
volume: '73657',
periodStart: fiveDaysAgo.toISOString(),
__typename: 'Candle',
},
{
high: '6315153',
low: '6294001',
open: '6296335',
close: '6315152',
volume: '89395',
periodStart: fiveDaysAgo.toISOString(),
__typename: 'Candle',
},
];
jest.mock('@vegaprotocol/data-provider', () => {
return {
...jest.requireActual('@vegaprotocol/data-provider'),
useThrottledDataProvider: jest.fn(() => ({
data: mockData,
error: false,
})),
};
});
describe('useCandles', () => {
it('should return one day candles and five day candles', () => {
const { result } = renderHook(() => useCandles({ marketId: '3456789' }));
const expectedOneDayCandles = [
{
high: '6293819',
low: '6263737',
open: '6266893',
close: '6293819',
volume: '72447',
periodStart: today.toISOString(),
__typename: 'Candle',
},
{
high: '6309988',
low: '6296335',
open: '6307451',
close: '6296335',
volume: '73657',
periodStart: today.toISOString(),
__typename: 'Candle',
},
{
high: '6315153',
low: '6294001',
open: '6296335',
close: '6315152',
volume: '89395',
periodStart: today.toISOString(),
__typename: 'Candle',
},
];
expect(result.current).toStrictEqual({
oneDayCandles: expectedOneDayCandles,
fiveDaysCandles: mockData.filter(Boolean),
error: false,
});
});
});

View File

@ -0,0 +1,37 @@
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
import { useFiveDaysAgo, useYesterday } from '@vegaprotocol/react-helpers';
import type { MarketCandlesFieldsFragment } from '../__generated__';
import { marketCandlesProvider } from '../market-candles-provider';
import { Interval } from '@vegaprotocol/types';
export const useCandles = ({ marketId }: { marketId?: string }) => {
const fiveDaysAgo = useFiveDaysAgo();
const yesterday = useYesterday();
const { data, error } = useThrottledDataProvider({
dataProvider: marketCandlesProvider,
variables: {
marketId: marketId || '',
interval: Interval.INTERVAL_I1H,
since: new Date(fiveDaysAgo).toISOString(),
},
skip: !marketId,
});
const fiveDaysCandles = data?.filter(Boolean);
const oneDayCandles = fiveDaysCandles?.filter((candle) =>
isCandleLessThan24hOld(candle, yesterday)
);
return { oneDayCandles, error, fiveDaysCandles };
};
export const isCandleLessThan24hOld = (
candle: MarketCandlesFieldsFragment | undefined,
yesterday: number
) => {
if (!candle?.periodStart) {
return false;
}
const candleDate = new Date(candle.periodStart);
return candleDate > new Date(yesterday);
};