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:
parent
3ed615c36e
commit
1ae1fdff5e
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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());
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user