fix(trading): price and volume in the last 24h can be incorrectly reported (#3870)

Co-authored-by: asiaznik <artur@vegaprotocol.io>
This commit is contained in:
m.ray 2023-05-25 13:39:07 +03:00 committed by GitHub
parent 3ed615c36e
commit 1ae1fdff5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 153 additions and 49 deletions

View File

@ -56,13 +56,7 @@ export const HeaderStats = ({ market }: HeaderStatsProps) => {
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat
heading={t('Volume (24h)')}
testId="market-volume"
description={t(
'The total number of contracts traded in the last 24 hours.'
)}
>
<HeaderStat heading={t('Volume (24h)')} testId="market-volume">
<Last24hVolume
marketId={market?.id}
positionDecimalPlaces={market?.positionDecimalPlaces}

View File

@ -25,7 +25,7 @@ export const PriceChangeCell = memo(
ref={ref}
className={`${signedNumberCssClass(
change
)} flex items-center gap-2 justify-end font-mono text-ui-small`}
)} flex items-center gap-2 font-mono text-ui-small`}
>
<Arrow value={change} />
<span data-testid="price-change-percentage">

View File

@ -1,12 +1,15 @@
import type { RefObject } from 'react';
import { useInView } from 'react-intersection-observer';
import { isNumeric } from '@vegaprotocol/utils';
import { useYesterday } from '@vegaprotocol/react-helpers';
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';
interface Props {
marketId?: string;
@ -24,30 +27,71 @@ export const Last24hPriceChange = ({
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(yesterday).toISOString(),
since: new Date(fiveDaysAgo).toISOString(),
},
skip: !marketId || !inView,
});
const candles =
data
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 &&
(!oneDayCandles || oneDayCandles?.length === 0)
) {
return (
<Tooltip
description={
<span className="justify-start">
{t(
'24 hour change is unavailable at this time. The price change in the last 120 hours is:'
)}{' '}
<PriceChangeCell
candles={fiveDaysCandles.map((c) => c.close) || []}
decimalPlaces={decimalPlaces}
/>
</span>
}
>
<span ref={ref}>{t('Unknown')} </span>
</Tooltip>
);
}
if (error || !isNumeric(decimalPlaces)) {
return <span ref={ref}>-</span>;
}
return (
<PriceChangeCell
candles={candles || []}
candles={oneDayCandles || []}
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

@ -3,9 +3,12 @@ import { useInView } from 'react-intersection-observer';
import { marketCandlesProvider } from '../../market-candles-provider';
import { calcCandleVolume } from '../../market-utils';
import { addDecimalsFormatNumber, isNumeric } from '@vegaprotocol/utils';
import { useYesterday } from '@vegaprotocol/react-helpers';
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';
interface Props {
marketId?: string;
@ -23,6 +26,7 @@ export const Last24hVolume = ({
initialValue,
}: Props) => {
const yesterday = useYesterday();
const fiveDaysAgo = useFiveDaysAgo();
const [ref, inView] = useInView({ root: inViewRoot?.current });
const { data } = useThrottledDataProvider({
@ -30,20 +34,66 @@ export const Last24hVolume = ({
variables: {
marketId: marketId || '',
interval: Schema.Interval.INTERVAL_I1H,
since: new Date(yesterday).toISOString(),
since: new Date(fiveDaysAgo).toISOString(),
},
skip: !(inView && marketId),
});
const candleVolume = data ? calcCandleVolume(data) : initialValue;
return (
<span ref={ref}>
{candleVolume && isNumeric(positionDecimalPlaces)
const fiveDaysCandles = data?.filter((candle) => Boolean(candle));
const oneDayCandles = fiveDaysCandles?.filter((candle) =>
isCandleLessThan24hOld(candle, yesterday)
);
if (
fiveDaysCandles &&
fiveDaysCandles.length > 0 &&
(!oneDayCandles || oneDayCandles?.length === 0)
) {
const candleVolume = calcCandleVolume(fiveDaysCandles);
const candleVolumeValue =
candleVolume && isNumeric(positionDecimalPlaces)
? addDecimalsFormatNumber(
candleVolume,
positionDecimalPlaces,
formatDecimals
)
: '-'}
</span>
: '-';
return (
<Tooltip
description={
<div>
<span className="flex flex-col">
{t(
'24 hour change is unavailable at this time. The volume change in the last 120 hours is %s',
[candleVolumeValue]
)}
</span>
</div>
}
>
<span ref={ref}>{t('Unknown')} </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}>
{candleVolume && isNumeric(positionDecimalPlaces)
? addDecimalsFormatNumber(
candleVolume,
positionDecimalPlaces,
formatDecimals
)
: '-'}
</span>
</Tooltip>
);
};

View File

@ -1,5 +1,5 @@
import { act } from 'react-dom/test-utils';
import { now, useYesterday } from './use-yesterday';
import { createAgo, now } from './use-yesterday';
import { renderHook } from '@testing-library/react';
describe('now', () => {
@ -25,33 +25,41 @@ describe('now', () => {
);
});
describe('useYesterday', () => {
beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date('1970-01-05T14:36:20.100Z'));
describe('createAgo', () => {
beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date('1970-01-30T14:36:20.100Z'));
});
afterAll(() => {
jest.useRealTimers();
});
it('returns yesterday timestamp rounded by 5 minutes', () => {
const { result, rerender } = renderHook(() => useYesterday());
expect(result.current).toEqual(
new Date('1970-01-04T14:35:00.000Z').getTime()
it.each([
['yesterday', 24 * 60 * 60 * 1000, '1970-01-29T14:35:00.000Z'],
['2 days ago', 2 * 24 * 60 * 60 * 1000, '1970-01-28T14:35:00.000Z'],
['5 days ago', 5 * 24 * 60 * 60 * 1000, '1970-01-25T14:35:00.000Z'],
['20 days ago', 20 * 24 * 60 * 60 * 1000, '1970-01-10T14:35:00.000Z'],
])('returns %s timestamp rounded by 5 minutes', (_, ago, expectedTime) => {
const { result, rerender } = renderHook(() =>
createAgo(ago)(5 * 60 * 1000)
);
expect(result.current).toEqual(new Date(expectedTime).getTime());
rerender();
rerender();
rerender();
expect(result.current).toEqual(
new Date('1970-01-04T14:35:00.000Z').getTime()
);
expect(result.current).toEqual(new Date(expectedTime).getTime());
});
it('updates yesterday timestamp after 5 minutes', () => {
const { result, rerender } = renderHook(() => useYesterday());
it.each([
['yesterday', 24 * 60 * 60 * 1000, '1970-01-29T14:40:00.000Z'],
['2 days ago', 2 * 24 * 60 * 60 * 1000, '1970-01-28T14:40:00.000Z'],
['5 days ago', 5 * 24 * 60 * 60 * 1000, '1970-01-25T14:40:00.000Z'],
['20 days ago', 20 * 24 * 60 * 60 * 1000, '1970-01-10T14:40:00.000Z'],
])('updates %s timestamp after 5 minutes', (_, ago, expectedTime) => {
const { result, rerender } = renderHook(() =>
createAgo(ago)(5 * 60 * 1000)
);
act(() => {
jest.advanceTimersByTime(5 * 60 * 1000);
rerender();
});
expect(result.current).toEqual(
new Date('1970-01-04T14:40:00.000Z').getTime()
);
expect(result.current).toEqual(new Date(expectedTime).getTime());
});
});

View File

@ -1,22 +1,30 @@
import { useEffect, useRef } from 'react';
const DEFAULT_ROUND_BY_MS = 5 * 60 * 1000;
const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;
const MINUTE = 60 * 1000;
const DAY = 24 * 60 * 60 * 1000;
export const now = (roundBy = 1) => {
return Math.floor((Math.round(Date.now() / 1000) * 1000) / roundBy) * roundBy;
};
export const createAgo =
(ago: number) =>
(roundBy = 5 * MINUTE) => {
const timestamp = useRef<number>(now(roundBy) - ago);
useEffect(() => {
const i = setInterval(() => {
timestamp.current = now(roundBy) - ago;
}, roundBy);
return () => clearInterval(i);
}, [roundBy]);
return timestamp.current;
};
/**
* Returns the yesterday's timestamp rounded by given number (in milliseconds; 5 minutes by default)
*/
export const useYesterday = (roundBy = DEFAULT_ROUND_BY_MS) => {
const yesterday = useRef<number>(now(roundBy) - TWENTY_FOUR_HOURS_MS);
useEffect(() => {
const i = setInterval(() => {
yesterday.current = now(roundBy) - TWENTY_FOUR_HOURS_MS;
}, roundBy);
return () => clearInterval(i);
}, [roundBy]);
return yesterday.current;
};
export const useYesterday = createAgo(DAY);
/**
* Returns the five days ago timestamp rounded by given number (in milliseconds; 5 minutes by default)
*/
export const useFiveDaysAgo = createAgo(5 * DAY);