Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bonsai-ui): Migrate Orderbook, Depth Chart, MidMarketPrice #1482

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
"@/bonsai/*/*",
"!@/bonsai/ontology",
"!@/bonsai/lib/*",
"!@/bonsai/types/summaryTypes"
"!@/bonsai/types/summaryTypes",
"!@/bonsai/types/orderbookTypes"
]
}
],
Expand Down
2 changes: 1 addition & 1 deletion src/bonsai/calculators/funding.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FundingDirection } from '@/constants/markets';
import { FundingDirection } from '@/constants/charts';
import { IndexerHistoricalFundingResponseObject } from '@/types/indexer/indexerApiGen';

import { MustBigNumber } from '@/lib/numbers';
Expand Down
163 changes: 122 additions & 41 deletions src/bonsai/calculators/orderbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { weakMapMemoize } from 'reselect';

import { SMALL_USD_DECIMALS } from '@/constants/numbers';
import { GroupingMultiplier } from '@/constants/orderbook';
import { IndexerOrderSide } from '@/types/indexer/indexerApiGen';

import { isTruthy } from '@/lib/isTruthy';
import { BIG_NUMBERS, MustBigNumber, roundToNearestFactor } from '@/lib/numbers';
import { objectEntries } from '@/lib/objectHelpers';
import { orEmptyRecord } from '@/lib/typeUtils';

import { OrderbookLine, SubaccountOpenOrderPriceMap } from '../types/orderbookTypes';
import { OrderbookData } from '../types/rawTypes';
import { OrderbookLine } from '../types/summaryTypes';
import { SubaccountOrder } from '../types/summaryTypes';

