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:
Bartłomiej Głownia 2022-06-27 12:05:05 +02:00 committed by GitHub
parent f36d3af286
commit 98d3c47808
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 335 additions and 259 deletions

View File

@ -1,3 +1,4 @@
import produce from 'immer';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import { makeDataProvider } from '@vegaprotocol/react-helpers'; import { makeDataProvider } from '@vegaprotocol/react-helpers';
import type { import type {
@ -87,15 +88,16 @@ export const FILTERS_QUERY = gql`
`; `;
const update = ( const update = (
draft: SimpleMarkets_markets[], data: SimpleMarkets_markets[],
delta: SimpleMarketDataSub_marketData delta: SimpleMarketDataSub_marketData
) => { ) =>
produce(data, (draft) => {
const index = draft.findIndex((m) => m.id === delta.market.id); const index = draft.findIndex((m) => m.id === delta.market.id);
if (index !== -1) { if (index !== -1) {
draft[index].data = delta; draft[index].data = delta;
} }
// @TODO - else push new market to draft // @TODO - else push new market to draft
}; });
const getData = (responseData: SimpleMarkets) => responseData.markets; const getData = (responseData: SimpleMarkets) => responseData.markets;
const getDelta = ( const getDelta = (

View File

@ -1,3 +1,4 @@
import produce from 'immer';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import type { import type {
Accounts, Accounts,
@ -52,9 +53,10 @@ export const getId = (
) => `${data.type}-${data.asset.symbol}-${data.market?.id ?? 'null'}`; ) => `${data.type}-${data.asset.symbol}-${data.market?.id ?? 'null'}`;
const update = ( const update = (
draft: Accounts_party_accounts[], data: Accounts_party_accounts[],
delta: AccountSubscribe_accounts delta: AccountSubscribe_accounts
) => { ) =>
produce(data, (draft) => {
const id = getId(delta); const id = getId(delta);
const index = draft.findIndex((a) => getId(a) === id); const index = draft.findIndex((a) => getId(a) === id);
if (index !== -1) { if (index !== -1) {
@ -62,7 +64,7 @@ const update = (
} else { } else {
draft.push(delta); draft.push(delta);
} }
}; });
const getData = (responseData: Accounts): Accounts_party_accounts[] | null => const getData = (responseData: Accounts): Accounts_party_accounts[] | null =>
responseData.party ? responseData.party.accounts : null; responseData.party ? responseData.party.accounts : null;
const getDelta = ( const getDelta = (

View File

@ -1,5 +1,4 @@
import { DepthChart } from 'pennant'; import { DepthChart } from 'pennant';
import { produce } from 'immer';
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { import {
@ -92,28 +91,31 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
if (!dataRef.current) { if (!dataRef.current) {
return false; return false;
} }
dataRef.current = produce(dataRef.current, (draft) => { dataRef.current = {
if (delta.buy) { ...dataRef.current,
draft.data.buy = updateLevels( midPrice: delta.market.data?.staticMidPrice
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
? formatMidPrice( ? formatMidPrice(
delta.market.data?.staticMidPrice, delta.market.data?.staticMidPrice,
decimalPlacesRef.current 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); setDepthDataThrottledRef.current(dataRef.current);
return true; return true;
}, },

View File

@ -86,27 +86,31 @@ const sequenceNumbers: Record<string, number> = {};
const update: Update< const update: Update<
MarketDepth_market, MarketDepth_market,
MarketDepthSubscription_marketDepthUpdate MarketDepthSubscription_marketDepthUpdate
> = (draft, delta, reload) => { > = (data, delta, reload) => {
if (delta.market.id !== draft.id) { if (delta.market.id !== data.id) {
return; return data;
} }
const sequenceNumber = Number(delta.sequenceNumber); const sequenceNumber = Number(delta.sequenceNumber);
if (sequenceNumber <= sequenceNumbers[delta.market.id]) { if (sequenceNumber <= sequenceNumbers[delta.market.id]) {
return; return data;
} }
/*
if (sequenceNumber - 1 !== sequenceNumbers[delta.market.id]) { if (sequenceNumber - 1 !== sequenceNumbers[delta.market.id]) {
sequenceNumbers[delta.market.id] = 0; sequenceNumbers[delta.market.id] = 0;
reload(); reload();
return; return;
} }
*/
sequenceNumbers[delta.market.id] = sequenceNumber; sequenceNumbers[delta.market.id] = sequenceNumber;
Object.assign(draft.data, delta.market.data); const updatedData = { ...data };
data.data = delta.market.data;
if (delta.buy) { if (delta.buy) {
draft.depth.buy = updateLevels(draft.depth.buy ?? [], delta.buy); updatedData.depth.buy = updateLevels(data.depth.buy ?? [], delta.buy);
} }
if (delta.sell) { 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) => { const getData = (responseData: MarketDepth) => {

View File

@ -55,10 +55,8 @@ describe('compactRows', () => {
'1097': 3, '1097': 3,
'1098': 2, '1098': 2,
'1099': 1, '1099': 1,
'1100': 0,
}); });
expect(orderbookRows[orderbookRows.length - 1].bidByLevel).toEqual({ expect(orderbookRows[orderbookRows.length - 1].bidByLevel).toEqual({
'901': 0,
'902': 1, '902': 1,
'903': 2, '903': 2,
'904': 3, '904': 3,
@ -81,7 +79,7 @@ describe('compactRows', () => {
}); });
describe('updateLevels', () => { describe('updateLevels', () => {
const levels: MarketDepth_market_depth_sell[] = new Array(10) let levels: MarketDepth_market_depth_sell[] = new Array(10)
.fill(null) .fill(null)
.map((n, i) => ({ .map((n, i) => ({
__typename: 'PriceLevel', __typename: 'PriceLevel',
@ -96,9 +94,9 @@ describe('updateLevels', () => {
volume: '0', volume: '0',
numberOfOrders: '0', numberOfOrders: '0',
}; };
updateLevels(levels, [removeFirstRow]); levels = updateLevels(levels, [removeFirstRow]);
expect(levels[0].price).toEqual('20'); expect(levels[0].price).toEqual('20');
updateLevels(levels, [removeFirstRow]); levels = updateLevels(levels, [removeFirstRow]);
expect(levels[0].price).toEqual('20'); expect(levels[0].price).toEqual('20');
expect(updateLevels([], [removeFirstRow])).toEqual([]); expect(updateLevels([], [removeFirstRow])).toEqual([]);
const addFirstRow: MarketDepthSubscription_marketDepthUpdate_sell = { const addFirstRow: MarketDepthSubscription_marketDepthUpdate_sell = {
@ -107,7 +105,7 @@ describe('updateLevels', () => {
volume: '10', volume: '10',
numberOfOrders: '10', numberOfOrders: '10',
}; };
updateLevels(levels, [addFirstRow]); levels = updateLevels(levels, [addFirstRow]);
expect(levels[0].price).toEqual('10'); expect(levels[0].price).toEqual('10');
const addBeforeLastRow: MarketDepthSubscription_marketDepthUpdate_sell = { const addBeforeLastRow: MarketDepthSubscription_marketDepthUpdate_sell = {
__typename: 'PriceLevel', __typename: 'PriceLevel',
@ -115,7 +113,7 @@ describe('updateLevels', () => {
volume: '95', volume: '95',
numberOfOrders: '95', numberOfOrders: '95',
}; };
updateLevels(levels, [addBeforeLastRow]); levels = updateLevels(levels, [addBeforeLastRow]);
expect(levels[levels.length - 2].price).toEqual('95'); expect(levels[levels.length - 2].price).toEqual('95');
const addAtTheEnd: MarketDepthSubscription_marketDepthUpdate_sell = { const addAtTheEnd: MarketDepthSubscription_marketDepthUpdate_sell = {
__typename: 'PriceLevel', __typename: 'PriceLevel',
@ -123,7 +121,7 @@ describe('updateLevels', () => {
volume: '115', volume: '115',
numberOfOrders: '115', numberOfOrders: '115',
}; };
updateLevels(levels, [addAtTheEnd]); levels = updateLevels(levels, [addAtTheEnd]);
expect(levels[levels.length - 1].price).toEqual('115'); expect(levels[levels.length - 1].price).toEqual('115');
const updateLastRow: MarketDepthSubscription_marketDepthUpdate_sell = { const updateLastRow: MarketDepthSubscription_marketDepthUpdate_sell = {
__typename: 'PriceLevel', __typename: 'PriceLevel',
@ -131,7 +129,7 @@ describe('updateLevels', () => {
volume: '116', volume: '116',
numberOfOrders: '115', numberOfOrders: '115',
}; };
updateLevels(levels, [updateLastRow]); levels = updateLevels(levels, [updateLastRow]);
expect(levels[levels.length - 1]).toEqual(updateLastRow); expect(levels[levels.length - 1]).toEqual(updateLastRow);
expect(updateLevels([], [updateLastRow])).toEqual([updateLastRow]); expect(updateLevels([], [updateLastRow])).toEqual([updateLastRow]);
}); });

View File

@ -1,4 +1,3 @@
import produce from 'immer';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import { VolumeType } from '@vegaprotocol/react-helpers'; import { VolumeType } from '@vegaprotocol/react-helpers';
import { MarketTradingMode } from '@vegaprotocol/types'; import { MarketTradingMode } from '@vegaprotocol/types';
@ -31,6 +30,8 @@ export interface OrderbookRowData {
cumulativeVol: CumulativeVol; cumulativeVol: CumulativeVol;
} }
type PartialOrderbookRowData = Pick<OrderbookRowData, 'price' | 'ask' | 'bid'>;
export type OrderbookData = Partial< export type OrderbookData = Partial<
Omit<MarketDepth_market_data, '__typename' | 'market'> Omit<MarketDepth_market_data, '__typename' | 'market'>
> & { rows: OrderbookRowData[] | null }; > & { 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 = ( export const createRow = (
price: string, price: string,
volume = 0, volume = 0,
dataType?: VolumeType dataType?: VolumeType
): OrderbookRowData => ({ ): OrderbookRowData => extendRow(createPartialRow(price, volume, dataType));
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 } : {},
});
const mapRawData = const mapRawData =
(dataType: VolumeType.ask | VolumeType.bid) => (dataType: VolumeType.ask | VolumeType.bid) =>
@ -99,8 +110,8 @@ const mapRawData =
| MarketDepthSubscription_marketDepthUpdate_sell | MarketDepthSubscription_marketDepthUpdate_sell
| MarketDepth_market_depth_buy | MarketDepth_market_depth_buy
| MarketDepthSubscription_marketDepthUpdate_buy | MarketDepthSubscription_marketDepthUpdate_buy
): OrderbookRowData => ): PartialOrderbookRowData =>
createRow(data.price, Number(data.volume), dataType); 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 * @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 resolution: number
) => { ) => {
// map raw sell data to OrderbookData // map raw sell data to OrderbookData
const askOrderbookData = [...(sell ?? [])].map<OrderbookRowData>( const askOrderbookData = [...(sell ?? [])].map<PartialOrderbookRowData>(
mapRawData(VolumeType.ask) mapRawData(VolumeType.ask)
); );
// map raw buy data to OrderbookData // map raw buy data to OrderbookData
const bidOrderbookData = [...(buy ?? [])].map<OrderbookRowData>( const bidOrderbookData = [...(buy ?? [])].map<PartialOrderbookRowData>(
mapRawData(VolumeType.bid) mapRawData(VolumeType.bid)
); );
// group by price level // group by price level
const groupedByLevel = groupBy<OrderbookRowData>( const groupedByLevel = groupBy<PartialOrderbookRowData>(
[...askOrderbookData, ...bidOrderbookData], [...askOrderbookData, ...bidOrderbookData],
(row) => getPriceLevel(row.price, resolution) (row) => getPriceLevel(row.price, resolution)
); );
const orderbookData: OrderbookRowData[] = [];
// create single OrderbookData from grouped OrderbookData[], sum volumes and atore volume by level Object.keys(groupedByLevel).forEach((price) => {
const orderbookData = Object.keys(groupedByLevel).reduce<OrderbookRowData[]>( const row = extendRow(
(rows, price) => groupedByLevel[price].pop() as PartialOrderbookRowData
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)
)
),
[]
); );
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 // 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))); orderbookData.sort((a, b) => Number(BigInt(b.price) - BigInt(a.price)));
// count cumulative volumes // count cumulative volumes
@ -163,13 +175,11 @@ export const compactRows = (
(i !== 0 ? orderbookData[i - 1].cumulativeVol.bid : 0); (i !== 0 ? orderbookData[i - 1].cumulativeVol.bid : 0);
} }
for (let i = maxIndex; i >= 0; i--) { for (let i = maxIndex; i >= 0; i--) {
if (!orderbookData[i].cumulativeVol.ask) {
orderbookData[i].cumulativeVol.ask = orderbookData[i].cumulativeVol.ask =
orderbookData[i].ask + orderbookData[i].ask +
(i !== maxIndex ? orderbookData[i + 1].cumulativeVol.ask : 0); (i !== maxIndex ? orderbookData[i + 1].cumulativeVol.ask : 0);
} }
} }
}
// count relative volumes // count relative volumes
updateRelativeData(orderbookData); updateRelativeData(orderbookData);
return orderbookData; return orderbookData;
@ -186,13 +196,13 @@ export const compactRows = (
*/ */
const partiallyUpdateCompactedRows = ( const partiallyUpdateCompactedRows = (
dataType: VolumeType, dataType: VolumeType,
draft: OrderbookRowData[], data: OrderbookRowData[],
delta: delta:
| MarketDepthSubscription_marketDepthUpdate_sell | MarketDepthSubscription_marketDepthUpdate_sell
| MarketDepthSubscription_marketDepthUpdate_buy, | MarketDepthSubscription_marketDepthUpdate_buy,
resolution: number, resolution: number,
modifiedIndex: number modifiedIndex: number
) => { ): [number, OrderbookRowData[]] => {
const { price } = delta; const { price } = delta;
const volume = Number(delta.volume); const volume = Number(delta.volume);
const priceLevel = getPriceLevel(price, resolution); const priceLevel = getPriceLevel(price, resolution);
@ -201,28 +211,36 @@ const partiallyUpdateCompactedRows = (
const oppositeVolKey = isAskDataType ? 'bid' : 'ask'; const oppositeVolKey = isAskDataType ? 'bid' : 'ask';
const volByLevelKey = isAskDataType ? 'askByLevel' : 'bidByLevel'; const volByLevelKey = isAskDataType ? 'askByLevel' : 'bidByLevel';
const resolveModifiedIndex = isAskDataType ? Math.max : Math.min; 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) { if (index !== -1) {
modifiedIndex = resolveModifiedIndex(modifiedIndex, index); modifiedIndex = resolveModifiedIndex(modifiedIndex, index);
draft[index][volKey] = data[index] = {
draft[index][volKey] - (draft[index][volByLevelKey][price] || 0) + volume; ...data[index],
draft[index][volByLevelKey][price] = volume; [volKey]:
data[index][volKey] - (data[index][volByLevelKey][price] || 0) + volume,
[volByLevelKey]: {
...data[index][volByLevelKey],
[price]: volume,
},
};
return [modifiedIndex, [...data]];
} else { } else {
const newData: OrderbookRowData = createRow(priceLevel, volume, dataType); 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) { if (index !== -1) {
draft.splice(index, 0, newData);
newData.cumulativeVol[oppositeVolKey] = newData.cumulativeVol[oppositeVolKey] =
draft[index + (isAskDataType ? -1 : 1)]?.cumulativeVol[ data[index + (isAskDataType ? 0 : 1)]?.cumulativeVol[oppositeVolKey] ??
oppositeVolKey 0;
] ?? 0;
modifiedIndex = resolveModifiedIndex(modifiedIndex, index); modifiedIndex = resolveModifiedIndex(modifiedIndex, index);
return [
modifiedIndex,
[...data.slice(0, index), newData, ...data.slice(index)],
];
} else { } else {
draft.push(newData); modifiedIndex = data.length - 1;
modifiedIndex = draft.length - 1; return [modifiedIndex, [...data, newData]];
} }
} }
return modifiedIndex;
}; };
/** /**
@ -239,23 +257,23 @@ export const updateCompactedRows = (
sell: MarketDepthSubscription_marketDepthUpdate_sell[] | null, sell: MarketDepthSubscription_marketDepthUpdate_sell[] | null,
buy: MarketDepthSubscription_marketDepthUpdate_buy[] | null, buy: MarketDepthSubscription_marketDepthUpdate_buy[] | null,
resolution: number resolution: number
) => ) => {
produce(rows, (draft) => {
let sellModifiedIndex = -1; let sellModifiedIndex = -1;
let data = [...rows];
sell?.forEach((delta) => { sell?.forEach((delta) => {
sellModifiedIndex = partiallyUpdateCompactedRows( [sellModifiedIndex, data] = partiallyUpdateCompactedRows(
VolumeType.ask, VolumeType.ask,
draft, data,
delta, delta,
resolution, resolution,
sellModifiedIndex sellModifiedIndex
); );
}); });
let buyModifiedIndex = draft.length; let buyModifiedIndex = data.length;
buy?.forEach((delta) => { buy?.forEach((delta) => {
buyModifiedIndex = partiallyUpdateCompactedRows( [buyModifiedIndex, data] = partiallyUpdateCompactedRows(
VolumeType.bid, VolumeType.bid,
draft, data,
delta, delta,
resolution, resolution,
buyModifiedIndex buyModifiedIndex
@ -264,34 +282,41 @@ export const updateCompactedRows = (
// update cummulative ask only below hihgest modified price level // update cummulative ask only below hihgest modified price level
if (sellModifiedIndex !== -1) { if (sellModifiedIndex !== -1) {
for (let i = Math.min(sellModifiedIndex, draft.length - 2); i >= 0; i--) { for (let i = Math.min(sellModifiedIndex, data.length - 2); i >= 0; i--) {
draft[i].cumulativeVol.ask = data[i] = {
draft[i + 1].cumulativeVol.ask + draft[i].ask; ...data[i],
cumulativeVol: {
...data[i].cumulativeVol,
ask: data[i + 1].cumulativeVol.ask + data[i].ask,
},
};
} }
} }
// update cummulative bid only above lowest modified price level // update cummulative bid only above lowest modified price level
if (buyModifiedIndex !== draft.length) { if (buyModifiedIndex !== data.length) {
for ( for (let i = Math.max(buyModifiedIndex, 1), l = data.length; i < l; i++) {
let i = Math.max(buyModifiedIndex, 1), l = draft.length; data[i] = {
i < l; ...data[i],
i++ cumulativeVol: {
) { ...data[i].cumulativeVol,
draft[i].cumulativeVol.bid = bid: data[i - 1].cumulativeVol.bid + data[i].bid,
draft[i - 1].cumulativeVol.bid + draft[i].bid; },
};
} }
} }
let index = 0; let index = 0;
// remove levels that do not have any volume // remove levels that do not have any volume
while (index < draft.length) { while (index < data.length) {
if (!draft[index].ask && !draft[index].bid) { if (!data[index].ask && !data[index].bid) {
draft.splice(index, 1); data.splice(index, 1);
} else { } else {
index += 1; index += 1;
} }
} }
// count relative volumes // count relative volumes
updateRelativeData(draft); updateRelativeData(data);
}); return data;
};
export const mapMarketData = ( export const mapMarketData = (
data: data:
@ -319,23 +344,24 @@ export const mapMarketData = (
* @returns * @returns
*/ */
export const updateLevels = ( export const updateLevels = (
levels: (MarketDepth_market_depth_buy | MarketDepth_market_depth_sell)[], draft: (MarketDepth_market_depth_buy | MarketDepth_market_depth_sell)[],
updates: ( updates: (
| MarketDepthSubscription_marketDepthUpdate_buy | MarketDepthSubscription_marketDepthUpdate_buy
| MarketDepthSubscription_marketDepthUpdate_sell | MarketDepthSubscription_marketDepthUpdate_sell
)[] )[]
) => { ) => {
const levels = [...draft];
updates.forEach((update) => { updates.forEach((update) => {
let index = levels.findIndex((level) => level.price === update.price); let index = levels.findIndex((level) => level.price === update.price);
if (index !== -1) { if (index !== -1) {
if (update.volume === '0') { if (update.volume === '0') {
levels.splice(index, 1); levels.splice(index, 1);
} else { } else {
Object.assign(levels[index], update); levels[index] = update;
} }
} else if (update.volume !== '0') { } else if (update.volume !== '0') {
index = levels.findIndex( index = levels.findIndex(
(level) => Number(level.price) > Number(update.price) (level) => BigInt(level.price) > BigInt(update.price)
); );
if (index !== -1) { if (index !== -1) {
levels.splice(index, 0, update); levels.splice(index, 0, update);

View File

@ -1,5 +1,4 @@
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import produce from 'immer';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { Orderbook } from './orderbook'; import { Orderbook } from './orderbook';
import { useDataProvider } from '@vegaprotocol/react-helpers'; import { useDataProvider } from '@vegaprotocol/react-helpers';
@ -25,27 +24,52 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
rows: null, rows: null,
}); });
const dataRef = useRef<OrderbookData>({ 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( const update = useCallback(
(delta: MarketDepthSubscription_marketDepthUpdate) => { (delta: MarketDepthSubscription_marketDepthUpdate) => {
if (!dataRef.current.rows) { if (!dataRef.current.rows) {
return false; return false;
} }
dataRef.current = produce(dataRef.current, (draft) => { if (deltaRef.current) {
Object.assign(draft, delta.market.data); deltaRef.current.market = delta.market;
draft.rows = updateCompactedRows( if (delta.sell) {
draft.rows ?? [], if (deltaRef.current.sell) {
delta.sell, deltaRef.current.sell.push(...delta.sell);
delta.buy, } else {
resolutionRef.current deltaRef.current.sell = delta.sell;
); }
Object.assign( }
draft, if (delta.buy) {
mapMarketData(delta.market.data, resolutionRef.current) if (deltaRef.current.buy) {
); deltaRef.current.buy.push(...delta.buy);
}); } else {
setOrderbookDataThrottled.current(dataRef.current); deltaRef.current.buy = delta.buy;
}
}
} else {
deltaRef.current = delta;
}
updateOrderbookData.current();
return true; return true;
}, },
// using resolutionRef.current to avoid using resolution as a dependency - it will cause data provider restart on resolution change // using resolutionRef.current to avoid using resolution as a dependency - it will cause data provider restart on resolution change

View File

@ -176,7 +176,10 @@ export const Orderbook = ({
// adjust to current rows position // adjust to current rows position
scrollTop += scrollTop +=
(scrollTopRef.current % rowHeight) - (scrollTop % rowHeight); (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) { if (scrollTopRef.current !== priceCenterScrollOffset) {
updateScrollOffset(priceCenterScrollOffset); updateScrollOffset(priceCenterScrollOffset);
scrollTopRef.current = priceCenterScrollOffset; scrollTopRef.current = priceCenterScrollOffset;
@ -184,7 +187,13 @@ export const Orderbook = ({
} }
} }
}, },
[maxPriceLevel, resolution, viewportHeight, updateScrollOffset] [
maxPriceLevel,
resolution,
viewportHeight,
numberOfRows,
updateScrollOffset,
]
); );
useEffect(() => { useEffect(() => {
@ -199,23 +208,36 @@ export const Orderbook = ({
return; return;
} }
priceInCenter.current = undefined; priceInCenter.current = undefined;
setLockOnMidPrice(true); let midPrice = getPriceLevel(
scrollToPrice(
getPriceLevel(
BigInt(bestStaticOfferPrice) + BigInt(bestStaticOfferPrice) +
(BigInt(bestStaticBidPrice) - BigInt(bestStaticOfferPrice)) / (BigInt(bestStaticBidPrice) - BigInt(bestStaticOfferPrice)) / BigInt(2),
BigInt(2),
resolution 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 // adjust scroll position to keep selected price in center
useLayoutEffect(() => { useLayoutEffect(() => {
if (resolutionRef.current !== resolution) { if (resolutionRef.current !== resolution) {
priceInCenter.current = undefined; priceInCenter.current = undefined;
resolutionRef.current = resolution; resolutionRef.current = resolution;
setLockOnMidPrice(true);
} }
if (priceInCenter.current) { if (priceInCenter.current) {
scrollToPrice(priceInCenter.current); scrollToPrice(priceInCenter.current);
@ -238,7 +260,6 @@ export const Orderbook = ({
return () => window.removeEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize);
}, []); }, []);
const renderedRows = useMemo(() => {
let offset = Math.max(0, Math.round(scrollOffset / rowHeight)); let offset = Math.max(0, Math.round(scrollOffset / rowHeight));
const prependingBufferSize = Math.min(bufferSize, offset); const prependingBufferSize = Math.min(bufferSize, offset);
offset -= prependingBufferSize; offset -= prependingBufferSize;
@ -247,12 +268,11 @@ export const Orderbook = ({
prependingBufferSize + viewportSize + bufferSize, prependingBufferSize + viewportSize + bufferSize,
numberOfRows - offset numberOfRows - offset
); );
return { const renderedRows = {
offset, offset,
limit, limit,
data: getRowsToRender(rows, resolution, offset, limit), data: getRowsToRender(rows, resolution, offset, limit),
}; };
}, [rows, scrollOffset, resolution, viewportHeight, numberOfRows]);
const paddingTop = renderedRows.offset * rowHeight; const paddingTop = renderedRows.offset * rowHeight;
const paddingBottom = const paddingBottom =

View File

@ -1,3 +1,4 @@
import produce from 'immer';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import type { import type {
Markets, 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); const index = draft.findIndex((m) => m.id === delta.market.id);
if (index !== -1) { if (index !== -1) {
draft[index].data = delta; draft[index].data = delta;
} }
// @TODO - else push new market to draft // @TODO - else push new market to draft
}; });
const getData = (responseData: Markets): Markets_markets[] | null => const getData = (responseData: Markets): Markets_markets[] | null =>
responseData.markets; responseData.markets;
const getDelta = (subscriptionData: MarketDataSub): MarketDataSub_marketData => const getDelta = (subscriptionData: MarketDataSub): MarketDataSub_marketData =>

View File

@ -1,3 +1,4 @@
import produce from 'immer';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import { makeDataProvider } from '@vegaprotocol/react-helpers'; import { makeDataProvider } from '@vegaprotocol/react-helpers';
import type { OrderFields } from './__generated__/OrderFields'; import type { OrderFields } from './__generated__/OrderFields';
@ -78,7 +79,8 @@ export const prepareIncomingOrders = (delta: OrderFields[]) => {
return incoming; return incoming;
}; };
const update = (draft: OrderFields[], delta: OrderFields[]) => { const update = (data: OrderFields[], delta: OrderFields[]) =>
produce(data, (draft) => {
const incoming = prepareIncomingOrders(delta); const incoming = prepareIncomingOrders(delta);
// Add or update incoming orders // Add or update incoming orders
@ -90,7 +92,7 @@ const update = (draft: OrderFields[], delta: OrderFields[]) => {
draft[index] = order; draft[index] = order;
} }
}); });
}; });
const getData = (responseData: Orders): Orders_party_orders[] | null => const getData = (responseData: Orders): Orders_party_orders[] | null =>
responseData?.party?.orders || null; responseData?.party?.orders || null;

View File

@ -1,3 +1,4 @@
import produce from 'immer';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import type { import type {
Positions, Positions,
@ -75,16 +76,17 @@ export const POSITIONS_SUB = gql`
`; `;
const update = ( const update = (
draft: Positions_party_positions[], data: Positions_party_positions[],
delta: PositionSubscribe_positions delta: PositionSubscribe_positions
) => { ) =>
produce(data, (draft) => {
const index = draft.findIndex((m) => m.market.id === delta.market.id); const index = draft.findIndex((m) => m.market.id === delta.market.id);
if (index !== -1) { if (index !== -1) {
draft[index] = delta; draft[index] = delta;
} else { } else {
draft.push(delta); draft.push(delta);
} }
}; });
const getData = (responseData: Positions): Positions_party_positions[] | null => const getData = (responseData: Positions): Positions_party_positions[] | null =>
responseData.party ? responseData.party.positions : null; responseData.party ? responseData.party.positions : null;
const getDelta = ( const getDelta = (

View File

@ -1,5 +1,3 @@
import { produce } from 'immer';
import type { Draft } from 'immer';
import type { import type {
ApolloClient, ApolloClient,
DocumentNode, DocumentNode,
@ -34,11 +32,7 @@ export interface Subscribe<Data, Delta> {
type Query<Result> = DocumentNode | TypedDocumentNode<Result, any>; type Query<Result> = DocumentNode | TypedDocumentNode<Result, any>;
export interface Update<Data, Delta> { export interface Update<Data, Delta> {
( (data: Data, delta: Delta, reload: (forceReset?: boolean) => void): Data;
draft: Draft<Data>,
delta: Delta,
reload: (forceReset?: boolean) => void
): void;
} }
interface GetData<QueryData, Data> { interface GetData<QueryData, Data> {
@ -105,14 +99,12 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
data = getData(res.data); data = getData(res.data);
// if there was some updates received from subscription during initial query loading apply them on just received 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) { if (data && updateQueue && updateQueue.length > 0) {
data = produce(data, (draft) => {
while (updateQueue.length) { while (updateQueue.length) {
const delta = updateQueue.shift(); const delta = updateQueue.shift();
if (delta) { if (delta) {
update(draft, delta, reload); data = update(data, delta, reload);
} }
} }
});
} }
} catch (e) { } catch (e) {
// if error will occur data provider stops subscription // if error will occur data provider stops subscription
@ -168,9 +160,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
if (loading || !data) { if (loading || !data) {
updateQueue.push(delta); updateQueue.push(delta);
} else { } else {
const newData = produce(data, (draft) => { const newData = update(data, delta, reload);
update(draft, delta, reload);
});
if (newData === data) { if (newData === data) {
return; return;
} }

View File

@ -4,6 +4,7 @@ import type { TradeFields } from './__generated__/TradeFields';
import type { Trades } from './__generated__/Trades'; import type { Trades } from './__generated__/Trades';
import type { TradesSub } from './__generated__/TradesSub'; import type { TradesSub } from './__generated__/TradesSub';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import produce from 'immer';
export const MAX_TRADES = 50; 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); const incoming = sortTrades(delta);
// Add new trades to the top // Add new trades to the top
@ -62,7 +64,7 @@ const update = (draft: TradeFields[], delta: TradeFields[]) => {
if (draft.length > MAX_TRADES) { if (draft.length > MAX_TRADES) {
draft.splice(MAX_TRADES, draft.length - MAX_TRADES); draft.splice(MAX_TRADES, draft.length - MAX_TRADES);
} }
}; });
const getData = (responseData: Trades): TradeFields[] | null => const getData = (responseData: Trades): TradeFields[] | null =>
responseData.market ? responseData.market.trades : null; responseData.market ? responseData.market.trades : null;