chore(candles-chart): fill up missing candles (#3664)
This commit is contained in:
parent
91207d31ee
commit
dc7832ac81
280
libs/candles-chart/src/lib/data-source.spec.ts
Normal file
280
libs/candles-chart/src/lib/data-source.spec.ts
Normal file
@ -0,0 +1,280 @@
|
||||
import { VegaDataSource } from './data-source';
|
||||
import type { ApolloClient } from '@apollo/client';
|
||||
import { Interval } from 'pennant';
|
||||
import type {
|
||||
CandleFieldsFragment,
|
||||
CandlesQuery,
|
||||
} from './__generated__/Candles';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const returnDataMocks = (nodes: CandleFieldsFragment[]): CandlesQuery => {
|
||||
return {
|
||||
data: {
|
||||
market: {
|
||||
decimalPlaces: 1,
|
||||
positionDecimalPlaces: 1,
|
||||
candlesConnection: {
|
||||
edges: nodes.map((node) => ({ node })),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as CandlesQuery;
|
||||
};
|
||||
|
||||
const dataMocks: { [key in Schema.Interval]: Partial<CandleFieldsFragment>[] } =
|
||||
{
|
||||
[Schema.Interval.INTERVAL_I1M]: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
periodStart: '2023-05-10T12:00:00Z',
|
||||
lastUpdateInPeriod: '',
|
||||
close: '10',
|
||||
volume: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
periodStart: '2023-05-10T12:05:00Z',
|
||||
lastUpdateInPeriod: '',
|
||||
close: '5',
|
||||
volume: '2',
|
||||
},
|
||||
],
|
||||
[Schema.Interval.INTERVAL_I5M]: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
periodStart: '2023-05-10T12:00:00Z',
|
||||
lastUpdateInPeriod: '',
|
||||
close: '10',
|
||||
volume: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
periodStart: '2023-05-10T12:25:00Z',
|
||||
lastUpdateInPeriod: '',
|
||||
close: '5',
|
||||
volume: '2',
|
||||
},
|
||||
],
|
||||
[Schema.Interval.INTERVAL_I15M]: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
periodStart: '2023-05-10T12:00:00Z',
|
||||
lastUpdateInPeriod: '',
|
||||
close: '10',
|
||||
volume: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
periodStart: '2023-05-10T13:15:00Z',
|
||||
lastUpdateInPeriod: '',
|
||||
close: '5',
|
||||
volume: '2',
|
||||
},
|
||||
],
|
||||
[Schema.Interval.INTERVAL_I1H]: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
periodStart: '2023-05-10T12:00:00Z',
|
||||
lastUpdateInPeriod: '',
|
||||
close: '10',
|
||||
volume: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
periodStart: '2023-05-10T17:00:00Z',
|
||||
lastUpdateInPeriod: '',
|
||||
close: '5',
|
||||
volume: '2',
|
||||
},
|
||||
],
|
||||
[Schema.Interval.INTERVAL_I6H]: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
periodStart: '2023-05-10T12:00:00Z',
|
||||
lastUpdateInPeriod: '',
|
||||
close: '10',
|
||||
volume: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
periodStart: '2023-05-11T18:00:00Z',
|
||||
lastUpdateInPeriod: '',
|
||||
close: '5',
|
||||
volume: '2',
|
||||
},
|
||||
],
|
||||
[Schema.Interval.INTERVAL_I1D]: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
periodStart: '2023-05-10T00:00:00Z',
|
||||
lastUpdateInPeriod: '',
|
||||
close: '10',
|
||||
volume: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
periodStart: '2023-05-15T00:00:00Z',
|
||||
lastUpdateInPeriod: '',
|
||||
close: '5',
|
||||
volume: '2',
|
||||
},
|
||||
],
|
||||
[Schema.Interval.INTERVAL_BLOCK]: [],
|
||||
};
|
||||
|
||||
describe('VegaDataSource', () => {
|
||||
const marketId = 'marketId';
|
||||
const partyId = 'partyId';
|
||||
const client = {
|
||||
query: jest.fn().mockImplementation(({ variables: { interval } }) => {
|
||||
return returnDataMocks(
|
||||
dataMocks[interval as Schema.Interval] as CandleFieldsFragment[]
|
||||
);
|
||||
}),
|
||||
} as unknown as ApolloClient<object>;
|
||||
|
||||
it('should be properly initialized', () => {
|
||||
const dataSource = new VegaDataSource(client, marketId, partyId);
|
||||
expect(dataSource).toBeInstanceOf(VegaDataSource);
|
||||
expect(dataSource.onReady).toBeDefined();
|
||||
expect(dataSource.query).toBeDefined();
|
||||
expect(dataSource.subscribeData).toBeDefined();
|
||||
expect(dataSource.unsubscribeData).toBeDefined();
|
||||
expect(dataSource.decimalPlaces).toBeDefined();
|
||||
expect(dataSource.positionDecimalPlaces).toBeDefined();
|
||||
});
|
||||
|
||||
describe('query should return continuous data', () => {
|
||||
it('when interval is I1M', async () => {
|
||||
const dataSource = new VegaDataSource(client, marketId, partyId);
|
||||
const data = await dataSource.query(Interval.I1M, '');
|
||||
expect(data).toHaveLength(6);
|
||||
expect(data[1]).toStrictEqual({
|
||||
date: new Date('2023-05-10T12:01:00Z'),
|
||||
high: 1,
|
||||
low: 1,
|
||||
open: 1,
|
||||
close: 1,
|
||||
volume: 0,
|
||||
});
|
||||
expect(data[2]).toStrictEqual({
|
||||
date: new Date('2023-05-10T12:02:00Z'),
|
||||
high: 1,
|
||||
low: 1,
|
||||
open: 1,
|
||||
close: 1,
|
||||
volume: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('when interval is I5M', async () => {
|
||||
const dataSource = new VegaDataSource(client, marketId, partyId);
|
||||
const data = await dataSource.query(Interval.I5M, '');
|
||||
expect(data).toHaveLength(6);
|
||||
expect(data[1]).toStrictEqual({
|
||||
date: new Date('2023-05-10T12:05:00Z'),
|
||||
high: 1,
|
||||
low: 1,
|
||||
open: 1,
|
||||
close: 1,
|
||||
volume: 0,
|
||||
});
|
||||
expect(data[2]).toStrictEqual({
|
||||
date: new Date('2023-05-10T12:10:00Z'),
|
||||
high: 1,
|
||||
low: 1,
|
||||
open: 1,
|
||||
close: 1,
|
||||
volume: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('when interval is I15M', async () => {
|
||||
const dataSource = new VegaDataSource(client, marketId, partyId);
|
||||
const data = await dataSource.query(Interval.I15M, '');
|
||||
expect(data).toHaveLength(6);
|
||||
expect(data[1]).toStrictEqual({
|
||||
date: new Date('2023-05-10T12:15:00Z'),
|
||||
high: 1,
|
||||
low: 1,
|
||||
open: 1,
|
||||
close: 1,
|
||||
volume: 0,
|
||||
});
|
||||
expect(data[2]).toStrictEqual({
|
||||
date: new Date('2023-05-10T12:30:00Z'),
|
||||
high: 1,
|
||||
low: 1,
|
||||
open: 1,
|
||||
close: 1,
|
||||
volume: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('when interval is I1H', async () => {
|
||||
const dataSource = new VegaDataSource(client, marketId, partyId);
|
||||
const data = await dataSource.query(Interval.I1H, '');
|
||||
expect(data).toHaveLength(6);
|
||||
expect(data[1]).toStrictEqual({
|
||||
date: new Date('2023-05-10T13:00:00Z'),
|
||||
high: 1,
|
||||
low: 1,
|
||||
open: 1,
|
||||
close: 1,
|
||||
volume: 0,
|
||||
});
|
||||
expect(data[2]).toStrictEqual({
|
||||
date: new Date('2023-05-10T14:00:00Z'),
|
||||
high: 1,
|
||||
low: 1,
|
||||
open: 1,
|
||||
close: 1,
|
||||
volume: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('when interval is I6H', async () => {
|
||||
const dataSource = new VegaDataSource(client, marketId, partyId);
|
||||
const data = await dataSource.query(Interval.I6H, '');
|
||||
expect(data).toHaveLength(6);
|
||||
expect(data[1]).toStrictEqual({
|
||||
date: new Date('2023-05-10T18:00:00Z'),
|
||||
high: 1,
|
||||
low: 1,
|
||||
open: 1,
|
||||
close: 1,
|
||||
volume: 0,
|
||||
});
|
||||
expect(data[2]).toStrictEqual({
|
||||
date: new Date('2023-05-11T00:00:00Z'),
|
||||
high: 1,
|
||||
low: 1,
|
||||
open: 1,
|
||||
close: 1,
|
||||
volume: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('when interval is I1D', async () => {
|
||||
const dataSource = new VegaDataSource(client, marketId, partyId);
|
||||
const data = await dataSource.query(Interval.I1D, '');
|
||||
expect(data).toHaveLength(6);
|
||||
expect(data[1]).toStrictEqual({
|
||||
date: new Date('2023-05-11T00:00:00Z'),
|
||||
high: 1,
|
||||
low: 1,
|
||||
open: 1,
|
||||
close: 1,
|
||||
volume: 0,
|
||||
});
|
||||
expect(data[2]).toStrictEqual({
|
||||
date: new Date('2023-05-12T00:00:00Z'),
|
||||
high: 1,
|
||||
low: 1,
|
||||
open: 1,
|
||||
close: 1,
|
||||
volume: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,4 +1,11 @@
|
||||
import type { ApolloClient } from '@apollo/client';
|
||||
import type { Duration } from 'date-fns';
|
||||
import {
|
||||
add,
|
||||
differenceInDays,
|
||||
differenceInHours,
|
||||
differenceInMinutes,
|
||||
} from 'date-fns';
|
||||
import type { Candle, DataSource } from 'pennant';
|
||||
import { Interval as PennantInterval } from 'pennant';
|
||||
|
||||
@ -153,7 +160,6 @@ export class VegaDataSource implements DataSource {
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
|
||||
if (data?.market?.candlesConnection?.edges) {
|
||||
const decimalPlaces = data.market.decimalPlaces;
|
||||
const positionDecimalPlaces = data.market.positionDecimalPlaces;
|
||||
@ -163,8 +169,8 @@ export class VegaDataSource implements DataSource {
|
||||
.filter((node): node is CandleFieldsFragment => !!node)
|
||||
.map((node) =>
|
||||
parseCandle(node, decimalPlaces, positionDecimalPlaces)
|
||||
);
|
||||
|
||||
)
|
||||
.reduce(checkGranulationContinuity(interval), []);
|
||||
return candles;
|
||||
} else {
|
||||
return [];
|
||||
@ -213,6 +219,85 @@ export class VegaDataSource implements DataSource {
|
||||
}
|
||||
}
|
||||
|
||||
const getDuration = (
|
||||
interval: PennantInterval,
|
||||
multiplier: number
|
||||
): Duration => {
|
||||
switch (interval) {
|
||||
case 'I1D':
|
||||
return {
|
||||
days: 1 * multiplier,
|
||||
};
|
||||
case 'I1H':
|
||||
return {
|
||||
hours: 1 * multiplier,
|
||||
};
|
||||
case 'I1M':
|
||||
return {
|
||||
minutes: 1 * multiplier,
|
||||
};
|
||||
case 'I5M':
|
||||
return {
|
||||
minutes: 5 * multiplier,
|
||||
};
|
||||
case 'I6H':
|
||||
return {
|
||||
hours: 6 * multiplier,
|
||||
};
|
||||
case 'I15M':
|
||||
return {
|
||||
minutes: 15 * multiplier,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getDifference = (
|
||||
interval: PennantInterval,
|
||||
dateLeft: Date,
|
||||
dateRight: Date
|
||||
): number => {
|
||||
switch (interval) {
|
||||
case 'I1D':
|
||||
return differenceInDays(dateRight, dateLeft);
|
||||
case 'I6H':
|
||||
return differenceInHours(dateRight, dateLeft) / 6;
|
||||
case 'I1H':
|
||||
return differenceInHours(dateRight, dateLeft);
|
||||
case 'I15M':
|
||||
return differenceInMinutes(dateRight, dateLeft) / 15;
|
||||
case 'I5M':
|
||||
return differenceInMinutes(dateRight, dateLeft) / 5;
|
||||
case 'I1M':
|
||||
return differenceInMinutes(dateRight, dateLeft);
|
||||
}
|
||||
};
|
||||
|
||||
const checkGranulationContinuity =
|
||||
(interval: PennantInterval) =>
|
||||
(agg: Candle[], candle: Candle, i: number): Candle[] => {
|
||||
if (agg.length && i) {
|
||||
const previous = agg[agg.length - 1];
|
||||
const difference = getDifference(interval, previous.date, candle.date);
|
||||
if (difference > 1) {
|
||||
for (let j = 1; j < difference; j++) {
|
||||
const duration = getDuration(interval, j);
|
||||
const newStartDate = add(previous.date, duration);
|
||||
const newParsedCandle: Candle = {
|
||||
date: newStartDate,
|
||||
high: previous.close,
|
||||
low: previous.close,
|
||||
open: previous.close,
|
||||
close: previous.close,
|
||||
volume: 0,
|
||||
};
|
||||
agg.push(newParsedCandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
agg.push(candle);
|
||||
return agg;
|
||||
};
|
||||
|
||||
function parseCandle(
|
||||
candle: CandleFieldsFragment,
|
||||
decimalPlaces: number,
|
||||
|
Loading…
Reference in New Issue
Block a user