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}
|
decimalPlaces={market?.decimalPlaces}
|
||||||
/>
|
/>
|
||||||
</HeaderStat>
|
</HeaderStat>
|
||||||
<HeaderStat
|
<HeaderStat heading={t('Volume (24h)')} testId="market-volume">
|
||||||
heading={t('Volume (24h)')}
|
|
||||||
testId="market-volume"
|
|
||||||
description={t(
|
|
||||||
'The total number of contracts traded in the last 24 hours.'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Last24hVolume
|
<Last24hVolume
|
||||||
marketId={market?.id}
|
marketId={market?.id}
|
||||||
positionDecimalPlaces={market?.positionDecimalPlaces}
|
positionDecimalPlaces={market?.positionDecimalPlaces}
|
||||||
|
@ -25,7 +25,7 @@ export const PriceChangeCell = memo(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={`${signedNumberCssClass(
|
className={`${signedNumberCssClass(
|
||||||
change
|
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} />
|
<Arrow value={change} />
|
||||||
<span data-testid="price-change-percentage">
|
<span data-testid="price-change-percentage">
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
import { isNumeric } from '@vegaprotocol/utils';
|
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 { useThrottledDataProvider } from '@vegaprotocol/data-provider';
|
||||||
import { PriceChangeCell } from '@vegaprotocol/datagrid';
|
import { PriceChangeCell } from '@vegaprotocol/datagrid';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import type { CandleClose } from '@vegaprotocol/types';
|
import type { CandleClose } from '@vegaprotocol/types';
|
||||||
import { marketCandlesProvider } from '../../market-candles-provider';
|
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 {
|
interface Props {
|
||||||
marketId?: string;
|
marketId?: string;
|
||||||
@ -24,30 +27,71 @@ export const Last24hPriceChange = ({
|
|||||||
inViewRoot,
|
inViewRoot,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [ref, inView] = useInView({ root: inViewRoot?.current });
|
const [ref, inView] = useInView({ root: inViewRoot?.current });
|
||||||
|
const fiveDaysAgo = useFiveDaysAgo();
|
||||||
const yesterday = useYesterday();
|
const yesterday = useYesterday();
|
||||||
const { data, error } = useThrottledDataProvider({
|
const { data, error } = useThrottledDataProvider({
|
||||||
dataProvider: marketCandlesProvider,
|
dataProvider: marketCandlesProvider,
|
||||||
variables: {
|
variables: {
|
||||||
marketId: marketId || '',
|
marketId: marketId || '',
|
||||||
interval: Schema.Interval.INTERVAL_I1H,
|
interval: Schema.Interval.INTERVAL_I1H,
|
||||||
since: new Date(yesterday).toISOString(),
|
since: new Date(fiveDaysAgo).toISOString(),
|
||||||
},
|
},
|
||||||
skip: !marketId || !inView,
|
skip: !marketId || !inView,
|
||||||
});
|
});
|
||||||
|
|
||||||
const candles =
|
const fiveDaysCandles = data?.filter((candle) => Boolean(candle));
|
||||||
data
|
|
||||||
|
const candles = fiveDaysCandles?.filter((candle) =>
|
||||||
|
isCandleLessThan24hOld(candle, yesterday)
|
||||||
|
);
|
||||||
|
const oneDayCandles =
|
||||||
|
candles
|
||||||
?.map((candle) => candle?.close)
|
?.map((candle) => candle?.close)
|
||||||
.filter((c): c is CandleClose => c !== null) || initialValue;
|
.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)) {
|
if (error || !isNumeric(decimalPlaces)) {
|
||||||
return <span ref={ref}>-</span>;
|
return <span ref={ref}>-</span>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<PriceChangeCell
|
<PriceChangeCell
|
||||||
candles={candles || []}
|
candles={oneDayCandles || []}
|
||||||
decimalPlaces={decimalPlaces}
|
decimalPlaces={decimalPlaces}
|
||||||
ref={ref}
|
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 { marketCandlesProvider } from '../../market-candles-provider';
|
||||||
import { calcCandleVolume } from '../../market-utils';
|
import { calcCandleVolume } from '../../market-utils';
|
||||||
import { addDecimalsFormatNumber, isNumeric } from '@vegaprotocol/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 { useThrottledDataProvider } from '@vegaprotocol/data-provider';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
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 {
|
interface Props {
|
||||||
marketId?: string;
|
marketId?: string;
|
||||||
@ -23,6 +26,7 @@ export const Last24hVolume = ({
|
|||||||
initialValue,
|
initialValue,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const yesterday = useYesterday();
|
const yesterday = useYesterday();
|
||||||
|
const fiveDaysAgo = useFiveDaysAgo();
|
||||||
const [ref, inView] = useInView({ root: inViewRoot?.current });
|
const [ref, inView] = useInView({ root: inViewRoot?.current });
|
||||||
|
|
||||||
const { data } = useThrottledDataProvider({
|
const { data } = useThrottledDataProvider({
|
||||||
@ -30,20 +34,66 @@ export const Last24hVolume = ({
|
|||||||
variables: {
|
variables: {
|
||||||
marketId: marketId || '',
|
marketId: marketId || '',
|
||||||
interval: Schema.Interval.INTERVAL_I1H,
|
interval: Schema.Interval.INTERVAL_I1H,
|
||||||
since: new Date(yesterday).toISOString(),
|
since: new Date(fiveDaysAgo).toISOString(),
|
||||||
},
|
},
|
||||||
skip: !(inView && marketId),
|
skip: !(inView && marketId),
|
||||||
});
|
});
|
||||||
const candleVolume = data ? calcCandleVolume(data) : initialValue;
|
|
||||||
return (
|
const fiveDaysCandles = data?.filter((candle) => Boolean(candle));
|
||||||
<span ref={ref}>
|
|
||||||
{candleVolume && isNumeric(positionDecimalPlaces)
|
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(
|
? addDecimalsFormatNumber(
|
||||||
candleVolume,
|
candleVolume,
|
||||||
positionDecimalPlaces,
|
positionDecimalPlaces,
|
||||||
formatDecimals
|
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 { act } from 'react-dom/test-utils';
|
||||||
import { now, useYesterday } from './use-yesterday';
|
import { createAgo, now } from './use-yesterday';
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
|
|
||||||
describe('now', () => {
|
describe('now', () => {
|
||||||
@ -25,33 +25,41 @@ describe('now', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useYesterday', () => {
|
describe('createAgo', () => {
|
||||||
beforeAll(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers().setSystemTime(new Date('1970-01-05T14:36:20.100Z'));
|
jest.useFakeTimers().setSystemTime(new Date('1970-01-30T14:36:20.100Z'));
|
||||||
});
|
});
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
it('returns yesterday timestamp rounded by 5 minutes', () => {
|
it.each([
|
||||||
const { result, rerender } = renderHook(() => useYesterday());
|
['yesterday', 24 * 60 * 60 * 1000, '1970-01-29T14:35:00.000Z'],
|
||||||
expect(result.current).toEqual(
|
['2 days ago', 2 * 24 * 60 * 60 * 1000, '1970-01-28T14:35:00.000Z'],
|
||||||
new Date('1970-01-04T14:35:00.000Z').getTime()
|
['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();
|
rerender();
|
||||||
rerender();
|
rerender();
|
||||||
expect(result.current).toEqual(
|
expect(result.current).toEqual(new Date(expectedTime).getTime());
|
||||||
new Date('1970-01-04T14:35:00.000Z').getTime()
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it('updates yesterday timestamp after 5 minutes', () => {
|
it.each([
|
||||||
const { result, rerender } = renderHook(() => useYesterday());
|
['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(() => {
|
act(() => {
|
||||||
jest.advanceTimersByTime(5 * 60 * 1000);
|
jest.advanceTimersByTime(5 * 60 * 1000);
|
||||||
rerender();
|
rerender();
|
||||||
});
|
});
|
||||||
expect(result.current).toEqual(
|
expect(result.current).toEqual(new Date(expectedTime).getTime());
|
||||||
new Date('1970-01-04T14:40:00.000Z').getTime()
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,22 +1,30 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
const DEFAULT_ROUND_BY_MS = 5 * 60 * 1000;
|
const MINUTE = 60 * 1000;
|
||||||
const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;
|
const DAY = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
export const now = (roundBy = 1) => {
|
export const now = (roundBy = 1) => {
|
||||||
return Math.floor((Math.round(Date.now() / 1000) * 1000) / roundBy) * roundBy;
|
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)
|
* Returns the yesterday's timestamp rounded by given number (in milliseconds; 5 minutes by default)
|
||||||
*/
|
*/
|
||||||
export const useYesterday = (roundBy = DEFAULT_ROUND_BY_MS) => {
|
export const useYesterday = createAgo(DAY);
|
||||||
const yesterday = useRef<number>(now(roundBy) - TWENTY_FOUR_HOURS_MS);
|
/**
|
||||||
useEffect(() => {
|
* Returns the five days ago timestamp rounded by given number (in milliseconds; 5 minutes by default)
|
||||||
const i = setInterval(() => {
|
*/
|
||||||
yesterday.current = now(roundBy) - TWENTY_FOUR_HOURS_MS;
|
export const useFiveDaysAgo = createAgo(5 * DAY);
|
||||||
}, roundBy);
|
|
||||||
return () => clearInterval(i);
|
|
||||||
}, [roundBy]);
|
|
||||||
return yesterday.current;
|
|
||||||
};
|
|
||||||
|
Loading…
Reference in New Issue
Block a user