feat(orderbook): improve data handling performance (#605)
* feat(orderbook): improve data handling performance * feat(orderbook): fix scrolling out of range
This commit is contained in:
parent
f36d3af286
commit
98d3c47808
@ -1,3 +1,4 @@
|
||||
import produce from 'immer';
|
||||
import { gql } from '@apollo/client';
|
||||
import { makeDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
@ -87,15 +88,16 @@ export const FILTERS_QUERY = gql`
|
||||
`;
|
||||
|
||||
const update = (
|
||||
draft: SimpleMarkets_markets[],
|
||||
data: SimpleMarkets_markets[],
|
||||
delta: SimpleMarketDataSub_marketData
|
||||
) => {
|
||||
) =>
|
||||
produce(data, (draft) => {
|
||||
const index = draft.findIndex((m) => m.id === delta.market.id);
|
||||
if (index !== -1) {
|
||||
draft[index].data = delta;
|
||||
}
|
||||
// @TODO - else push new market to draft
|
||||
};
|
||||
});
|
||||
|
||||
const getData = (responseData: SimpleMarkets) => responseData.markets;
|
||||
const getDelta = (
|
||||
|
@ -1,3 +1,4 @@
|
||||
import produce from 'immer';
|
||||
import { gql } from '@apollo/client';
|
||||
import type {
|
||||
Accounts,
|
||||
@ -52,9 +53,10 @@ export const getId = (
|
||||
) => `${data.type}-${data.asset.symbol}-${data.market?.id ?? 'null'}`;
|
||||
|
||||
const update = (
|
||||
draft: Accounts_party_accounts[],
|
||||
data: Accounts_party_accounts[],
|
||||
delta: AccountSubscribe_accounts
|
||||
) => {
|
||||
) =>
|
||||
produce(data, (draft) => {
|
||||
const id = getId(delta);
|
||||
const index = draft.findIndex((a) => getId(a) === id);
|
||||
if (index !== -1) {
|
||||
@ -62,7 +64,7 @@ const update = (
|
||||
} else {
|
||||
draft.push(delta);
|
||||
}
|
||||
};
|
||||
});
|
||||
const getData = (responseData: Accounts): Accounts_party_accounts[] | null =>
|
||||
responseData.party ? responseData.party.accounts : null;
|
||||
const getDelta = (
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { DepthChart } from 'pennant';
|
||||
import { produce } from 'immer';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
@ -92,28 +91,31 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
if (!dataRef.current) {
|
||||
return false;
|
||||
}
|
||||
dataRef.current = produce(dataRef.current, (draft) => {
|
||||
if (delta.buy) {
|
||||
draft.data.buy = updateLevels(
|
||||
draft.data.buy,
|
||||
delta.buy,
|
||||
decimalPlacesRef.current
|
||||
);
|
||||
}
|
||||
if (delta.sell) {
|
||||
draft.data.sell = updateLevels(
|
||||
draft.data.sell,
|
||||
delta.sell,
|
||||
decimalPlacesRef.current
|
||||
);
|
||||
}
|
||||
draft.midPrice = delta.market.data?.staticMidPrice
|
||||
dataRef.current = {
|
||||
...dataRef.current,
|
||||
midPrice: delta.market.data?.staticMidPrice
|
||||
? formatMidPrice(
|
||||
delta.market.data?.staticMidPrice,
|
||||
decimalPlacesRef.current
|
||||
)
|
||||
: undefined;
|
||||
});
|
||||
: undefined,
|
||||
data: {
|
||||
buy: delta.buy
|
||||
? updateLevels(
|
||||
dataRef.current.data.buy,
|
||||
delta.buy,
|
||||
decimalPlacesRef.current
|
||||
)
|
||||
: dataRef.current.data.buy,
|
||||
sell: delta.sell
|
||||
? updateLevels(
|
||||
dataRef.current.data.sell,
|
||||
delta.sell,
|
||||
decimalPlacesRef.current
|
||||
)
|
||||
: dataRef.current.data.sell,
|
||||
},
|
||||
};
|
||||
setDepthDataThrottledRef.current(dataRef.current);
|
||||
return true;
|
||||
},
|
||||
|
@ -86,27 +86,31 @@ const sequenceNumbers: Record<string, number> = {};
|
||||
const update: Update<
|
||||
MarketDepth_market,
|
||||
MarketDepthSubscription_marketDepthUpdate
|
||||
> = (draft, delta, reload) => {
|
||||
if (delta.market.id !== draft.id) {
|
||||
return;
|
||||
> = (data, delta, reload) => {
|
||||
if (delta.market.id !== data.id) {
|
||||
return data;
|
||||
}
|
||||
const sequenceNumber = Number(delta.sequenceNumber);
|
||||
if (sequenceNumber <= sequenceNumbers[delta.market.id]) {
|
||||
return;
|
||||
return data;
|
||||
}
|
||||
/*
|
||||
if (sequenceNumber - 1 !== sequenceNumbers[delta.market.id]) {
|
||||
sequenceNumbers[delta.market.id] = 0;
|
||||
reload();
|
||||
return;
|
||||
}
|
||||
*/
|
||||
sequenceNumbers[delta.market.id] = sequenceNumber;
|
||||
Object.assign(draft.data, delta.market.data);
|
||||
const updatedData = { ...data };
|
||||
data.data = delta.market.data;
|
||||
if (delta.buy) {
|
||||
draft.depth.buy = updateLevels(draft.depth.buy ?? [], delta.buy);
|
||||
updatedData.depth.buy = updateLevels(data.depth.buy ?? [], delta.buy);
|
||||
}
|
||||
if (delta.sell) {
|
||||
draft.depth.sell = updateLevels(draft.depth.sell ?? [], delta.sell);
|
||||
updatedData.depth.sell = updateLevels(data.depth.sell ?? [], delta.sell);
|
||||
}
|
||||
return updatedData;
|
||||
};
|
||||
|
||||
const getData = (responseData: MarketDepth) => {
|
||||
|
@ -55,10 +55,8 @@ describe('compactRows', () => {
|
||||
'1097': 3,
|
||||
'1098': 2,
|
||||
'1099': 1,
|
||||
'1100': 0,
|
||||
});
|
||||
expect(orderbookRows[orderbookRows.length - 1].bidByLevel).toEqual({
|
||||
'901': 0,
|
||||
'902': 1,
|
||||
'903': 2,
|
||||
'904': 3,
|
||||
@ -81,7 +79,7 @@ describe('compactRows', () => {
|
||||
});
|
||||
|
||||
describe('updateLevels', () => {
|
||||
const levels: MarketDepth_market_depth_sell[] = new Array(10)
|
||||
let levels: MarketDepth_market_depth_sell[] = new Array(10)
|
||||
.fill(null)
|
||||
.map((n, i) => ({
|
||||
__typename: 'PriceLevel',
|
||||
@ -96,9 +94,9 @@ describe('updateLevels', () => {
|
||||
volume: '0',
|
||||
numberOfOrders: '0',
|
||||
};
|
||||
updateLevels(levels, [removeFirstRow]);
|
||||
levels = updateLevels(levels, [removeFirstRow]);
|
||||
expect(levels[0].price).toEqual('20');
|
||||
updateLevels(levels, [removeFirstRow]);
|
||||
levels = updateLevels(levels, [removeFirstRow]);
|
||||
expect(levels[0].price).toEqual('20');
|
||||
expect(updateLevels([], [removeFirstRow])).toEqual([]);
|
||||
const addFirstRow: MarketDepthSubscription_marketDepthUpdate_sell = {
|
||||
@ -107,7 +105,7 @@ describe('updateLevels', () => {
|
||||
volume: '10',
|
||||
numberOfOrders: '10',
|
||||
};
|
||||
updateLevels(levels, [addFirstRow]);
|
||||
levels = updateLevels(levels, [addFirstRow]);
|
||||
expect(levels[0].price).toEqual('10');
|
||||
const addBeforeLastRow: MarketDepthSubscription_marketDepthUpdate_sell = {
|
||||
__typename: 'PriceLevel',
|
||||
@ -115,7 +113,7 @@ describe('updateLevels', () => {
|
||||
volume: '95',
|
||||
numberOfOrders: '95',
|
||||
};
|
||||
updateLevels(levels, [addBeforeLastRow]);
|
||||
levels = updateLevels(levels, [addBeforeLastRow]);
|
||||
expect(levels[levels.length - 2].price).toEqual('95');
|
||||
const addAtTheEnd: MarketDepthSubscription_marketDepthUpdate_sell = {
|
||||
__typename: 'PriceLevel',
|
||||
@ -123,7 +121,7 @@ describe('updateLevels', () => {
|
||||
volume: '115',
|
||||
numberOfOrders: '115',
|
||||
};
|
||||
updateLevels(levels, [addAtTheEnd]);
|
||||
levels = updateLevels(levels, [addAtTheEnd]);
|
||||
expect(levels[levels.length - 1].price).toEqual('115');
|
||||
const updateLastRow: MarketDepthSubscription_marketDepthUpdate_sell = {
|
||||
__typename: 'PriceLevel',
|
||||
@ -131,7 +129,7 @@ describe('updateLevels', () => {
|
||||
volume: '116',
|
||||
numberOfOrders: '115',
|
||||
};
|
||||
updateLevels(levels, [updateLastRow]);
|
||||
levels = updateLevels(levels, [updateLastRow]);
|
||||
expect(levels[levels.length - 1]).toEqual(updateLastRow);
|
||||
expect(updateLevels([], [updateLastRow])).toEqual([updateLastRow]);
|
||||
});
|
||||
|
@ -1,4 +1,3 @@
|
||||
import produce from 'immer';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { VolumeType } from '@vegaprotocol/react-helpers';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
@ -31,6 +30,8 @@ export interface OrderbookRowData {
|
||||
cumulativeVol: CumulativeVol;
|
||||
}
|
||||
|
||||
type PartialOrderbookRowData = Pick<OrderbookRowData, 'price' | 'ask' | 'bid'>;
|
||||
|
||||
export type OrderbookData = Partial<
|
||||
Omit<MarketDepth_market_data, '__typename' | 'market'>
|
||||
> & { rows: OrderbookRowData[] | null };
|
||||
@ -75,21 +76,31 @@ const updateRelativeData = (data: OrderbookRowData[]) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const createPartialRow = (
|
||||
price: string,
|
||||
volume = 0,
|
||||
dataType?: VolumeType
|
||||
): PartialOrderbookRowData => ({
|
||||
price,
|
||||
ask: dataType === VolumeType.ask ? volume : 0,
|
||||
bid: dataType === VolumeType.bid ? volume : 0,
|
||||
});
|
||||
|
||||
export const extendRow = (row: PartialOrderbookRowData): OrderbookRowData =>
|
||||
Object.assign(row, {
|
||||
cumulativeVol: {
|
||||
ask: 0,
|
||||
bid: 0,
|
||||
},
|
||||
askByLevel: row.ask ? { [row.price]: row.ask } : {},
|
||||
bidByLevel: row.bid ? { [row.price]: row.bid } : {},
|
||||
});
|
||||
|
||||
export const createRow = (
|
||||
price: string,
|
||||
volume = 0,
|
||||
dataType?: VolumeType
|
||||
): OrderbookRowData => ({
|
||||
price,
|
||||
ask: dataType === VolumeType.ask ? volume : 0,
|
||||
bid: dataType === VolumeType.bid ? volume : 0,
|
||||
cumulativeVol: {
|
||||
ask: dataType === VolumeType.ask ? volume : 0,
|
||||
bid: dataType === VolumeType.bid ? volume : 0,
|
||||
},
|
||||
askByLevel: dataType === VolumeType.ask ? { [price]: volume } : {},
|
||||
bidByLevel: dataType === VolumeType.bid ? { [price]: volume } : {},
|
||||
});
|
||||
): OrderbookRowData => extendRow(createPartialRow(price, volume, dataType));
|
||||
|
||||
const mapRawData =
|
||||
(dataType: VolumeType.ask | VolumeType.bid) =>
|
||||
@ -99,8 +110,8 @@ const mapRawData =
|
||||
| MarketDepthSubscription_marketDepthUpdate_sell
|
||||
| MarketDepth_market_depth_buy
|
||||
| MarketDepthSubscription_marketDepthUpdate_buy
|
||||
): OrderbookRowData =>
|
||||
createRow(data.price, Number(data.volume), dataType);
|
||||
): PartialOrderbookRowData =>
|
||||
createPartialRow(data.price, Number(data.volume), dataType);
|
||||
|
||||
/**
|
||||
* @summary merges sell amd buy data, orders by price desc, group by price level, counts cumulative and relative values
|
||||
@ -121,37 +132,38 @@ export const compactRows = (
|
||||
resolution: number
|
||||
) => {
|
||||
// map raw sell data to OrderbookData
|
||||
const askOrderbookData = [...(sell ?? [])].map<OrderbookRowData>(
|
||||
const askOrderbookData = [...(sell ?? [])].map<PartialOrderbookRowData>(
|
||||
mapRawData(VolumeType.ask)
|
||||
);
|
||||
// map raw buy data to OrderbookData
|
||||
const bidOrderbookData = [...(buy ?? [])].map<OrderbookRowData>(
|
||||
const bidOrderbookData = [...(buy ?? [])].map<PartialOrderbookRowData>(
|
||||
mapRawData(VolumeType.bid)
|
||||
);
|
||||
|
||||
// group by price level
|
||||
const groupedByLevel = groupBy<OrderbookRowData>(
|
||||
const groupedByLevel = groupBy<PartialOrderbookRowData>(
|
||||
[...askOrderbookData, ...bidOrderbookData],
|
||||
(row) => getPriceLevel(row.price, resolution)
|
||||
);
|
||||
|
||||
// create single OrderbookData from grouped OrderbookData[], sum volumes and atore volume by level
|
||||
const orderbookData = Object.keys(groupedByLevel).reduce<OrderbookRowData[]>(
|
||||
(rows, price) =>
|
||||
rows.concat(
|
||||
groupedByLevel[price].reduce<OrderbookRowData>(
|
||||
(a, c) => ({
|
||||
...a,
|
||||
ask: a.ask + c.ask,
|
||||
askByLevel: Object.assign(a.askByLevel, c.askByLevel),
|
||||
bid: (a.bid ?? 0) + (c.bid ?? 0),
|
||||
bidByLevel: Object.assign(a.bidByLevel, c.bidByLevel),
|
||||
}),
|
||||
createRow(price)
|
||||
)
|
||||
),
|
||||
[]
|
||||
const orderbookData: OrderbookRowData[] = [];
|
||||
Object.keys(groupedByLevel).forEach((price) => {
|
||||
const row = extendRow(
|
||||
groupedByLevel[price].pop() as PartialOrderbookRowData
|
||||
);
|
||||
row.price = price;
|
||||
let subRow: PartialOrderbookRowData | undefined;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while ((subRow = groupedByLevel[price].pop())) {
|
||||
row.ask += subRow.ask;
|
||||
row.bid += subRow.bid;
|
||||
if (subRow.ask) {
|
||||
row.askByLevel[subRow.price] = subRow.ask;
|
||||
}
|
||||
if (subRow.bid) {
|
||||
row.bidByLevel[subRow.price] = subRow.bid;
|
||||
}
|
||||
}
|
||||
orderbookData.push(row);
|
||||
});
|
||||
// order by price, it's safe to cast to number price diff should not exceed Number.MAX_SAFE_INTEGER
|
||||
orderbookData.sort((a, b) => Number(BigInt(b.price) - BigInt(a.price)));
|
||||
// count cumulative volumes
|
||||
@ -163,13 +175,11 @@ export const compactRows = (
|
||||
(i !== 0 ? orderbookData[i - 1].cumulativeVol.bid : 0);
|
||||
}
|
||||
for (let i = maxIndex; i >= 0; i--) {
|
||||
if (!orderbookData[i].cumulativeVol.ask) {
|
||||
orderbookData[i].cumulativeVol.ask =
|
||||
orderbookData[i].ask +
|
||||
(i !== maxIndex ? orderbookData[i + 1].cumulativeVol.ask : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
// count relative volumes
|
||||
updateRelativeData(orderbookData);
|
||||
return orderbookData;
|
||||
@ -186,13 +196,13 @@ export const compactRows = (
|
||||
*/
|
||||
const partiallyUpdateCompactedRows = (
|
||||
dataType: VolumeType,
|
||||
draft: OrderbookRowData[],
|
||||
data: OrderbookRowData[],
|
||||
delta:
|
||||
| MarketDepthSubscription_marketDepthUpdate_sell
|
||||
| MarketDepthSubscription_marketDepthUpdate_buy,
|
||||
resolution: number,
|
||||
modifiedIndex: number
|
||||
) => {
|
||||
): [number, OrderbookRowData[]] => {
|
||||
const { price } = delta;
|
||||
const volume = Number(delta.volume);
|
||||
const priceLevel = getPriceLevel(price, resolution);
|
||||
@ -201,28 +211,36 @@ const partiallyUpdateCompactedRows = (
|
||||
const oppositeVolKey = isAskDataType ? 'bid' : 'ask';
|
||||
const volByLevelKey = isAskDataType ? 'askByLevel' : 'bidByLevel';
|
||||
const resolveModifiedIndex = isAskDataType ? Math.max : Math.min;
|
||||
let index = draft.findIndex((data) => data.price === priceLevel);
|
||||
let index = data.findIndex((row) => row.price === priceLevel);
|
||||
if (index !== -1) {
|
||||
modifiedIndex = resolveModifiedIndex(modifiedIndex, index);
|
||||
draft[index][volKey] =
|
||||
draft[index][volKey] - (draft[index][volByLevelKey][price] || 0) + volume;
|
||||
draft[index][volByLevelKey][price] = volume;
|
||||
data[index] = {
|
||||
...data[index],
|
||||
[volKey]:
|
||||
data[index][volKey] - (data[index][volByLevelKey][price] || 0) + volume,
|
||||
[volByLevelKey]: {
|
||||
...data[index][volByLevelKey],
|
||||
[price]: volume,
|
||||
},
|
||||
};
|
||||
return [modifiedIndex, [...data]];
|
||||
} else {
|
||||
const newData: OrderbookRowData = createRow(priceLevel, volume, dataType);
|
||||
index = draft.findIndex((data) => BigInt(data.price) < BigInt(priceLevel));
|
||||
index = data.findIndex((row) => BigInt(row.price) < BigInt(priceLevel));
|
||||
if (index !== -1) {
|
||||
draft.splice(index, 0, newData);
|
||||
newData.cumulativeVol[oppositeVolKey] =
|
||||
draft[index + (isAskDataType ? -1 : 1)]?.cumulativeVol[
|
||||
oppositeVolKey
|
||||
] ?? 0;
|
||||
data[index + (isAskDataType ? 0 : 1)]?.cumulativeVol[oppositeVolKey] ??
|
||||
0;
|
||||
modifiedIndex = resolveModifiedIndex(modifiedIndex, index);
|
||||
return [
|
||||
modifiedIndex,
|
||||
[...data.slice(0, index), newData, ...data.slice(index)],
|
||||
];
|
||||
} else {
|
||||
draft.push(newData);
|
||||
modifiedIndex = draft.length - 1;
|
||||
modifiedIndex = data.length - 1;
|
||||
return [modifiedIndex, [...data, newData]];
|
||||
}
|
||||
}
|
||||
return modifiedIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -239,23 +257,23 @@ export const updateCompactedRows = (
|
||||
sell: MarketDepthSubscription_marketDepthUpdate_sell[] | null,
|
||||
buy: MarketDepthSubscription_marketDepthUpdate_buy[] | null,
|
||||
resolution: number
|
||||
) =>
|
||||
produce(rows, (draft) => {
|
||||
) => {
|
||||
let sellModifiedIndex = -1;
|
||||
let data = [...rows];
|
||||
sell?.forEach((delta) => {
|
||||
sellModifiedIndex = partiallyUpdateCompactedRows(
|
||||
[sellModifiedIndex, data] = partiallyUpdateCompactedRows(
|
||||
VolumeType.ask,
|
||||
draft,
|
||||
data,
|
||||
delta,
|
||||
resolution,
|
||||
sellModifiedIndex
|
||||
);
|
||||
});
|
||||
let buyModifiedIndex = draft.length;
|
||||
let buyModifiedIndex = data.length;
|
||||
buy?.forEach((delta) => {
|
||||
buyModifiedIndex = partiallyUpdateCompactedRows(
|
||||
[buyModifiedIndex, data] = partiallyUpdateCompactedRows(
|
||||
VolumeType.bid,
|
||||
draft,
|
||||
data,
|
||||
delta,
|
||||
resolution,
|
||||
buyModifiedIndex
|
||||
@ -264,34 +282,41 @@ export const updateCompactedRows = (
|
||||
|
||||
// update cummulative ask only below hihgest modified price level
|
||||
if (sellModifiedIndex !== -1) {
|
||||
for (let i = Math.min(sellModifiedIndex, draft.length - 2); i >= 0; i--) {
|
||||
draft[i].cumulativeVol.ask =
|
||||
draft[i + 1].cumulativeVol.ask + draft[i].ask;
|
||||
for (let i = Math.min(sellModifiedIndex, data.length - 2); i >= 0; i--) {
|
||||
data[i] = {
|
||||
...data[i],
|
||||
cumulativeVol: {
|
||||
...data[i].cumulativeVol,
|
||||
ask: data[i + 1].cumulativeVol.ask + data[i].ask,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
// update cummulative bid only above lowest modified price level
|
||||
if (buyModifiedIndex !== draft.length) {
|
||||
for (
|
||||
let i = Math.max(buyModifiedIndex, 1), l = draft.length;
|
||||
i < l;
|
||||
i++
|
||||
) {
|
||||
draft[i].cumulativeVol.bid =
|
||||
draft[i - 1].cumulativeVol.bid + draft[i].bid;
|
||||
if (buyModifiedIndex !== data.length) {
|
||||
for (let i = Math.max(buyModifiedIndex, 1), l = data.length; i < l; i++) {
|
||||
data[i] = {
|
||||
...data[i],
|
||||
cumulativeVol: {
|
||||
...data[i].cumulativeVol,
|
||||
bid: data[i - 1].cumulativeVol.bid + data[i].bid,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
let index = 0;
|
||||
// remove levels that do not have any volume
|
||||
while (index < draft.length) {
|
||||
if (!draft[index].ask && !draft[index].bid) {
|
||||
draft.splice(index, 1);
|
||||
while (index < data.length) {
|
||||
if (!data[index].ask && !data[index].bid) {
|
||||
data.splice(index, 1);
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
// count relative volumes
|
||||
updateRelativeData(draft);
|
||||
});
|
||||
updateRelativeData(data);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const mapMarketData = (
|
||||
data:
|
||||
@ -319,23 +344,24 @@ export const mapMarketData = (
|
||||
* @returns
|
||||
*/
|
||||
export const updateLevels = (
|
||||
levels: (MarketDepth_market_depth_buy | MarketDepth_market_depth_sell)[],
|
||||
draft: (MarketDepth_market_depth_buy | MarketDepth_market_depth_sell)[],
|
||||
updates: (
|
||||
| MarketDepthSubscription_marketDepthUpdate_buy
|
||||
| MarketDepthSubscription_marketDepthUpdate_sell
|
||||
)[]
|
||||
) => {
|
||||
const levels = [...draft];
|
||||
updates.forEach((update) => {
|
||||
let index = levels.findIndex((level) => level.price === update.price);
|
||||
if (index !== -1) {
|
||||
if (update.volume === '0') {
|
||||
levels.splice(index, 1);
|
||||
} else {
|
||||
Object.assign(levels[index], update);
|
||||
levels[index] = update;
|
||||
}
|
||||
} else if (update.volume !== '0') {
|
||||
index = levels.findIndex(
|
||||
(level) => Number(level.price) > Number(update.price)
|
||||
(level) => BigInt(level.price) > BigInt(update.price)
|
||||
);
|
||||
if (index !== -1) {
|
||||
levels.splice(index, 0, update);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import throttle from 'lodash/throttle';
|
||||
import produce from 'immer';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { Orderbook } from './orderbook';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
@ -25,27 +24,52 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
rows: null,
|
||||
});
|
||||
const dataRef = useRef<OrderbookData>({ rows: null });
|
||||
const setOrderbookDataThrottled = useRef(throttle(setOrderbookData, 1000));
|
||||
const deltaRef = useRef<MarketDepthSubscription_marketDepthUpdate>();
|
||||
const updateOrderbookData = useRef(
|
||||
throttle(() => {
|
||||
if (!deltaRef.current) {
|
||||
return;
|
||||
}
|
||||
dataRef.current = {
|
||||
...deltaRef.current.market.data,
|
||||
...mapMarketData(deltaRef.current.market.data, resolutionRef.current),
|
||||
rows: updateCompactedRows(
|
||||
dataRef.current.rows ?? [],
|
||||
deltaRef.current.sell,
|
||||
deltaRef.current.buy,
|
||||
resolutionRef.current
|
||||
),
|
||||
};
|
||||
deltaRef.current = undefined;
|
||||
setOrderbookData(dataRef.current);
|
||||
}, 1000)
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
(delta: MarketDepthSubscription_marketDepthUpdate) => {
|
||||
if (!dataRef.current.rows) {
|
||||
return false;
|
||||
}
|
||||
dataRef.current = produce(dataRef.current, (draft) => {
|
||||
Object.assign(draft, delta.market.data);
|
||||
draft.rows = updateCompactedRows(
|
||||
draft.rows ?? [],
|
||||
delta.sell,
|
||||
delta.buy,
|
||||
resolutionRef.current
|
||||
);
|
||||
Object.assign(
|
||||
draft,
|
||||
mapMarketData(delta.market.data, resolutionRef.current)
|
||||
);
|
||||
});
|
||||
setOrderbookDataThrottled.current(dataRef.current);
|
||||
if (deltaRef.current) {
|
||||
deltaRef.current.market = delta.market;
|
||||
if (delta.sell) {
|
||||
if (deltaRef.current.sell) {
|
||||
deltaRef.current.sell.push(...delta.sell);
|
||||
} else {
|
||||
deltaRef.current.sell = delta.sell;
|
||||
}
|
||||
}
|
||||
if (delta.buy) {
|
||||
if (deltaRef.current.buy) {
|
||||
deltaRef.current.buy.push(...delta.buy);
|
||||
} else {
|
||||
deltaRef.current.buy = delta.buy;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deltaRef.current = delta;
|
||||
}
|
||||
updateOrderbookData.current();
|
||||
return true;
|
||||
},
|
||||
// using resolutionRef.current to avoid using resolution as a dependency - it will cause data provider restart on resolution change
|
||||
|
@ -176,7 +176,10 @@ export const Orderbook = ({
|
||||
// adjust to current rows position
|
||||
scrollTop +=
|
||||
(scrollTopRef.current % rowHeight) - (scrollTop % rowHeight);
|
||||
const priceCenterScrollOffset = Math.max(0, Math.min(scrollTop));
|
||||
const priceCenterScrollOffset = Math.max(
|
||||
0,
|
||||
Math.min(scrollTop, numberOfRows * rowHeight - viewportHeight)
|
||||
);
|
||||
if (scrollTopRef.current !== priceCenterScrollOffset) {
|
||||
updateScrollOffset(priceCenterScrollOffset);
|
||||
scrollTopRef.current = priceCenterScrollOffset;
|
||||
@ -184,7 +187,13 @@ export const Orderbook = ({
|
||||
}
|
||||
}
|
||||
},
|
||||
[maxPriceLevel, resolution, viewportHeight, updateScrollOffset]
|
||||
[
|
||||
maxPriceLevel,
|
||||
resolution,
|
||||
viewportHeight,
|
||||
numberOfRows,
|
||||
updateScrollOffset,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -199,23 +208,36 @@ export const Orderbook = ({
|
||||
return;
|
||||
}
|
||||
priceInCenter.current = undefined;
|
||||
setLockOnMidPrice(true);
|
||||
scrollToPrice(
|
||||
getPriceLevel(
|
||||
let midPrice = getPriceLevel(
|
||||
BigInt(bestStaticOfferPrice) +
|
||||
(BigInt(bestStaticBidPrice) - BigInt(bestStaticOfferPrice)) /
|
||||
BigInt(2),
|
||||
(BigInt(bestStaticBidPrice) - BigInt(bestStaticOfferPrice)) / BigInt(2),
|
||||
resolution
|
||||
)
|
||||
);
|
||||
}, [bestStaticOfferPrice, bestStaticBidPrice, scrollToPrice, resolution]);
|
||||
if (BigInt(midPrice) > BigInt(maxPriceLevel)) {
|
||||
midPrice = maxPriceLevel;
|
||||
} else {
|
||||
const minPriceLevel =
|
||||
BigInt(maxPriceLevel) - BigInt(Math.floor(numberOfRows * resolution));
|
||||
if (BigInt(midPrice) < minPriceLevel) {
|
||||
midPrice = minPriceLevel.toString();
|
||||
}
|
||||
}
|
||||
scrollToPrice(midPrice);
|
||||
setLockOnMidPrice(true);
|
||||
}, [
|
||||
bestStaticOfferPrice,
|
||||
bestStaticBidPrice,
|
||||
scrollToPrice,
|
||||
resolution,
|
||||
maxPriceLevel,
|
||||
numberOfRows,
|
||||
]);
|
||||
|
||||
// adjust scroll position to keep selected price in center
|
||||
useLayoutEffect(() => {
|
||||
if (resolutionRef.current !== resolution) {
|
||||
priceInCenter.current = undefined;
|
||||
resolutionRef.current = resolution;
|
||||
setLockOnMidPrice(true);
|
||||
}
|
||||
if (priceInCenter.current) {
|
||||
scrollToPrice(priceInCenter.current);
|
||||
@ -238,7 +260,6 @@ export const Orderbook = ({
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const renderedRows = useMemo(() => {
|
||||
let offset = Math.max(0, Math.round(scrollOffset / rowHeight));
|
||||
const prependingBufferSize = Math.min(bufferSize, offset);
|
||||
offset -= prependingBufferSize;
|
||||
@ -247,12 +268,11 @@ export const Orderbook = ({
|
||||
prependingBufferSize + viewportSize + bufferSize,
|
||||
numberOfRows - offset
|
||||
);
|
||||
return {
|
||||
const renderedRows = {
|
||||
offset,
|
||||
limit,
|
||||
data: getRowsToRender(rows, resolution, offset, limit),
|
||||
};
|
||||
}, [rows, scrollOffset, resolution, viewportHeight, numberOfRows]);
|
||||
|
||||
const paddingTop = renderedRows.offset * rowHeight;
|
||||
const paddingBottom =
|
||||
|
@ -1,3 +1,4 @@
|
||||
import produce from 'immer';
|
||||
import { gql } from '@apollo/client';
|
||||
import type {
|
||||
Markets,
|
||||
@ -90,13 +91,14 @@ const MARKET_DATA_SUB = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
const update = (draft: Markets_markets[], delta: MarketDataSub_marketData) => {
|
||||
const update = (data: Markets_markets[], delta: MarketDataSub_marketData) =>
|
||||
produce(data, (draft) => {
|
||||
const index = draft.findIndex((m) => m.id === delta.market.id);
|
||||
if (index !== -1) {
|
||||
draft[index].data = delta;
|
||||
}
|
||||
// @TODO - else push new market to draft
|
||||
};
|
||||
});
|
||||
const getData = (responseData: Markets): Markets_markets[] | null =>
|
||||
responseData.markets;
|
||||
const getDelta = (subscriptionData: MarketDataSub): MarketDataSub_marketData =>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import produce from 'immer';
|
||||
import { gql } from '@apollo/client';
|
||||
import { makeDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import type { OrderFields } from './__generated__/OrderFields';
|
||||
@ -78,7 +79,8 @@ export const prepareIncomingOrders = (delta: OrderFields[]) => {
|
||||
return incoming;
|
||||
};
|
||||
|
||||
const update = (draft: OrderFields[], delta: OrderFields[]) => {
|
||||
const update = (data: OrderFields[], delta: OrderFields[]) =>
|
||||
produce(data, (draft) => {
|
||||
const incoming = prepareIncomingOrders(delta);
|
||||
|
||||
// Add or update incoming orders
|
||||
@ -90,7 +92,7 @@ const update = (draft: OrderFields[], delta: OrderFields[]) => {
|
||||
draft[index] = order;
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
const getData = (responseData: Orders): Orders_party_orders[] | null =>
|
||||
responseData?.party?.orders || null;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import produce from 'immer';
|
||||
import { gql } from '@apollo/client';
|
||||
import type {
|
||||
Positions,
|
||||
@ -75,16 +76,17 @@ export const POSITIONS_SUB = gql`
|
||||
`;
|
||||
|
||||
const update = (
|
||||
draft: Positions_party_positions[],
|
||||
data: Positions_party_positions[],
|
||||
delta: PositionSubscribe_positions
|
||||
) => {
|
||||
) =>
|
||||
produce(data, (draft) => {
|
||||
const index = draft.findIndex((m) => m.market.id === delta.market.id);
|
||||
if (index !== -1) {
|
||||
draft[index] = delta;
|
||||
} else {
|
||||
draft.push(delta);
|
||||
}
|
||||
};
|
||||
});
|
||||
const getData = (responseData: Positions): Positions_party_positions[] | null =>
|
||||
responseData.party ? responseData.party.positions : null;
|
||||
const getDelta = (
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { produce } from 'immer';
|
||||
import type { Draft } from 'immer';
|
||||
import type {
|
||||
ApolloClient,
|
||||
DocumentNode,
|
||||
@ -34,11 +32,7 @@ export interface Subscribe<Data, Delta> {
|
||||
type Query<Result> = DocumentNode | TypedDocumentNode<Result, any>;
|
||||
|
||||
export interface Update<Data, Delta> {
|
||||
(
|
||||
draft: Draft<Data>,
|
||||
delta: Delta,
|
||||
reload: (forceReset?: boolean) => void
|
||||
): void;
|
||||
(data: Data, delta: Delta, reload: (forceReset?: boolean) => void): Data;
|
||||
}
|
||||
|
||||
interface GetData<QueryData, Data> {
|
||||
@ -105,14 +99,12 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
||||
data = getData(res.data);
|
||||
// if there was some updates received from subscription during initial query loading apply them on just received data
|
||||
if (data && updateQueue && updateQueue.length > 0) {
|
||||
data = produce(data, (draft) => {
|
||||
while (updateQueue.length) {
|
||||
const delta = updateQueue.shift();
|
||||
if (delta) {
|
||||
update(draft, delta, reload);
|
||||
data = update(data, delta, reload);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// if error will occur data provider stops subscription
|
||||
@ -168,9 +160,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
||||
if (loading || !data) {
|
||||
updateQueue.push(delta);
|
||||
} else {
|
||||
const newData = produce(data, (draft) => {
|
||||
update(draft, delta, reload);
|
||||
});
|
||||
const newData = update(data, delta, reload);
|
||||
if (newData === data) {
|
||||
return;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import type { TradeFields } from './__generated__/TradeFields';
|
||||
import type { Trades } from './__generated__/Trades';
|
||||
import type { TradesSub } from './__generated__/TradesSub';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import produce from 'immer';
|
||||
|
||||
export const MAX_TRADES = 50;
|
||||
|
||||
@ -52,7 +53,8 @@ export const sortTrades = (trades: TradeFields[]) => {
|
||||
);
|
||||
};
|
||||
|
||||
const update = (draft: TradeFields[], delta: TradeFields[]) => {
|
||||
const update = (data: TradeFields[], delta: TradeFields[]) =>
|
||||
produce(data, (draft) => {
|
||||
const incoming = sortTrades(delta);
|
||||
|
||||
// Add new trades to the top
|
||||
@ -62,7 +64,7 @@ const update = (draft: TradeFields[], delta: TradeFields[]) => {
|
||||
if (draft.length > MAX_TRADES) {
|
||||
draft.splice(MAX_TRADES, draft.length - MAX_TRADES);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const getData = (responseData: Trades): TradeFields[] | null =>
|
||||
responseData.market ? responseData.market.trades : null;
|
||||
|
Loading…
Reference in New Issue
Block a user