type OrderbookLineBN = {
price: BigNumber;
Expand All @@ -24,6 +26,12 @@ type OrderbookLineBN = {

type RawOrderbookLineBN = Omit<OrderbookLineBN, 'depth' | 'depthCost'>;

export type OrderbookProcessingOptions = {
groupingMultiplier?: GroupingMultiplier;
asksSortOrder?: 'asc' | 'desc';
bidsSortOrder?: 'asc' | 'desc';
};

export const calculateOrderbook = weakMapMemoize((orderbook: OrderbookData | undefined) => {
if (orderbook == null) {
return undefined;
Expand Down Expand Up @@ -66,11 +74,11 @@ export const calculateOrderbook = weakMapMemoize((orderbook: OrderbookData | und
const highestBid = processedBids.at(0);
const midPriceBN = lowestAsk && highestBid && lowestAsk.price.plus(highestBid.price).div(2);
const spreadBN = lowestAsk && highestBid && lowestAsk.price.minus(highestBid.price);
const spreadPercentBN = spreadBN && midPriceBN && spreadBN.div(midPriceBN).times(100);
const spreadPercentBN = spreadBN && midPriceBN && spreadBN.div(midPriceBN);

return {
bids: processedBids,
asks: processedAsks,
bids: processedBids, // descending bids
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ty for these comments

asks: processedAsks, // ascending asks
midPrice: midPriceBN,
spread: spreadBN,
spreadPercent: spreadPercentBN,
Expand All @@ -84,67 +92,76 @@ export const calculateOrderbook = weakMapMemoize((orderbook: OrderbookData | und
export const formatOrderbook = weakMapMemoize(
(
currentMarketOrderbook: ReturnType<typeof calculateOrderbook>,
groupingMultiplier: GroupingMultiplier,
tickSize: string
tickSize: string,
options?: OrderbookProcessingOptions
) => {
if (currentMarketOrderbook == null) {
return undefined;
}

const {
groupingMultiplier = GroupingMultiplier.ONE,
asksSortOrder = 'desc',
bidsSortOrder = 'desc',
} = options ?? {
groupingMultiplier: GroupingMultiplier.ONE,
asksSortOrder: 'desc',
bidsSortOrder: 'desc',
};

// If groupingMultiplier is ONE, return the orderbook as is.
if (groupingMultiplier === GroupingMultiplier.ONE) {
const tickSizeDecimals = MustBigNumber(tickSize).dp() ?? SMALL_USD_DECIMALS;
const asks = (
asksSortOrder === 'desc'
? currentMarketOrderbook.asks.toReversed()
: currentMarketOrderbook.asks
).map(mapOrderbookLineToNumber);

const bids = (
bidsSortOrder === 'desc'
? currentMarketOrderbook.bids
: currentMarketOrderbook.bids.toReversed()
).map(mapOrderbookLineToNumber);

return {
asks: orderBy(
currentMarketOrderbook.asks.map(mapOrderbookLineToNumber),
(ask) => ask.price,
'desc'
),
bids: currentMarketOrderbook.bids.map(mapOrderbookLineToNumber),
asksMap: Object.fromEntries(
currentMarketOrderbook.asks.map((line) => [
line.price.toFixed(tickSizeDecimals),
mapOrderbookLineToNumber(line),
])
),
bidsMap: Object.fromEntries(
currentMarketOrderbook.bids.map((line) => [
line.price.toFixed(tickSizeDecimals),
mapOrderbookLineToNumber(line),
])
),
asks,
bids,
spread: currentMarketOrderbook.spread?.toNumber(),
spreadPercent: currentMarketOrderbook.spreadPercent?.toNumber(),
midPrice: currentMarketOrderbook.midPrice?.toNumber(),
};
}

const groupingTickSize = getGroupingTickSize(tickSize, groupingMultiplier);
const { groupingTickSize, groupingTickSizeDecimals } = getGroupingTickSize(
tickSize,
groupingMultiplier
);

const { asks, bids } = currentMarketOrderbook;
const groupedAsks = orEmptyRecord(group(asks, groupingTickSize));
const groupedBids = orEmptyRecord(group(bids, groupingTickSize, true));

// Convert groupedAsks and groupedBids to list and sort by price. Asks will now be descending and bids will be remain descending
const groupedAsks = orEmptyRecord(group(asks, groupingTickSize, groupingTickSizeDecimals));
const groupedBids = orEmptyRecord(
group(bids, groupingTickSize, groupingTickSizeDecimals, true)
);

// Convert groupedAsks and groupedBids to list and sort by price depending on options. Default is descending.
const asksList = orderBy(
Object.values(groupedAsks).map(mapOrderbookLineToNumber),
(ask) => ask.price,
'desc'
[(ask) => ask.price],
[asksSortOrder]
);
const bidsList = orderBy(
Object.values(groupedBids).map(mapOrderbookLineToNumber),
(bid) => bid.price,
'desc'
[bidsSortOrder]
);

const lowestAsk = asksList.at(-1);
const highestBid = bidsList.at(0);
const lowestAsk = asksList.at(asksSortOrder === 'desc' ? -1 : 0);
const highestBid = bidsList.at(bidsSortOrder === 'desc' ? 0 : -1);

return {
asks: asksList,
asksMap: groupedAsks,
bids: bidsList,
bidsMap: groupedBids,
spread: currentMarketOrderbook.spread?.toNumber(),
spreadPercent: currentMarketOrderbook.spreadPercent?.toNumber(),
midPrice: roundMidPrice(lowestAsk, highestBid, groupingTickSize)?.toNumber(),
Expand All @@ -153,6 +170,7 @@ export const formatOrderbook = weakMapMemoize(
);

/** ------ .map Helper Functions ------ */

function mapRawOrderbookLineToBN(
rawOrderbookLineEntry: [string, { size: string; offset: number }]
) {
Expand Down Expand Up @@ -256,9 +274,14 @@ function calculateDepthAndDepthCost(

/** ------ Grouping Helpers ------ */

const getGroupingTickSize = (tickSize: string, multiplier: GroupingMultiplier) => {
return MustBigNumber(tickSize).times(multiplier).toNumber();
};
export function getGroupingTickSize(tickSize: string, multiplier: GroupingMultiplier) {
const groupingTickSize = MustBigNumber(tickSize).times(multiplier).toNumber();

return {
groupingTickSize,
groupingTickSizeDecimals: MustBigNumber(groupingTickSize).dp() ?? SMALL_USD_DECIMALS,
};
}

/**
*
Expand All @@ -267,7 +290,12 @@ const getGroupingTickSize = (tickSize: string, multiplier: GroupingMultiplier) =
* @param shouldFloor we want to round asks up and bids down so they don't have an overlapping group in the middle
* @returns Grouped OrderbookLines in a dictionary using price as key
*/
const group = (orderbook: OrderbookLineBN[], groupingTickSize: number, shouldFloor?: boolean) => {
const group = (
orderbook: OrderbookLineBN[],
groupingTickSize: number,
groupingTickSizeDecimals: number,
shouldFloor?: boolean
) => {
if (orderbook.length === 0) {
return null;
}
Expand All @@ -281,7 +309,7 @@ const group = (orderbook: OrderbookLineBN[], groupingTickSize: number, shouldFlo
roundingMode: shouldFloor ? BigNumber.ROUND_DOWN : BigNumber.ROUND_UP,
});

const key = price.toString();
const key = price.toFixed(groupingTickSizeDecimals);
const existingLine = mapResult[key];

if (existingLine) {
Expand Down Expand Up @@ -324,3 +352,56 @@ function roundMidPrice(
})
: roundedMidPrice;
}

/** ------ Orderbook + Open Order Helpers ------ */

export function getSubaccountOpenOrdersPriceMap(
subaccountOpenOrders: SubaccountOrder[],
groupingTickSize: number,
groupingTickSizeDecimals: number
) {
return subaccountOpenOrders.reduce(
(acc, order) => {
const side = order.side;

const key = roundToNearestFactor({
number: MustBigNumber(order.price),
factor: groupingTickSize,
roundingMode: side === IndexerOrderSide.BUY ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL,
}).toFixed(groupingTickSizeDecimals);

if (acc[side][key]) {
acc[side][key] = acc[side][key].plus(order.size);
} else {
acc[side][key] = order.size;
}
return acc;
},
{
[IndexerOrderSide.BUY]: {},
[IndexerOrderSide.SELL]: {},
} as SubaccountOpenOrderPriceMap
);
}

export function findMine({
orderMap,
side,
price,
groupingTickSize,
groupingTickSizeDecimals,
}: {
orderMap: SubaccountOpenOrderPriceMap;
side: IndexerOrderSide;
price: number;
groupingTickSize: number;
groupingTickSizeDecimals: number;
}): number | undefined {
const key = roundToNearestFactor({
number: MustBigNumber(price),
factor: groupingTickSize,
roundingMode: side === IndexerOrderSide.BUY ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL,
}).toFixed(groupingTickSizeDecimals);

return orderMap[side][key]?.toNumber();
}
33 changes: 29 additions & 4 deletions src/bonsai/ontology.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { HeightResponse } from '@dydxprotocol/v4-client-js';
import BigNumber from 'bignumber.js';

import { GroupingMultiplier } from '@/constants/orderbook';
import { IndexerWsTradesUpdateObject } from '@/types/indexer/indexerManual';

import { type RootState } from '@/state/_store';
Expand Down Expand Up @@ -47,7 +49,11 @@ import {
import { selectEquityTiers, selectFeeTiers } from './selectors/configs';
import { selectCurrentMarketOrderbookLoading } from './selectors/markets';
import {
createSelectCurrentMarketGroupedOrderbook,
createSelectCurrentMarketOrderbook,
selectCurrentMarketDepthChart,
selectCurrentMarketMidPrice,
} from './selectors/orderbook';
import {
createSelectMarketSummaryById,
selectAllMarketSummaries,
selectAllMarketSummariesLoading,
Expand All @@ -59,6 +65,7 @@ import {
StablePerpetualMarketSummary,
} from './selectors/summary';
import { selectUserStats } from './selectors/userStats';
import { DepthChartData, OrderbookProcessedData } from './types/orderbookTypes';
import {
AccountBalances,
AllAssetData,
Expand All @@ -67,7 +74,6 @@ import {
EquityTiersSummary,
FeeTierSummary,
GroupedSubaccountSummary,
OrderbookProcessedData,
PendingIsolatedPosition,
PerpetualMarketSummaries,
PerpetualMarketSummary,
Expand Down Expand Up @@ -217,7 +223,18 @@ interface BonsaiHelpersShape {
fills: BasicSelector<SubaccountFill[]>;
};
orderbook: {
createSelectGroupedData: ParameterizedSelector<OrderbookProcessedData | undefined, [number]>;
createSelectGroupedData: ParameterizedSelector<
OrderbookProcessedData | undefined,
[GroupingMultiplier | undefined]
>;
loading: BasicSelector<LoadableStatus>;
};
midPrice: {
data: BasicSelector<BigNumber | undefined>;
loading: BasicSelector<LoadableStatus>;
};
depthChart: {
data: BasicSelector<DepthChartData | undefined>;
loading: BasicSelector<LoadableStatus>;
};
};
Expand Down Expand Up @@ -256,7 +273,15 @@ export const BonsaiHelpers: BonsaiHelpersShape = {
assetLogo: selectCurrentMarketAssetLogoUrl,
assetName: selectCurrentMarketAssetName,
orderbook: {
createSelectGroupedData: createSelectCurrentMarketGroupedOrderbook,
createSelectGroupedData: createSelectCurrentMarketOrderbook,
loading: selectCurrentMarketOrderbookLoading,
},
midPrice: {
data: selectCurrentMarketMidPrice,
loading: selectCurrentMarketOrderbookLoading,
},
depthChart: {
data: selectCurrentMarketDepthChart,
loading: selectCurrentMarketOrderbookLoading,
},
account: {
Expand Down
Loading
Loading