From 9129235e4215900c302700bb4103a5404a3ba668 Mon Sep 17 00:00:00 2001 From: jaredvu Date: Mon, 27 Jan 2025 18:05:24 -0800 Subject: [PATCH 01/13] Add grouping calculations, fix orderbook calc --- src/bonsai/calculators/orderbook.ts | 234 +++++++++++++++++++++------- src/bonsai/ontology.ts | 10 +- src/bonsai/selectors/markets.ts | 18 --- src/bonsai/selectors/summary.ts | 24 ++- src/constants/orderbook.ts | 17 ++ 5 files changed, 218 insertions(+), 85 deletions(-) diff --git a/src/bonsai/calculators/orderbook.ts b/src/bonsai/calculators/orderbook.ts index 674c1201f..18c60f1f9 100644 --- a/src/bonsai/calculators/orderbook.ts +++ b/src/bonsai/calculators/orderbook.ts @@ -1,71 +1,71 @@ import BigNumber from 'bignumber.js'; import { weakMapMemoize } from 'reselect'; +import { GroupingMultiplier } from '@/constants/orderbook'; + import { isTruthy } from '@/lib/isTruthy'; -import { BIG_NUMBERS, MustBigNumber } from '@/lib/numbers'; +import { BIG_NUMBERS, MustBigNumber, roundToNearestFactor } from '@/lib/numbers'; import { objectEntries } from '@/lib/objectHelpers'; import { OrderbookData } from '../types/rawTypes'; -import { OrderbookLine, OrderbookProcessedData } from '../types/summaryTypes'; +import { OrderbookLine } from '../types/summaryTypes'; type OrderbookLineBN = { price: BigNumber; size: BigNumber; sizeCost: BigNumber; offset: number; - depth?: BigNumber; - depthCost?: BigNumber; + depth: BigNumber; + depthCost: BigNumber; }; -export const calculateOrderbook = weakMapMemoize( - (orderbook: OrderbookData | undefined): OrderbookProcessedData | undefined => { - if (orderbook == null) { - return undefined; - } +export const calculateOrderbook = weakMapMemoize((orderbook: OrderbookData | undefined) => { + if (orderbook == null) { + return undefined; + } - const { asks, bids } = orderbook; - - /** - * 1. Process raw orderbook data - * 2. filter out lines with size <= 0 - * 3. sort by price - */ - const asksBase: OrderbookLineBN[] = objectEntries(asks) - .map(mapRawOrderbookLineToBN) - .filter(isTruthy) - .sort((a, b) => a.price.minus(b.price).toNumber()); - - const bidsBase: OrderbookLineBN[] = objectEntries(bids) - .map(mapRawOrderbookLineToBN) - .filter(isTruthy) - .sort((a, b) => a.price.minus(b.price).toNumber()); - - // calculate depth and depthCost - const asksComposite = calculateDepthAndDepthCost(asksBase); - const bidsComposite = calculateDepthAndDepthCost(bidsBase); - - // un-cross orderbook - const uncrossedBook: { asks: OrderbookLineBN[]; bids: OrderbookLineBN[] } = uncrossOrderbook( - asksComposite, - bidsComposite - ); - - // calculate midPrice, spread, and spreadPercent - const lowestAsk = uncrossedBook.asks.at(0); - const highestBid = uncrossedBook.bids.at(-1); - 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 { asks, bids } = orderbook; - return { - bids: uncrossedBook.bids.map(mapOrderbookLineToNumber), - asks: uncrossedBook.asks.map(mapOrderbookLineToNumber), - midPrice: midPriceBN?.toNumber(), - spread: spreadBN?.toNumber(), - spreadPercent: spreadPercentBN?.toNumber(), - }; - } -); + /** + * 1. Process raw orderbook data and + * 2. filter out lines with size <= 0 + * 3. sort by price: asks ascending, bids descending + */ + const asksBase: Omit[] = objectEntries(asks) + .map(mapRawOrderbookLineToBN) + .filter(isTruthy) + .sort((a, b) => a.price.minus(b.price).toNumber()); + + const bidsBase: Omit[] = objectEntries(bids) + .map(mapRawOrderbookLineToBN) + .filter(isTruthy) + .sort((a, b) => b.price.minus(a.price).toNumber()); + + // calculate depth and depthCost + const asksComposite = calculateDepthAndDepthCost(asksBase); + const bidsComposite = calculateDepthAndDepthCost(bidsBase); + + // un-cross orderbook + const uncrossedBook: { asks: OrderbookLineBN[]; bids: OrderbookLineBN[] } = uncrossOrderbook( + asksComposite, + bidsComposite + ); + + // calculate midPrice, spread, and spreadPercent + const lowestAsk = uncrossedBook.asks.at(0); + const highestBid = uncrossedBook.bids.at(-1); + 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); + + return { + bids: uncrossedBook.bids, + asks: uncrossedBook.asks, + midPrice: midPriceBN, + spread: spreadBN, + spreadPercent: spreadPercentBN, + }; +}); function uncrossOrderbook(asks: OrderbookLineBN[], bids: OrderbookLineBN[]) { if (asks.length === 0 || bids.length === 0) { @@ -82,21 +82,25 @@ function uncrossOrderbook(asks: OrderbookLineBN[], bids: OrderbookLineBN[]) { if (ask.offset === bid.offset) { // If offsets are the same, give precedence to the larger size. In this case, // one of the sizes "should" be zero, but we simply check for the larger size. - if (ask.size.gt(bid.size)) { + if (ask.size.gte(bid.size)) { // remove the bid - bid = bidsCopy.shift(); + bidsCopy.shift(); + bid = bidsCopy.at(0); } else { // remove the ask - ask = asksCopy.shift(); + asksCopy.pop(); + ask = asksCopy.at(-1); } } else { // If offsets are different, remove the older offset. if (ask.offset < bid.offset) { // remove the ask - ask = asksCopy.shift(); + asksCopy.pop(); + ask = asksCopy.at(-1); } else { // remove the bid - bid = bidsCopy.shift(); + bidsCopy.shift(); + bid = bidsCopy.at(0); } } } @@ -104,7 +108,9 @@ function uncrossOrderbook(asks: OrderbookLineBN[], bids: OrderbookLineBN[]) { return { asks: asksCopy, bids: bidsCopy }; } -function calculateDepthAndDepthCost(lines: OrderbookLineBN[]) { +function calculateDepthAndDepthCost( + lines: Omit[] +): OrderbookLineBN[] { let depth = BIG_NUMBERS.ZERO; let depthCost = BIG_NUMBERS.ZERO; @@ -137,7 +143,7 @@ function mapRawOrderbookLineToBN( }; } -function mapOrderbookLineToNumber(orderbookLineBN: OrderbookLineBN): OrderbookLine { +export function mapOrderbookLineToNumber(orderbookLineBN: OrderbookLineBN): OrderbookLine { const { price, size, sizeCost, depth, depthCost, offset } = orderbookLineBN; return { @@ -145,11 +151,119 @@ function mapOrderbookLineToNumber(orderbookLineBN: OrderbookLineBN): OrderbookLi size: size.toNumber(), sizeCost: sizeCost.toNumber(), offset, - depth: depth?.toNumber() ?? 0, - depthCost: depthCost?.toNumber() ?? 0, + depth: depth.toNumber(), + depthCost: depthCost.toNumber(), }; } function isCrossed(ask: OrderbookLineBN, bid: OrderbookLineBN) { return ask.price.lte(bid.price); } + +const getGroupingTickSize = (tickSize: string, multiplier: GroupingMultiplier) => { + return MustBigNumber(tickSize).times(multiplier).toNumber(); +}; + +/** + * + * @param orderbook + * @param groupingTickSize + * @param shouldFloor we want to round asks up and bids down so they don't have an overlapping group in the middle + * @returns grouped orderbook side or null + */ +const group = weakMapMemoize( + ( + orderbook: OrderbookLineBN[], + groupingTickSize: number, + shouldFloor?: boolean + ): OrderbookLineBN[] | null => { + if (orderbook.length === 0) { + return null; + } + + const result: OrderbookLineBN[] = []; + + orderbook.forEach((line) => { + const price = roundToNearestFactor({ + number: line.price, + factor: groupingTickSize, + roundingMode: shouldFloor ? BigNumber.ROUND_DOWN : BigNumber.ROUND_UP, + }); + + const existingLine = result.find((r) => r.price.eq(price)); + + if (existingLine) { + existingLine.size = existingLine.size.plus(line.size); + existingLine.sizeCost = existingLine.sizeCost.plus(line.sizeCost); + existingLine.depth = line.depth; + existingLine.depthCost = line.depthCost; + } else { + // Asks are ascending and bids are descending, so we need to unshift asks and push bids + if (shouldFloor) { + result.push({ + ...line, + price, + }); + } else { + result.unshift({ + ...line, + price, + }); + } + } + }); + + return result; + } +); + +function roundMidPrice( + lowestAsk: OrderbookLineBN | undefined, + highestBid: OrderbookLineBN | undefined, + groupingTickSize: number +) { + if (!lowestAsk || !highestBid) { + return undefined; + } + + const roundedMidPrice = roundToNearestFactor({ + number: lowestAsk.price.plus(highestBid.price).div(2), + factor: groupingTickSize, + roundingMode: BigNumber.ROUND_HALF_UP, + }); + + return roundedMidPrice.eq(lowestAsk!.price) || roundedMidPrice.eq(highestBid!.price) + ? roundToNearestFactor({ + number: lowestAsk!.price.plus(highestBid!.price).div(2), + factor: groupingTickSize / 10, + roundingMode: BigNumber.ROUND_HALF_UP, + }) + : roundedMidPrice; +} + +export const groupOrderbook = weakMapMemoize( + ( + currentMarketOrderbook: ReturnType, + groupingMultiplier: GroupingMultiplier, + tickSize: string + ) => { + if (currentMarketOrderbook == null) { + return undefined; + } + + const groupingTickSize = getGroupingTickSize(tickSize, groupingMultiplier); + const { asks, bids } = currentMarketOrderbook; + const groupedAsks = group(asks, groupingTickSize); + const groupedBids = group(bids, groupingTickSize, true); + const lowestAsk = groupedAsks?.at(-1); + const highestBid = groupedBids?.at(0); + + return { + asks: groupedAsks?.map(mapOrderbookLineToNumber) ?? [], + bids: groupedBids?.map(mapOrderbookLineToNumber) ?? [], + spread: currentMarketOrderbook.spread?.toNumber(), + spreadPercent: currentMarketOrderbook.spreadPercent?.toNumber(), + midPrice: roundMidPrice(lowestAsk, highestBid, groupingTickSize)?.toNumber(), + }; + } +); diff --git a/src/bonsai/ontology.ts b/src/bonsai/ontology.ts index d410e5576..1ca69c475 100644 --- a/src/bonsai/ontology.ts +++ b/src/bonsai/ontology.ts @@ -42,11 +42,9 @@ import { selectRawIndexerHeightDataLoading, selectRawValidatorHeightDataLoading, } from './selectors/base'; +import { selectCurrentMarketOrderbookLoading } from './selectors/markets'; import { - selectCurrentMarketOrderbookData, - selectCurrentMarketOrderbookLoading, -} from './selectors/markets'; -import { + createSelectCurrentMarketGroupedOrderbook, createSelectMarketSummaryById, selectAllMarketSummaries, selectAllMarketSummariesLoading, @@ -182,7 +180,7 @@ interface BonsaiHelpersShape { fills: BasicSelector; }; orderbook: { - data: BasicSelector; + createSelectGroupedData: ParameterizedSelector; loading: BasicSelector; }; }; @@ -217,7 +215,7 @@ export const BonsaiHelpers: BonsaiHelpersShape = { marketInfo: selectCurrentMarketInfo, stableMarketInfo: selectCurrentMarketInfoStable, orderbook: { - data: selectCurrentMarketOrderbookData, + createSelectGroupedData: createSelectCurrentMarketGroupedOrderbook, loading: selectCurrentMarketOrderbookLoading, }, account: { diff --git a/src/bonsai/selectors/markets.ts b/src/bonsai/selectors/markets.ts index 5e6b68307..9e89dd7ee 100644 --- a/src/bonsai/selectors/markets.ts +++ b/src/bonsai/selectors/markets.ts @@ -2,7 +2,6 @@ import { createAppSelector } from '@/state/appTypes'; import { getCurrentMarketId } from '@/state/perpetualsSelectors'; import { calculateAllMarkets, formatSparklineData } from '../calculators/markets'; -import { calculateOrderbook } from '../calculators/orderbook'; import { mergeLoadableStatus } from '../lib/mapLoadable'; import { selectRawMarketsData, @@ -40,20 +39,3 @@ export const selectCurrentMarketOrderbookLoading = createAppSelector( (currentMarketOrderbook) => currentMarketOrderbook ? mergeLoadableStatus(currentMarketOrderbook) : 'idle' ); - -export const selectCurrentMarketOrderbookData = createAppSelector( - [selectCurrentMarketOrderbook], - (currentMarketOrderbook) => calculateOrderbook(currentMarketOrderbook?.data) -); - -export const createSelectCurrentMarketOrderbook = createAppSelector( - [selectCurrentMarketOrderbook, (_s, groupingMultiplier?: number) => groupingMultiplier], - (currentMarketOrderbook, _groupingMultiplier) => { - if (currentMarketOrderbook?.data == null) { - return undefined; - } - - // TODO: groupMultiplier calculations on the orderbook - return calculateOrderbook(currentMarketOrderbook.data); - } -); diff --git a/src/bonsai/selectors/summary.ts b/src/bonsai/selectors/summary.ts index 4033e6daa..d512bdca4 100644 --- a/src/bonsai/selectors/summary.ts +++ b/src/bonsai/selectors/summary.ts @@ -6,11 +6,16 @@ import { getFavoritedMarkets } from '@/state/appUiConfigsSelectors'; import { getCurrentMarketId } from '@/state/perpetualsSelectors'; import { createMarketSummary } from '../calculators/markets'; +import { calculateOrderbook, groupOrderbook } from '../calculators/orderbook'; import { mergeLoadableStatus } from '../lib/mapLoadable'; import { PerpetualMarketSummary } from '../types/summaryTypes'; import { selectAllAssetsInfo } from './assets'; import { selectRawAssets, selectRawMarkets } from './base'; -import { selectAllMarketsInfo, selectSparkLinesData } from './markets'; +import { + selectAllMarketsInfo, + selectCurrentMarketOrderbook, + selectSparkLinesData, +} from './markets'; export const selectAllMarketSummariesLoading = createAppSelector( [selectRawMarkets, selectRawAssets], @@ -78,3 +83,20 @@ export const createSelectMarketSummaryById = () => return allSummaries?.[marketId]; } ); + +export const createSelectCurrentMarketGroupedOrderbook = () => + createAppSelector( + [ + selectCurrentMarketOrderbook, + selectCurrentMarketInfoStable, + (_s, groupingMultiplier?: number) => groupingMultiplier ?? 1, + ], + (currentMarketOrderbook, currentMarketStableInfo, groupingMultiplier) => { + if (currentMarketOrderbook == null || currentMarketStableInfo == null) { + return undefined; + } + const { tickSize } = currentMarketStableInfo; + const orderbook = calculateOrderbook(currentMarketOrderbook.data); + return groupOrderbook(orderbook, groupingMultiplier, tickSize); + } + ); diff --git a/src/constants/orderbook.ts b/src/constants/orderbook.ts index 046846ba6..9ff460f59 100644 --- a/src/constants/orderbook.ts +++ b/src/constants/orderbook.ts @@ -14,3 +14,20 @@ export const ORDERBOOK_WIDTH = 300; export const ORDERBOOK_ROW_HEIGHT = 21; export const ORDERBOOK_ROW_PADDING_RIGHT = 8; export const ORDERBOOK_HEADER_HEIGHT = 70; + +/** + * @description Orderbook grouping constants + */ +export enum GroupingMultiplier { + ONE = 1, + TEN = 10, + HUNDRED = 100, + THOUSAND = 1_000, +} + +export const GROUPING_MULTIPLIER_LIST = [ + GroupingMultiplier.ONE, + GroupingMultiplier.TEN, + GroupingMultiplier.HUNDRED, + GroupingMultiplier.THOUSAND, +]; From 87dfa39ff82f9c16868b0d8cd0829a7c5e383d06 Mon Sep 17 00:00:00 2001 From: jaredvu Date: Tue, 28 Jan 2025 08:35:40 -0800 Subject: [PATCH 02/13] replace orderbook selectors --- src/hooks/Orderbook/useDrawOrderbook.ts | 10 +-- src/hooks/Orderbook/useOrderbookValues.ts | 68 ++++++++++++---- src/views/CanvasOrderbook/CanvasOrderbook.tsx | 79 +++++++++++-------- .../CanvasOrderbook/OrderbookControls.tsx | 44 +++++------ src/views/CanvasOrderbook/OrderbookRow.tsx | 15 +--- 5 files changed, 131 insertions(+), 85 deletions(-) diff --git a/src/hooks/Orderbook/useDrawOrderbook.ts b/src/hooks/Orderbook/useDrawOrderbook.ts index 1c4c561f7..e36231e13 100644 --- a/src/hooks/Orderbook/useDrawOrderbook.ts +++ b/src/hooks/Orderbook/useDrawOrderbook.ts @@ -2,7 +2,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { shallowEqual } from 'react-redux'; -import type { PerpetualMarketOrderbookLevel } from '@/constants/abacus'; import { SMALL_USD_DECIMALS, TOKEN_DECIMALS } from '@/constants/numbers'; import { ORDERBOOK_ANIMATION_DURATION, @@ -32,11 +31,12 @@ import { generateFadedColorVariant } from '@/lib/styles'; import { orEmptyObj } from '@/lib/typeUtils'; import { useLocaleSeparators } from '../useLocaleSeparators'; +import { OrderbookLineWithMine } from './useOrderbookValues'; type ElementProps = { - data: Array; + data: Array; histogramRange: number; - side: PerpetualMarketOrderbookLevel['side']; + side: OrderbookLineWithMine['side']; displayUnit: DisplayUnit; }; @@ -323,7 +323,7 @@ export const useDrawOrderbook = ({ }: { ctx: CanvasRenderingContext2D; idx: number; - rowToRender?: PerpetualMarketOrderbookLevel; + rowToRender?: OrderbookLineWithMine; animationType?: OrderbookRowAnimationType; }) => { if (!rowToRender) return; @@ -356,7 +356,7 @@ export const useDrawOrderbook = ({ drawText({ animationType, ctx, - depth: depth ?? undefined, + depth, depthCost, sizeCost, price, diff --git a/src/hooks/Orderbook/useOrderbookValues.ts b/src/hooks/Orderbook/useOrderbookValues.ts index 30a8eb5a9..076d7af52 100644 --- a/src/hooks/Orderbook/useOrderbookValues.ts +++ b/src/hooks/Orderbook/useOrderbookValues.ts @@ -1,10 +1,12 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo, useState } from 'react'; +import { BonsaiHelpers } from '@/bonsai/ontology'; +import { OrderbookLine } from '@/bonsai/types/summaryTypes'; import { OrderSide } from '@dydxprotocol/v4-client-js'; import { shallowEqual } from 'react-redux'; -import { OrderbookLine, type PerpetualMarketOrderbookLevel } from '@/constants/abacus'; import { DepthChartDatum, DepthChartSeries } from '@/constants/charts'; +import { GROUPING_MULTIPLIER_LIST, GroupingMultiplier } from '@/constants/orderbook'; import { getSubaccountOrderSizeBySideAndOrderbookLevel } from '@/state/accountSelectors'; import { useAppSelector } from '@/state/appTypes'; @@ -14,19 +16,49 @@ import { MustBigNumber } from '@/lib/numbers'; import { safeAssign } from '@/lib/objectHelpers'; import { orEmptyRecord } from '@/lib/typeUtils'; +import { useParameterizedSelector } from '../useParameterizedSelector'; + +export type OrderbookLineWithMine = OrderbookLine & { + mine?: number; + key: string; + side: 'ask' | 'bid'; +}; + export const useCalculateOrderbookData = ({ rowsPerSide }: { rowsPerSide: number }) => { - const orderbook = useAppSelector(getCurrentMarketOrderbook, shallowEqual); + // const orderbook = useAppSelector(getCurrentMarketOrderbook, shallowEqual); + const [groupingMultiplier, setGroupingMultiplier] = useState(GroupingMultiplier.ONE); const subaccountOrderSizeBySideAndPrice = orEmptyRecord( useAppSelector(getSubaccountOrderSizeBySideAndOrderbookLevel, shallowEqual) ); + const orderbook = useParameterizedSelector( + BonsaiHelpers.currentMarket.orderbook.createSelectGroupedData, + groupingMultiplier + ); + + const modifyGroupingMultiplier = useCallback((shouldIncrease: boolean) => { + setGroupingMultiplier((prev) => { + const currIdx = GROUPING_MULTIPLIER_LIST.indexOf(prev); + if (currIdx === -1) return prev; + const canIncrease = prev !== GroupingMultiplier.THOUSAND; + const canDecrease = prev !== GroupingMultiplier.ONE; + if (shouldIncrease && canIncrease) { + return GROUPING_MULTIPLIER_LIST[currIdx + 1]!; + } + + if (!shouldIncrease && canDecrease) { + return GROUPING_MULTIPLIER_LIST[currIdx - 1]!; + } + + return prev; + }); + }, []); + return useMemo(() => { - const asks: Array = ( - orderbook?.asks?.toArray() ?? [] - ) + const asks: Array = (orderbook?.asks ?? []) .map( - (row: OrderbookLine, idx: number): PerpetualMarketOrderbookLevel => + (row: OrderbookLine, idx: number): OrderbookLineWithMine => safeAssign( {}, { @@ -37,13 +69,11 @@ export const useCalculateOrderbookData = ({ rowsPerSide }: { rowsPerSide: number row ) ) - .slice(0, rowsPerSide); + .slice(-1 * rowsPerSide); - const bids: Array = ( - orderbook?.bids?.toArray() ?? [] - ) + const bids: Array = (orderbook?.bids ?? []) .map( - (row: OrderbookLine, idx: number): PerpetualMarketOrderbookLevel => + (row: OrderbookLine, idx: number): OrderbookLineWithMine => safeAssign( {}, { @@ -67,13 +97,23 @@ export const useCalculateOrderbookData = ({ rowsPerSide }: { rowsPerSide: number return { asks, bids, + midMarketPrice: orderbook?.midPrice, spread, spreadPercent, histogramRange, hasOrderbook: !!orderbook, - currentGrouping: orderbook?.grouping, + + // Orderbook grouping + groupingMultiplier, + modifyGroupingMultiplier, }; - }, [rowsPerSide, orderbook, subaccountOrderSizeBySideAndPrice]); + }, [ + rowsPerSide, + orderbook, + subaccountOrderSizeBySideAndPrice, + groupingMultiplier, + modifyGroupingMultiplier, + ]); }; export const useOrderbookValuesForDepthChart = () => { diff --git a/src/views/CanvasOrderbook/CanvasOrderbook.tsx b/src/views/CanvasOrderbook/CanvasOrderbook.tsx index 0abb469f3..52aadb30e 100644 --- a/src/views/CanvasOrderbook/CanvasOrderbook.tsx +++ b/src/views/CanvasOrderbook/CanvasOrderbook.tsx @@ -1,18 +1,21 @@ import { forwardRef, useCallback, useMemo, useRef } from 'react'; -import { shallowEqual } from 'react-redux'; +import { BonsaiHelpers } from '@/bonsai/ontology'; import styled, { css } from 'styled-components'; import tw from 'twin.macro'; -import { AbacusInputTypes, Nullable, type PerpetualMarketOrderbookLevel } from '@/constants/abacus'; +import { AbacusInputTypes, Nullable } from '@/constants/abacus'; import { STRING_KEYS } from '@/constants/localization'; -import { SMALL_USD_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; +import { USD_DECIMALS } from '@/constants/numbers'; import { ORDERBOOK_MAX_ROWS_PER_SIDE, ORDERBOOK_ROW_HEIGHT } from '@/constants/orderbook'; import { useCenterOrderbook } from '@/hooks/Orderbook/useCenterOrderbook'; import { useDrawOrderbook } from '@/hooks/Orderbook/useDrawOrderbook'; import { useOrderbookMiddleRowScrollListener } from '@/hooks/Orderbook/useOrderbookMiddleRowScrollListener'; -import { useCalculateOrderbookData } from '@/hooks/Orderbook/useOrderbookValues'; +import { + OrderbookLineWithMine, + useCalculateOrderbookData, +} from '@/hooks/Orderbook/useOrderbookValues'; import { useStringGetter } from '@/hooks/useStringGetter'; import { Canvas } from '@/components/Canvas'; @@ -24,13 +27,9 @@ import { useAppDispatch, useAppSelector } from '@/state/appTypes'; import { getSelectedDisplayUnit } from '@/state/appUiConfigsSelectors'; import { setTradeFormInputs } from '@/state/inputs'; import { getCurrentInput } from '@/state/inputsSelectors'; -import { - getCurrentMarketConfig, - getCurrentMarketData, - getCurrentMarketId, -} from '@/state/perpetualsSelectors'; import { MustBigNumber } from '@/lib/numbers'; +import { orEmptyObj } from '@/lib/typeUtils'; import { OrderbookControls } from './OrderbookControls'; import { OrderbookMiddleRow, OrderbookRow } from './OrderbookRow'; @@ -57,18 +56,25 @@ export const CanvasOrderbook = forwardRef( }: ElementProps & StyleProps, ref: React.ForwardedRef ) => { - const { asks, bids, hasOrderbook, histogramRange, currentGrouping } = useCalculateOrderbookData( - { - rowsPerSide, - } - ); + const { + asks, + bids, + midMarketPrice, + hasOrderbook, + histogramRange, + groupingMultiplier, + modifyGroupingMultiplier, + } = useCalculateOrderbookData({ + rowsPerSide, + }); - const stringGetter = useStringGetter(); - const currentMarket = useAppSelector(getCurrentMarketId) ?? ''; - const currentMarketConfig = useAppSelector(getCurrentMarketConfig, shallowEqual); - const { assetId: id } = useAppSelector(getCurrentMarketData, shallowEqual) ?? {}; + const { + assetId: id, + ticker, + tickSizeDecimals = USD_DECIMALS, + } = orEmptyObj(useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo)); - const { tickSizeDecimals = USD_DECIMALS } = currentMarketConfig ?? {}; + const stringGetter = useStringGetter(); /** * Slice asks and bids to rowsPerSide using empty rows @@ -79,19 +85,13 @@ export const CanvasOrderbook = forwardRef( ? new Array(rowsPerSide - asks.length).fill(undefined) : []; - const newAsksSlice: Array = [ - ...emptyAskRows, - ...asks.reverse(), - ]; + const newAsksSlice: Array = [...emptyAskRows, ...asks]; const emptyBidRows = bids.length < rowsPerSide ? new Array(rowsPerSide - bids.length).fill(undefined) : []; - const newBidsSlice: Array = [ - ...bids, - ...emptyBidRows, - ]; + const newBidsSlice: Array = [...bids, ...emptyBidRows]; return { asksSlice: layout === 'horizontal' ? newAsksSlice : newAsksSlice.reverse(), @@ -100,9 +100,10 @@ export const CanvasOrderbook = forwardRef( }, [asks, bids, layout, rowsPerSide]); const orderbookRef = useRef(null); + useCenterOrderbook({ orderbookRef, - marketId: currentMarket, + marketId: ticker ?? '', disabled: layout === 'horizontal', }); @@ -127,7 +128,7 @@ export const CanvasOrderbook = forwardRef( // avoid scientific notation for when converting small number to string dispatch( setTradeFormInputs({ - limitPriceInput: MustBigNumber(price).toFixed(tickSizeDecimals ?? SMALL_USD_DECIMALS), + limitPriceInput: MustBigNumber(price).toFixed(tickSizeDecimals), }) ); } @@ -156,7 +157,7 @@ export const CanvasOrderbook = forwardRef( const asksOrderbook = ( <$OrderbookSideContainer $side="asks" $rows={rowsPerSide}> <$HoverRows $bottom={layout !== 'horizontal'}> - {[...asksSlice].reverse().map((row: PerpetualMarketOrderbookLevel | undefined, idx) => + {asksSlice.map((row: OrderbookLineWithMine | undefined, idx) => row ? ( <$Row // eslint-disable-next-line react/no-array-index-key @@ -178,7 +179,7 @@ export const CanvasOrderbook = forwardRef( const bidsOrderbook = ( <$OrderbookSideContainer $side="bids" $rows={rowsPerSide}> <$HoverRows> - {bidsSlice.map((row: PerpetualMarketOrderbookLevel | undefined, idx) => + {bidsSlice.map((row: OrderbookLineWithMine | undefined, idx) => row ? ( <$Row // eslint-disable-next-line react/no-array-index-key @@ -205,7 +206,13 @@ export const CanvasOrderbook = forwardRef( return (
<$OrderbookContent $isLoading={!hasOrderbook}> - {!hideHeader && } + {!hideHeader && ( + + )} {!hideHeader && ( <$OrderbookRow tw="h-1.75 text-color-text-0"> @@ -225,6 +232,7 @@ export const CanvasOrderbook = forwardRef( {(displaySide === 'top' || layout === 'horizontal') && ( <$OrderbookMiddleRow side="top" + midMarketPrice={midMarketPrice} tickSizeDecimals={tickSizeDecimals} isHeader={layout === 'horizontal'} /> @@ -234,13 +242,18 @@ export const CanvasOrderbook = forwardRef(
{asksOrderbook} {bidsOrderbook}
{displaySide === 'bottom' && ( - <$OrderbookMiddleRow side="bottom" tickSizeDecimals={tickSizeDecimals} /> + <$OrderbookMiddleRow + side="bottom" + midMarketPrice={midMarketPrice} + tickSizeDecimals={tickSizeDecimals} + /> )} ) : ( diff --git a/src/views/CanvasOrderbook/OrderbookControls.tsx b/src/views/CanvasOrderbook/OrderbookControls.tsx index 882d77d71..4dbd13e9c 100644 --- a/src/views/CanvasOrderbook/OrderbookControls.tsx +++ b/src/views/CanvasOrderbook/OrderbookControls.tsx @@ -1,12 +1,12 @@ import { useCallback } from 'react'; -import { clamp } from 'lodash'; -import { shallowEqual, useDispatch } from 'react-redux'; +import { BonsaiHelpers } from '@/bonsai/ontology'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import { MarketOrderbookGrouping, Nullable, OrderbookGrouping } from '@/constants/abacus'; import { ButtonShape, ButtonSize, ButtonStyle } from '@/constants/buttons'; import { USD_DECIMALS } from '@/constants/numbers'; +import { GroupingMultiplier } from '@/constants/orderbook'; import { DisplayUnit } from '@/constants/trade'; import { Button } from '@/components/Button'; @@ -17,33 +17,32 @@ import { ToggleGroup } from '@/components/ToggleGroup'; import { useAppSelector } from '@/state/appTypes'; import { setDisplayUnit } from '@/state/appUiConfigs'; import { getSelectedDisplayUnit } from '@/state/appUiConfigsSelectors'; -import { getCurrentMarketConfig } from '@/state/perpetualsSelectors'; -import abacusStateManager from '@/lib/abacus'; import { getDisplayableAssetFromBaseAsset } from '@/lib/assetUtils'; +import { MustBigNumber } from '@/lib/numbers'; +import { orEmptyObj } from '@/lib/typeUtils'; type OrderbookControlsProps = { className?: string; assetId?: string; - grouping: Nullable; + grouping: GroupingMultiplier; + modifyGrouping: (increase: boolean) => void; }; -export const OrderbookControls = ({ className, assetId, grouping }: OrderbookControlsProps) => { +export const OrderbookControls = ({ + className, + assetId, + grouping, + modifyGrouping, +}: OrderbookControlsProps) => { const dispatch = useDispatch(); const displayUnit = useAppSelector(getSelectedDisplayUnit); - - const modifyScale = useCallback( - (direction: number) => { - const start = grouping?.multiplier.ordinal ?? 0; - const end = clamp(start + direction, 0, 3); - abacusStateManager.modifyOrderbookLevel( - OrderbookGrouping.values().find((v) => v.ordinal === end)! - ); - }, - [grouping?.multiplier.ordinal] + const { tickSize, tickSizeDecimals = USD_DECIMALS } = orEmptyObj( + useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) ); - const currentMarketConfig = useAppSelector(getCurrentMarketConfig, shallowEqual); - const tickSizeDecimals = currentMarketConfig?.tickSizeDecimals ?? USD_DECIMALS; + + const displayTickSize = tickSize && MustBigNumber(tickSize).times(grouping).toNumber(); + const onToggleDisplayUnit = useCallback( (newValue: DisplayUnit) => { if (!assetId) return; @@ -57,6 +56,7 @@ export const OrderbookControls = ({ className, assetId, grouping }: OrderbookCon }, [dispatch, assetId] ); + return ( <$OrderbookControlsContainer className={className}>
@@ -67,7 +67,7 @@ export const OrderbookControls = ({ className, assetId, grouping }: OrderbookCon size={ButtonSize.XSmall} shape={ButtonShape.Square} buttonStyle={ButtonStyle.WithoutBackground} - onClick={() => modifyScale(-1)} + onClick={() => modifyGrouping(false)} > - @@ -75,7 +75,7 @@ export const OrderbookControls = ({ className, assetId, grouping }: OrderbookCon size={ButtonSize.XSmall} shape={ButtonShape.Square} buttonStyle={ButtonStyle.WithoutBackground} - onClick={() => modifyScale(1)} + onClick={() => modifyGrouping(true)} > + @@ -83,7 +83,7 @@ export const OrderbookControls = ({ className, assetId, grouping }: OrderbookCon diff --git a/src/views/CanvasOrderbook/OrderbookRow.tsx b/src/views/CanvasOrderbook/OrderbookRow.tsx index 9c1954277..5be5fa5e8 100644 --- a/src/views/CanvasOrderbook/OrderbookRow.tsx +++ b/src/views/CanvasOrderbook/OrderbookRow.tsx @@ -2,25 +2,21 @@ import { forwardRef } from 'react'; import styled, { css } from 'styled-components'; -import type { Nullable } from '@/constants/abacus'; import { STRING_KEYS } from '@/constants/localization'; -import { TOKEN_DECIMALS } from '@/constants/numbers'; import { ORDERBOOK_ROW_HEIGHT } from '@/constants/orderbook'; import { useStringGetter } from '@/hooks/useStringGetter'; import { Output, OutputType } from '@/components/Output'; -import { useAppSelector } from '@/state/appTypes'; -import { getCurrentMarketMidMarketPriceWithOraclePriceFallback } from '@/state/perpetualsSelectors'; - type StyleProps = { side?: 'top' | 'bottom'; isHeader?: boolean; }; type ElementProps = { - tickSizeDecimals?: Nullable; + midMarketPrice?: number; + tickSizeDecimals?: number; }; export const OrderbookRow = styled.div<{ isHeader?: boolean }>` @@ -51,11 +47,8 @@ export const OrderbookRow = styled.div<{ isHeader?: boolean }>` `; export const OrderbookMiddleRow = forwardRef( - ({ side, isHeader, tickSizeDecimals = TOKEN_DECIMALS }, ref) => { + ({ side, isHeader, midMarketPrice, tickSizeDecimals }, ref) => { const stringGetter = useStringGetter(); - const orderbookMidMarketPrice = useAppSelector( - getCurrentMarketMidMarketPriceWithOraclePriceFallback - ); return ( <$OrderbookMiddleRow ref={ref} side={side} isHeader={isHeader}> @@ -64,7 +57,7 @@ export const OrderbookMiddleRow = forwardRef From e154ebedd4ce71b0e5595570f3f3980bc69a7a11 Mon Sep 17 00:00:00 2001 From: jaredvu Date: Tue, 28 Jan 2025 14:13:35 -0800 Subject: [PATCH 03/13] fix calculator again --- src/bonsai/calculators/orderbook.ts | 282 +++++++++++++++++----------- src/bonsai/selectors/summary.ts | 8 +- 2 files changed, 176 insertions(+), 114 deletions(-) diff --git a/src/bonsai/calculators/orderbook.ts b/src/bonsai/calculators/orderbook.ts index 18c60f1f9..62303f014 100644 --- a/src/bonsai/calculators/orderbook.ts +++ b/src/bonsai/calculators/orderbook.ts @@ -1,4 +1,5 @@ import BigNumber from 'bignumber.js'; +import { orderBy } from 'lodash'; import { weakMapMemoize } from 'reselect'; import { GroupingMultiplier } from '@/constants/orderbook'; @@ -6,6 +7,7 @@ import { GroupingMultiplier } from '@/constants/orderbook'; import { isTruthy } from '@/lib/isTruthy'; import { BIG_NUMBERS, MustBigNumber, roundToNearestFactor } from '@/lib/numbers'; import { objectEntries } from '@/lib/objectHelpers'; +import { orEmptyRecord } from '@/lib/typeUtils'; import { OrderbookData } from '../types/rawTypes'; import { OrderbookLine } from '../types/summaryTypes'; @@ -19,6 +21,8 @@ type OrderbookLineBN = { depthCost: BigNumber; }; +type RawOrderbookLineBN = Omit; + export const calculateOrderbook = weakMapMemoize((orderbook: OrderbookData | undefined) => { if (orderbook == null) { return undefined; @@ -31,43 +35,154 @@ export const calculateOrderbook = weakMapMemoize((orderbook: OrderbookData | und * 2. filter out lines with size <= 0 * 3. sort by price: asks ascending, bids descending */ - const asksBase: Omit[] = objectEntries(asks) - .map(mapRawOrderbookLineToBN) - .filter(isTruthy) - .sort((a, b) => a.price.minus(b.price).toNumber()); - - const bidsBase: Omit[] = objectEntries(bids) - .map(mapRawOrderbookLineToBN) - .filter(isTruthy) - .sort((a, b) => b.price.minus(a.price).toNumber()); + const sortedAsks: RawOrderbookLineBN[] = orderBy( + objectEntries(asks).map(mapRawOrderbookLineToBN).filter(isTruthy), + priceBNToNumber, + 'asc' + ); - // calculate depth and depthCost - const asksComposite = calculateDepthAndDepthCost(asksBase); - const bidsComposite = calculateDepthAndDepthCost(bidsBase); + const sortedBids: RawOrderbookLineBN[] = orderBy( + objectEntries(bids).map(mapRawOrderbookLineToBN).filter(isTruthy), + priceBNToNumber, + 'desc' + ); // un-cross orderbook - const uncrossedBook: { asks: OrderbookLineBN[]; bids: OrderbookLineBN[] } = uncrossOrderbook( - asksComposite, - bidsComposite - ); + const { + asks: uncrossedAsks, + bids: uncrossedBids, + }: { + asks: RawOrderbookLineBN[]; + bids: RawOrderbookLineBN[]; + } = uncrossOrderbook(sortedAsks, sortedBids); + + // calculate depth and depthCost + const processedAsks: OrderbookLineBN[] = calculateDepthAndDepthCost(uncrossedAsks); + const processedBids: OrderbookLineBN[] = calculateDepthAndDepthCost(uncrossedBids); // calculate midPrice, spread, and spreadPercent - const lowestAsk = uncrossedBook.asks.at(0); - const highestBid = uncrossedBook.bids.at(-1); + const lowestAsk = processedAsks.at(0); + 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); return { - bids: uncrossedBook.bids, - asks: uncrossedBook.asks, + bids: processedBids, + asks: processedAsks, midPrice: midPriceBN, spread: spreadBN, spreadPercent: spreadPercentBN, }; }); -function uncrossOrderbook(asks: OrderbookLineBN[], bids: OrderbookLineBN[]) { +/** + * @description: final formatting for displaying the orderbook data. + * @returns asks and bids in a sorted list or a map, along with spread, spreadPercent, and midPrice + */ +export const formatOrderbook = weakMapMemoize( + ( + currentMarketOrderbook: ReturnType, + groupingMultiplier: GroupingMultiplier, + tickSize: string + ) => { + if (currentMarketOrderbook == null) { + return undefined; + } + + // If groupingMultiplier is ONE, return the orderbook as is. + if (groupingMultiplier === GroupingMultiplier.ONE) { + return { + asks: orderBy(currentMarketOrderbook.asks.map(mapOrderbookLineToNumber), 'price', 'desc'), + bids: currentMarketOrderbook.bids.map(mapOrderbookLineToNumber), + asksMap: Object.fromEntries( + currentMarketOrderbook.asks.map((line) => [ + line.price.toString(), + mapOrderbookLineToNumber(line), + ]) + ), + bidsMap: Object.fromEntries( + currentMarketOrderbook.bids.map((line) => [ + line.price.toString(), + mapOrderbookLineToNumber(line), + ]) + ), + spread: currentMarketOrderbook.spread?.toNumber(), + spreadPercent: currentMarketOrderbook.spreadPercent?.toNumber(), + midPrice: currentMarketOrderbook.midPrice?.toNumber(), + }; + } + + const groupingTickSize = 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 asksList = orderBy( + Object.values(groupedAsks).map(mapOrderbookLineToNumber), + 'price', + 'desc' + ); + const bidsList = orderBy( + Object.values(groupedBids).map(mapOrderbookLineToNumber), + 'price', + 'desc' + ); + + const lowestAsk = asksList.at(-1); + const highestBid = bidsList.at(0); + + return { + asks: asksList, + asksMap: groupedAsks, + bids: bidsList, + bidsMap: groupedBids, + spread: currentMarketOrderbook.spread?.toNumber(), + spreadPercent: currentMarketOrderbook.spreadPercent?.toNumber(), + midPrice: roundMidPrice(lowestAsk, highestBid, groupingTickSize)?.toNumber(), + }; + } +); + +/** ------ .map Helper Functions ------ */ +function mapRawOrderbookLineToBN( + rawOrderbookLineEntry: [string, { size: string; offset: number }] +) { + const [price, { size, offset }] = rawOrderbookLineEntry; + const sizeBN = MustBigNumber(size); + const priceBN = MustBigNumber(price); + + if (sizeBN.isZero()) return undefined; + + return { + price: priceBN, + size: sizeBN, + sizeCost: priceBN.times(sizeBN), + offset, + }; +} + +function mapOrderbookLineToNumber(orderbookLineBN: OrderbookLineBN): OrderbookLine { + const { price, size, sizeCost, depth, depthCost, offset } = orderbookLineBN; + + return { + price: price.toNumber(), + size: size.toNumber(), + sizeCost: sizeCost.toNumber(), + offset, + depth: depth.toNumber(), + depthCost: depthCost.toNumber(), + }; +} + +function priceBNToNumber({ price }: Pick) { + return price.toNumber(); +} + +/** ------ UnCrossing functions ------ */ + +function uncrossOrderbook(asks: RawOrderbookLineBN[], bids: RawOrderbookLineBN[]) { if (asks.length === 0 || bids.length === 0) { return { asks, bids }; } @@ -75,32 +190,32 @@ function uncrossOrderbook(asks: OrderbookLineBN[], bids: OrderbookLineBN[]) { const asksCopy = [...asks]; const bidsCopy = [...bids]; - let ask = asksCopy.at(-1); - let bid = bidsCopy.at(0); + let lowestAsk = asksCopy.at(0); + let highestBid = bidsCopy.at(0); - while (ask && bid && isCrossed(ask, bid)) { - if (ask.offset === bid.offset) { + while (lowestAsk && highestBid && isCrossed(lowestAsk, highestBid)) { + if (lowestAsk.offset === highestBid.offset) { // If offsets are the same, give precedence to the larger size. In this case, // one of the sizes "should" be zero, but we simply check for the larger size. - if (ask.size.gte(bid.size)) { + if (lowestAsk.size.gte(highestBid.size)) { // remove the bid bidsCopy.shift(); - bid = bidsCopy.at(0); + highestBid = bidsCopy.at(0); } else { // remove the ask - asksCopy.pop(); - ask = asksCopy.at(-1); + asksCopy.shift(); + lowestAsk = asksCopy.at(0); } } else { // If offsets are different, remove the older offset. - if (ask.offset < bid.offset) { + if (lowestAsk.offset < highestBid.offset) { // remove the ask - asksCopy.pop(); - ask = asksCopy.at(-1); + asksCopy.shift(); + lowestAsk = asksCopy.at(0); } else { // remove the bid bidsCopy.shift(); - bid = bidsCopy.at(0); + highestBid = bidsCopy.at(0); } } } @@ -108,6 +223,12 @@ function uncrossOrderbook(asks: OrderbookLineBN[], bids: OrderbookLineBN[]) { return { asks: asksCopy, bids: bidsCopy }; } +function isCrossed(ask: RawOrderbookLineBN, bid: RawOrderbookLineBN) { + return ask.price.lte(bid.price); +} + +/** ------ Depth and DepthCost Calculation ------ */ + function calculateDepthAndDepthCost( lines: Omit[] ): OrderbookLineBN[] { @@ -126,39 +247,7 @@ function calculateDepthAndDepthCost( }); } -function mapRawOrderbookLineToBN( - rawOrderbookLineEntry: [string, { size: string; offset: number }] -) { - const [price, { size, offset }] = rawOrderbookLineEntry; - const sizeBN = MustBigNumber(size); - const priceBN = MustBigNumber(price); - - if (sizeBN.isZero()) return undefined; - - return { - price: priceBN, - size: sizeBN, - sizeCost: priceBN.times(sizeBN), - offset, - }; -} - -export function mapOrderbookLineToNumber(orderbookLineBN: OrderbookLineBN): OrderbookLine { - const { price, size, sizeCost, depth, depthCost, offset } = orderbookLineBN; - - return { - price: price.toNumber(), - size: size.toNumber(), - sizeCost: sizeCost.toNumber(), - offset, - depth: depth.toNumber(), - depthCost: depthCost.toNumber(), - }; -} - -function isCrossed(ask: OrderbookLineBN, bid: OrderbookLineBN) { - return ask.price.lte(bid.price); -} +/** ------ Grouping Helpers ------ */ const getGroupingTickSize = (tickSize: string, multiplier: GroupingMultiplier) => { return MustBigNumber(tickSize).times(multiplier).toNumber(); @@ -169,19 +258,15 @@ const getGroupingTickSize = (tickSize: string, multiplier: GroupingMultiplier) = * @param orderbook * @param groupingTickSize * @param shouldFloor we want to round asks up and bids down so they don't have an overlapping group in the middle - * @returns grouped orderbook side or null + * @returns Grouped OrderbookLines in a dictionary using price as key */ const group = weakMapMemoize( - ( - orderbook: OrderbookLineBN[], - groupingTickSize: number, - shouldFloor?: boolean - ): OrderbookLineBN[] | null => { + (orderbook: OrderbookLineBN[], groupingTickSize: number, shouldFloor?: boolean) => { if (orderbook.length === 0) { return null; } - const result: OrderbookLineBN[] = []; + const mapResult: Record = {}; orderbook.forEach((line) => { const price = roundToNearestFactor({ @@ -190,7 +275,8 @@ const group = weakMapMemoize( roundingMode: shouldFloor ? BigNumber.ROUND_DOWN : BigNumber.ROUND_UP, }); - const existingLine = result.find((r) => r.price.eq(price)); + const key = price.toString(); + const existingLine = mapResult[key]; if (existingLine) { existingLine.size = existingLine.size.plus(line.size); @@ -200,26 +286,26 @@ const group = weakMapMemoize( } else { // Asks are ascending and bids are descending, so we need to unshift asks and push bids if (shouldFloor) { - result.push({ + mapResult[key] = { ...line, price, - }); + }; } else { - result.unshift({ + mapResult[key] = { ...line, price, - }); + }; } } }); - return result; + return mapResult; } ); function roundMidPrice( - lowestAsk: OrderbookLineBN | undefined, - highestBid: OrderbookLineBN | undefined, + lowestAsk: OrderbookLine | undefined, + highestBid: OrderbookLine | undefined, groupingTickSize: number ) { if (!lowestAsk || !highestBid) { @@ -227,43 +313,17 @@ function roundMidPrice( } const roundedMidPrice = roundToNearestFactor({ - number: lowestAsk.price.plus(highestBid.price).div(2), + number: MustBigNumber(lowestAsk.price).plus(highestBid.price).div(2), factor: groupingTickSize, roundingMode: BigNumber.ROUND_HALF_UP, }); - return roundedMidPrice.eq(lowestAsk!.price) || roundedMidPrice.eq(highestBid!.price) + // Reduce precision if midPrice is equal to lowestAsk or highestBid + return roundedMidPrice.eq(lowestAsk.price) || roundedMidPrice.eq(highestBid.price) ? roundToNearestFactor({ - number: lowestAsk!.price.plus(highestBid!.price).div(2), + number: MustBigNumber(lowestAsk.price).plus(highestBid.price).div(2), factor: groupingTickSize / 10, roundingMode: BigNumber.ROUND_HALF_UP, }) : roundedMidPrice; } - -export const groupOrderbook = weakMapMemoize( - ( - currentMarketOrderbook: ReturnType, - groupingMultiplier: GroupingMultiplier, - tickSize: string - ) => { - if (currentMarketOrderbook == null) { - return undefined; - } - - const groupingTickSize = getGroupingTickSize(tickSize, groupingMultiplier); - const { asks, bids } = currentMarketOrderbook; - const groupedAsks = group(asks, groupingTickSize); - const groupedBids = group(bids, groupingTickSize, true); - const lowestAsk = groupedAsks?.at(-1); - const highestBid = groupedBids?.at(0); - - return { - asks: groupedAsks?.map(mapOrderbookLineToNumber) ?? [], - bids: groupedBids?.map(mapOrderbookLineToNumber) ?? [], - spread: currentMarketOrderbook.spread?.toNumber(), - spreadPercent: currentMarketOrderbook.spreadPercent?.toNumber(), - midPrice: roundMidPrice(lowestAsk, highestBid, groupingTickSize)?.toNumber(), - }; - } -); diff --git a/src/bonsai/selectors/summary.ts b/src/bonsai/selectors/summary.ts index d512bdca4..354b14555 100644 --- a/src/bonsai/selectors/summary.ts +++ b/src/bonsai/selectors/summary.ts @@ -1,12 +1,14 @@ import { omit } from 'lodash'; import { shallowEqual } from 'react-redux'; +import { GroupingMultiplier } from '@/constants/orderbook'; + import { createAppSelector } from '@/state/appTypes'; import { getFavoritedMarkets } from '@/state/appUiConfigsSelectors'; import { getCurrentMarketId } from '@/state/perpetualsSelectors'; import { createMarketSummary } from '../calculators/markets'; -import { calculateOrderbook, groupOrderbook } from '../calculators/orderbook'; +import { calculateOrderbook, formatOrderbook } from '../calculators/orderbook'; import { mergeLoadableStatus } from '../lib/mapLoadable'; import { PerpetualMarketSummary } from '../types/summaryTypes'; import { selectAllAssetsInfo } from './assets'; @@ -89,7 +91,7 @@ export const createSelectCurrentMarketGroupedOrderbook = () => [ selectCurrentMarketOrderbook, selectCurrentMarketInfoStable, - (_s, groupingMultiplier?: number) => groupingMultiplier ?? 1, + (_s, groupingMultiplier?: GroupingMultiplier) => groupingMultiplier ?? GroupingMultiplier.ONE, ], (currentMarketOrderbook, currentMarketStableInfo, groupingMultiplier) => { if (currentMarketOrderbook == null || currentMarketStableInfo == null) { @@ -97,6 +99,6 @@ export const createSelectCurrentMarketGroupedOrderbook = () => } const { tickSize } = currentMarketStableInfo; const orderbook = calculateOrderbook(currentMarketOrderbook.data); - return groupOrderbook(orderbook, groupingMultiplier, tickSize); + return formatOrderbook(orderbook, groupingMultiplier, tickSize); } ); From 43b12a528ac3b68d97908167402b52e5dde2f0b8 Mon Sep 17 00:00:00 2001 From: jaredvu Date: Tue, 28 Jan 2025 14:30:41 -0800 Subject: [PATCH 04/13] remove shouldFloor if statement --- src/bonsai/calculators/orderbook.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/bonsai/calculators/orderbook.ts b/src/bonsai/calculators/orderbook.ts index 62303f014..91aaacd8b 100644 --- a/src/bonsai/calculators/orderbook.ts +++ b/src/bonsai/calculators/orderbook.ts @@ -284,18 +284,10 @@ const group = weakMapMemoize( existingLine.depth = line.depth; existingLine.depthCost = line.depthCost; } else { - // Asks are ascending and bids are descending, so we need to unshift asks and push bids - if (shouldFloor) { - mapResult[key] = { - ...line, - price, - }; - } else { - mapResult[key] = { - ...line, - price, - }; - } + mapResult[key] = { + ...line, + price, + }; } }); From 0a2456ca3150a1f8965a5e0604b0fd5dbed3760d Mon Sep 17 00:00:00 2001 From: jaredvu Date: Tue, 28 Jan 2025 14:42:25 -0800 Subject: [PATCH 05/13] mid market price --- src/bonsai/ontology.ts | 5 ++++- src/views/MidMarketPrice.tsx | 33 +++++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/bonsai/ontology.ts b/src/bonsai/ontology.ts index 1ca69c475..1e312103b 100644 --- a/src/bonsai/ontology.ts +++ b/src/bonsai/ontology.ts @@ -180,7 +180,10 @@ interface BonsaiHelpersShape { fills: BasicSelector; }; orderbook: { - createSelectGroupedData: ParameterizedSelector; + createSelectGroupedData: ParameterizedSelector< + OrderbookProcessedData | undefined, + [number | undefined] + >; loading: BasicSelector; }; }; diff --git a/src/views/MidMarketPrice.tsx b/src/views/MidMarketPrice.tsx index 7e583d6e4..2bfa4a542 100644 --- a/src/views/MidMarketPrice.tsx +++ b/src/views/MidMarketPrice.tsx @@ -1,22 +1,21 @@ import { useEffect, useRef } from 'react'; -import { shallowEqual } from 'react-redux'; +import { BonsaiHelpers } from '@/bonsai/ontology'; import styled, { css } from 'styled-components'; import { Nullable } from '@/constants/abacus'; +import { useParameterizedSelector } from '@/hooks/useParameterizedSelector'; + import { layoutMixins } from '@/styles/layoutMixins'; import { LoadingDots } from '@/components/Loading/LoadingDots'; import { Output, OutputType } from '@/components/Output'; import { useAppSelector } from '@/state/appTypes'; -import { - getCurrentMarketConfig, - getCurrentMarketMidMarketPrice, -} from '@/state/perpetualsSelectors'; import { MustBigNumber } from '@/lib/numbers'; +import { orEmptyObj } from '@/lib/typeUtils'; const getMidMarketPriceColor = ({ midMarketPrice, @@ -36,8 +35,19 @@ const getMidMarketPriceColor = ({ }; export const MidMarketPrice = () => { - const { tickSizeDecimals } = useAppSelector(getCurrentMarketConfig, shallowEqual) ?? {}; - const midMarketPrice = useAppSelector(getCurrentMarketMidMarketPrice); + const { tickSizeDecimals } = orEmptyObj( + useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) + ); + + const midMarketPriceLoading = ['pending', 'idle'].includes( + useAppSelector(BonsaiHelpers.currentMarket.orderbook.loading) + ); + + const midMarketPrice = useParameterizedSelector( + BonsaiHelpers.currentMarket.orderbook.createSelectGroupedData, + undefined + )?.midPrice; + const lastMidMarketPrice = useRef(midMarketPrice); const midMarketColor = getMidMarketPriceColor({ @@ -49,7 +59,11 @@ export const MidMarketPrice = () => { lastMidMarketPrice.current = midMarketPrice; }, [midMarketPrice]); - return midMarketPrice !== undefined ? ( + if (midMarketPriceLoading) { + return ; + } + + return ( <$Output withSubscript type={OutputType.Fiat} @@ -57,10 +71,9 @@ export const MidMarketPrice = () => { color={midMarketColor} fractionDigits={tickSizeDecimals} /> - ) : ( - ); }; + const $Output = styled(Output)<{ color?: string }>` ${layoutMixins.row} From d582ad0263f4d380fe7640f5177204ca6230d64e Mon Sep 17 00:00:00 2001 From: jaredvu Date: Tue, 28 Jan 2025 15:17:37 -0800 Subject: [PATCH 06/13] fix --- src/bonsai/calculators/orderbook.ts | 73 +++++++++++++++-------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/src/bonsai/calculators/orderbook.ts b/src/bonsai/calculators/orderbook.ts index 91aaacd8b..8bb62d4c7 100644 --- a/src/bonsai/calculators/orderbook.ts +++ b/src/bonsai/calculators/orderbook.ts @@ -2,6 +2,7 @@ import BigNumber from 'bignumber.js'; import { orderBy } from 'lodash'; import { weakMapMemoize } from 'reselect'; +import { SMALL_USD_DECIMALS } from '@/constants/numbers'; import { GroupingMultiplier } from '@/constants/orderbook'; import { isTruthy } from '@/lib/isTruthy'; @@ -92,18 +93,24 @@ export const formatOrderbook = weakMapMemoize( // If groupingMultiplier is ONE, return the orderbook as is. if (groupingMultiplier === GroupingMultiplier.ONE) { + const tickSizeDecimals = MustBigNumber(tickSize).dp() ?? SMALL_USD_DECIMALS; + return { - asks: orderBy(currentMarketOrderbook.asks.map(mapOrderbookLineToNumber), 'price', 'desc'), + asks: orderBy( + currentMarketOrderbook.asks.map(mapOrderbookLineToNumber), + (ask) => ask.price, + 'desc' + ), bids: currentMarketOrderbook.bids.map(mapOrderbookLineToNumber), asksMap: Object.fromEntries( currentMarketOrderbook.asks.map((line) => [ - line.price.toString(), + line.price.toFixed(tickSizeDecimals), mapOrderbookLineToNumber(line), ]) ), bidsMap: Object.fromEntries( currentMarketOrderbook.bids.map((line) => [ - line.price.toString(), + line.price.toFixed(tickSizeDecimals), mapOrderbookLineToNumber(line), ]) ), @@ -121,12 +128,12 @@ export const formatOrderbook = weakMapMemoize( // Convert groupedAsks and groupedBids to list and sort by price. Asks will now be descending and bids will be remain descending const asksList = orderBy( Object.values(groupedAsks).map(mapOrderbookLineToNumber), - 'price', + (ask) => ask.price, 'desc' ); const bidsList = orderBy( Object.values(groupedBids).map(mapOrderbookLineToNumber), - 'price', + (bid) => bid.price, 'desc' ); @@ -260,40 +267,38 @@ 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 = weakMapMemoize( - (orderbook: OrderbookLineBN[], groupingTickSize: number, shouldFloor?: boolean) => { - if (orderbook.length === 0) { - return null; - } +const group = (orderbook: OrderbookLineBN[], groupingTickSize: number, shouldFloor?: boolean) => { + if (orderbook.length === 0) { + return null; + } - const mapResult: Record = {}; + const mapResult: Record = {}; - orderbook.forEach((line) => { - const price = roundToNearestFactor({ - number: line.price, - factor: groupingTickSize, - roundingMode: shouldFloor ? BigNumber.ROUND_DOWN : BigNumber.ROUND_UP, - }); + orderbook.forEach((line) => { + const price = roundToNearestFactor({ + number: line.price, + factor: groupingTickSize, + roundingMode: shouldFloor ? BigNumber.ROUND_DOWN : BigNumber.ROUND_UP, + }); - const key = price.toString(); - const existingLine = mapResult[key]; + const key = price.toString(); + const existingLine = mapResult[key]; - if (existingLine) { - existingLine.size = existingLine.size.plus(line.size); - existingLine.sizeCost = existingLine.sizeCost.plus(line.sizeCost); - existingLine.depth = line.depth; - existingLine.depthCost = line.depthCost; - } else { - mapResult[key] = { - ...line, - price, - }; - } - }); + if (existingLine) { + existingLine.size = existingLine.size.plus(line.size); + existingLine.sizeCost = existingLine.sizeCost.plus(line.sizeCost); + existingLine.depth = line.depth; + existingLine.depthCost = line.depthCost; + } else { + mapResult[key] = { + ...line, + price, + }; + } + }); - return mapResult; - } -); + return mapResult; +}; function roundMidPrice( lowestAsk: OrderbookLine | undefined, From 706d55af3779066f727000295010c7e29ea23ac0 Mon Sep 17 00:00:00 2001 From: jaredvu Date: Wed, 29 Jan 2025 13:28:06 -0800 Subject: [PATCH 07/13] new selectors for components --- src/bonsai/calculators/orderbook.ts | 157 +++++++++++++----- src/bonsai/ontology.ts | 30 +++- src/bonsai/selectors/orderbook.ts | 142 ++++++++++++++++ src/bonsai/selectors/summary.ts | 26 +-- src/bonsai/types/orderbookTypes.ts | 39 +++++ src/bonsai/types/summaryTypes.ts | 24 +-- src/hooks/Orderbook/useDrawOrderbook.ts | 4 + src/hooks/Orderbook/useOrderbookValues.ts | 108 +++--------- src/views/CanvasOrderbook/CanvasOrderbook.tsx | 4 +- src/views/MidMarketPrice.tsx | 25 ++- src/views/charts/DepthChart/Tooltip.tsx | 23 +-- src/views/charts/DepthChart/index.tsx | 82 ++++----- 12 files changed, 424 insertions(+), 240 deletions(-) create mode 100644 src/bonsai/selectors/orderbook.ts create mode 100644 src/bonsai/types/orderbookTypes.ts diff --git a/src/bonsai/calculators/orderbook.ts b/src/bonsai/calculators/orderbook.ts index 8bb62d4c7..b0b5f8a0a 100644 --- a/src/bonsai/calculators/orderbook.ts +++ b/src/bonsai/calculators/orderbook.ts @@ -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 } from '../types/orderbookTypes'; import { OrderbookData } from '../types/rawTypes'; -import { OrderbookLine } from '../types/summaryTypes'; +import { SubaccountOpenOrderPriceMap, SubaccountOrder } from '../types/summaryTypes'; type OrderbookLineBN = { price: BigNumber; @@ -24,6 +26,12 @@ type OrderbookLineBN = { type RawOrderbookLineBN = Omit; +export type OrderbookProcessingOptions = { + groupingMultiplier?: GroupingMultiplier; + asksSortOrder?: 'asc' | 'desc'; + bidsSortOrder?: 'asc' | 'desc'; +}; + export const calculateOrderbook = weakMapMemoize((orderbook: OrderbookData | undefined) => { if (orderbook == null) { return undefined; @@ -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 + asks: processedAsks, // ascending asks midPrice: midPriceBN, spread: spreadBN, spreadPercent: spreadPercentBN, @@ -84,57 +92,68 @@ export const calculateOrderbook = weakMapMemoize((orderbook: OrderbookData | und export const formatOrderbook = weakMapMemoize( ( currentMarketOrderbook: ReturnType, - 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)); + + 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. Asks will now be descending and bids will be remain 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); @@ -142,9 +161,7 @@ export const formatOrderbook = weakMapMemoize( return { asks: asksList, - asksMap: groupedAsks, bids: bidsList, - bidsMap: groupedBids, spread: currentMarketOrderbook.spread?.toNumber(), spreadPercent: currentMarketOrderbook.spreadPercent?.toNumber(), midPrice: roundMidPrice(lowestAsk, highestBid, groupingTickSize)?.toNumber(), @@ -153,6 +170,7 @@ export const formatOrderbook = weakMapMemoize( ); /** ------ .map Helper Functions ------ */ + function mapRawOrderbookLineToBN( rawOrderbookLineEntry: [string, { size: string; offset: number }] ) { @@ -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, + }; +} /** * @@ -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; } @@ -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) { @@ -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(); +} diff --git a/src/bonsai/ontology.ts b/src/bonsai/ontology.ts index 10032ac81..56540bbf0 100644 --- a/src/bonsai/ontology.ts +++ b/src/bonsai/ontology.ts @@ -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'; @@ -45,7 +47,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, @@ -54,6 +60,7 @@ import { StablePerpetualMarketSummary, } from './selectors/summary'; import { selectUserStats } from './selectors/userStats'; +import { DepthChartData, OrderbookProcessedData } from './types/orderbookTypes'; import { AllAssetData, ApiState, @@ -61,7 +68,6 @@ import { EquityTiersSummary, FeeTierSummary, GroupedSubaccountSummary, - OrderbookProcessedData, PendingIsolatedPosition, PerpetualMarketSummaries, PerpetualMarketSummary, @@ -201,10 +207,18 @@ interface BonsaiHelpersShape { orderbook: { createSelectGroupedData: ParameterizedSelector< OrderbookProcessedData | undefined, - [number | undefined] + [GroupingMultiplier | undefined] >; loading: BasicSelector; }; + midPrice: { + data: BasicSelector; + loading: BasicSelector; + }; + depthChart: { + data: BasicSelector; + loading: BasicSelector; + }; }; assets: { createSelectAssetInfo: ParameterizedSelector; @@ -237,7 +251,15 @@ export const BonsaiHelpers: BonsaiHelpersShape = { marketInfo: selectCurrentMarketInfo, stableMarketInfo: selectCurrentMarketInfoStable, orderbook: { - createSelectGroupedData: createSelectCurrentMarketGroupedOrderbook, + createSelectGroupedData: createSelectCurrentMarketOrderbook, + loading: selectCurrentMarketOrderbookLoading, + }, + midPrice: { + data: selectCurrentMarketMidPrice, + loading: selectCurrentMarketOrderbookLoading, + }, + depthChart: { + data: selectCurrentMarketDepthChart, loading: selectCurrentMarketOrderbookLoading, }, account: { diff --git a/src/bonsai/selectors/orderbook.ts b/src/bonsai/selectors/orderbook.ts new file mode 100644 index 000000000..bd3b2e265 --- /dev/null +++ b/src/bonsai/selectors/orderbook.ts @@ -0,0 +1,142 @@ +import { DepthChartSeries } from '@/constants/charts'; +import { EMPTY_ARR } from '@/constants/objects'; +import { GroupingMultiplier } from '@/constants/orderbook'; +import { IndexerOrderSide } from '@/types/indexer/indexerApiGen'; + +import { createAppSelector } from '@/state/appTypes'; + +import { orEmptyObj } from '@/lib/typeUtils'; + +import { + calculateOrderbook, + findMine, + formatOrderbook, + getGroupingTickSize, + getSubaccountOpenOrdersPriceMap, +} from '../calculators/orderbook'; +import { selectCurrentMarketOpenOrders } from './account'; +import { selectCurrentMarketOrderbook } from './markets'; +import { selectCurrentMarketInfoStable } from './summary'; + +export const selectCurrentMarketMidPrice = createAppSelector( + [selectCurrentMarketOrderbook], + (orderbook) => { + if (orderbook == null) { + return undefined; + } + + return calculateOrderbook(orderbook.data)?.midPrice; + } +); + +export const createSelectCurrentMarketOrderbook = () => + createAppSelector( + [ + selectCurrentMarketOrderbook, + selectCurrentMarketInfoStable, + selectCurrentMarketOpenOrders, + (_s, groupingMultiplier?: GroupingMultiplier) => groupingMultiplier, + ], + (orderbook, stableInfo, openOrders, groupingMultiplier) => { + if (orderbook == null || stableInfo == null) { + return undefined; + } + + const orderbookBN = calculateOrderbook(orderbook.data); + const { groupingTickSize, groupingTickSizeDecimals } = getGroupingTickSize( + stableInfo.tickSize, + groupingMultiplier ?? GroupingMultiplier.ONE + ); + + const subaccountOpenOrdersPriceMap = getSubaccountOpenOrdersPriceMap( + openOrders, + groupingTickSize, + groupingTickSizeDecimals + ); + + const formattedOrderbook = formatOrderbook(orderbookBN, stableInfo.tickSize, { + groupingMultiplier, + asksSortOrder: 'asc', + }); + + const bids = + formattedOrderbook?.bids.map((line) => ({ + ...line, + side: 'bid', + mine: findMine({ + price: line.price, + side: IndexerOrderSide.BUY, + orderMap: subaccountOpenOrdersPriceMap, + groupingTickSize, + groupingTickSizeDecimals, + }), + })) ?? EMPTY_ARR; + + const asks = + formattedOrderbook?.asks.map((line) => ({ + ...line, + side: 'ask', + mine: findMine({ + price: line.price, + side: IndexerOrderSide.SELL, + orderMap: subaccountOpenOrdersPriceMap, + groupingTickSize, + groupingTickSizeDecimals, + }), + })) ?? EMPTY_ARR; + + const { midPrice, spread, spreadPercent } = orEmptyObj(formattedOrderbook); + + return { + bids, + asks, + midPrice, + spread, + spreadPercent, + }; + } + ); + +export const selectCurrentMarketDepthChart = createAppSelector( + [selectCurrentMarketOrderbook, selectCurrentMarketInfoStable], + (orderbook, stableInfo) => { + if (orderbook == null || stableInfo == null) { + return undefined; + } + const { tickSize } = stableInfo; + const calculatedOrderbook = calculateOrderbook(orderbook.data); + const formattedOrderbook = formatOrderbook(calculatedOrderbook, tickSize, { + groupingMultiplier: GroupingMultiplier.ONE, + asksSortOrder: 'asc', + bidsSortOrder: 'asc', + }); + + const asks = + formattedOrderbook?.asks.map((datum) => ({ + ...datum, + seriesKey: DepthChartSeries.Asks, + })) ?? EMPTY_ARR; + const bids = + formattedOrderbook?.bids.map((datum) => ({ + ...datum, + seriesKey: DepthChartSeries.Bids, + })) ?? EMPTY_ARR; + + const lowestBid = bids.at(0); + const highestBid = bids.at(-1); + const lowestAsk = asks.at(0); + const highestAsk = asks.at(-1); + + return { + bids, + asks, + lowestBid, + highestBid, + lowestAsk, + highestAsk, + spread: formattedOrderbook?.spread, + spreadPercent: formattedOrderbook?.spreadPercent, + midPrice: formattedOrderbook?.midPrice, + }; + } +); diff --git a/src/bonsai/selectors/summary.ts b/src/bonsai/selectors/summary.ts index 354b14555..4033e6daa 100644 --- a/src/bonsai/selectors/summary.ts +++ b/src/bonsai/selectors/summary.ts @@ -1,23 +1,16 @@ import { omit } from 'lodash'; import { shallowEqual } from 'react-redux'; -import { GroupingMultiplier } from '@/constants/orderbook'; - import { createAppSelector } from '@/state/appTypes'; import { getFavoritedMarkets } from '@/state/appUiConfigsSelectors'; import { getCurrentMarketId } from '@/state/perpetualsSelectors'; import { createMarketSummary } from '../calculators/markets'; -import { calculateOrderbook, formatOrderbook } from '../calculators/orderbook'; import { mergeLoadableStatus } from '../lib/mapLoadable'; import { PerpetualMarketSummary } from '../types/summaryTypes'; import { selectAllAssetsInfo } from './assets'; import { selectRawAssets, selectRawMarkets } from './base'; -import { - selectAllMarketsInfo, - selectCurrentMarketOrderbook, - selectSparkLinesData, -} from './markets'; +import { selectAllMarketsInfo, selectSparkLinesData } from './markets'; export const selectAllMarketSummariesLoading = createAppSelector( [selectRawMarkets, selectRawAssets], @@ -85,20 +78,3 @@ export const createSelectMarketSummaryById = () => return allSummaries?.[marketId]; } ); - -export const createSelectCurrentMarketGroupedOrderbook = () => - createAppSelector( - [ - selectCurrentMarketOrderbook, - selectCurrentMarketInfoStable, - (_s, groupingMultiplier?: GroupingMultiplier) => groupingMultiplier ?? GroupingMultiplier.ONE, - ], - (currentMarketOrderbook, currentMarketStableInfo, groupingMultiplier) => { - if (currentMarketOrderbook == null || currentMarketStableInfo == null) { - return undefined; - } - const { tickSize } = currentMarketStableInfo; - const orderbook = calculateOrderbook(currentMarketOrderbook.data); - return formatOrderbook(orderbook, groupingMultiplier, tickSize); - } - ); diff --git a/src/bonsai/types/orderbookTypes.ts b/src/bonsai/types/orderbookTypes.ts new file mode 100644 index 000000000..b4d85dea8 --- /dev/null +++ b/src/bonsai/types/orderbookTypes.ts @@ -0,0 +1,39 @@ +import { DepthChartSeries } from '@/constants/charts'; + +export type OrderbookLine = { + price: number; + size: number; + depth: number; + sizeCost: number; + depthCost: number; + offset: number; +}; + +export type OrderbookProcessedData = { + asks: OrderbookLine[] | CanvasOrderbookLine[]; + bids: OrderbookLine[] | CanvasOrderbookLine[]; + midPrice: number | undefined; + spread: number | undefined; + spreadPercent: number | undefined; +}; + +export type CanvasOrderbookLine = OrderbookLine & { + mine: number | undefined; + side: 'ask' | 'bid'; +}; + +type DepthChartDatum = OrderbookLine & { + seriesKey: DepthChartSeries; +}; + +export type DepthChartData = { + asks: DepthChartDatum[]; + bids: DepthChartDatum[]; + midPrice: number | undefined; + spread: number | undefined; + spreadPercent: number | undefined; + lowestBid: DepthChartDatum | undefined; + highestBid: DepthChartDatum | undefined; + lowestAsk: DepthChartDatum | undefined; + highestAsk: DepthChartDatum | undefined; +}; diff --git a/src/bonsai/types/summaryTypes.ts b/src/bonsai/types/summaryTypes.ts index ac6458871..edad04325 100644 --- a/src/bonsai/types/summaryTypes.ts +++ b/src/bonsai/types/summaryTypes.ts @@ -295,19 +295,11 @@ export interface UserStats { takerVolume30D?: number; } -export type OrderbookLine = { - price: number; - size: number; - depth: number; - sizeCost: number; - depthCost: number; - offset: number; -}; - -export type OrderbookProcessedData = { - asks: OrderbookLine[]; - bids: OrderbookLine[]; - midPrice: number | undefined; - spread: number | undefined; - spreadPercent: number | undefined; -}; +export interface SubaccountOpenOrderPriceMap { + [IndexerOrderSide.BUY]: { + [price: string]: BigNumber; + }; + [IndexerOrderSide.SELL]: { + [price: string]: BigNumber; + }; +} diff --git a/src/hooks/Orderbook/useDrawOrderbook.ts b/src/hooks/Orderbook/useDrawOrderbook.ts index e36231e13..5c786c6a9 100644 --- a/src/hooks/Orderbook/useDrawOrderbook.ts +++ b/src/hooks/Orderbook/useDrawOrderbook.ts @@ -432,5 +432,9 @@ export const useDrawOrderbook = ({ drawOrderbookRow, ]); + // if (side === 'ask') { + // console.log(data); + // } + return { canvasRef }; }; diff --git a/src/hooks/Orderbook/useOrderbookValues.ts b/src/hooks/Orderbook/useOrderbookValues.ts index 076d7af52..ea348c31e 100644 --- a/src/hooks/Orderbook/useOrderbookValues.ts +++ b/src/hooks/Orderbook/useOrderbookValues.ts @@ -1,37 +1,17 @@ import { useCallback, useMemo, useState } from 'react'; import { BonsaiHelpers } from '@/bonsai/ontology'; -import { OrderbookLine } from '@/bonsai/types/summaryTypes'; -import { OrderSide } from '@dydxprotocol/v4-client-js'; -import { shallowEqual } from 'react-redux'; -import { DepthChartDatum, DepthChartSeries } from '@/constants/charts'; import { GROUPING_MULTIPLIER_LIST, GroupingMultiplier } from '@/constants/orderbook'; -import { getSubaccountOrderSizeBySideAndOrderbookLevel } from '@/state/accountSelectors'; -import { useAppSelector } from '@/state/appTypes'; -import { getCurrentMarketOrderbook } from '@/state/perpetualsSelectors'; - -import { MustBigNumber } from '@/lib/numbers'; import { safeAssign } from '@/lib/objectHelpers'; -import { orEmptyRecord } from '@/lib/typeUtils'; import { useParameterizedSelector } from '../useParameterizedSelector'; -export type OrderbookLineWithMine = OrderbookLine & { - mine?: number; - key: string; - side: 'ask' | 'bid'; -}; - export const useCalculateOrderbookData = ({ rowsPerSide }: { rowsPerSide: number }) => { // const orderbook = useAppSelector(getCurrentMarketOrderbook, shallowEqual); const [groupingMultiplier, setGroupingMultiplier] = useState(GroupingMultiplier.ONE); - const subaccountOrderSizeBySideAndPrice = orEmptyRecord( - useAppSelector(getSubaccountOrderSizeBySideAndOrderbookLevel, shallowEqual) - ); - const orderbook = useParameterizedSelector( BonsaiHelpers.currentMarket.orderbook.createSelectGroupedData, groupingMultiplier @@ -56,33 +36,27 @@ export const useCalculateOrderbookData = ({ rowsPerSide }: { rowsPerSide: number }, []); return useMemo(() => { - const asks: Array = (orderbook?.asks ?? []) - .map( - (row: OrderbookLine, idx: number): OrderbookLineWithMine => - safeAssign( - {}, - { - key: `ask-${idx}`, - side: 'ask' as const, - mine: subaccountOrderSizeBySideAndPrice[OrderSide.SELL]?.[row.price], - }, - row - ) + const asks = (orderbook?.asks ?? []) + .map((row, idx: number) => + safeAssign( + {}, + { + key: `ask-${idx}`, + }, + row + ) ) - .slice(-1 * rowsPerSide); + .slice(0, rowsPerSide); - const bids: Array = (orderbook?.bids ?? []) - .map( - (row: OrderbookLine, idx: number): OrderbookLineWithMine => - safeAssign( - {}, - { - key: `bid-${idx}`, - side: 'bid' as const, - mine: subaccountOrderSizeBySideAndPrice[OrderSide.BUY]?.[row.price], - }, - row - ) + const bids = (orderbook?.bids ?? []) + .map((row, idx: number) => + safeAssign( + {}, + { + key: `bid-${idx}`, + }, + row + ) ) .slice(0, rowsPerSide); @@ -107,47 +81,5 @@ export const useCalculateOrderbookData = ({ rowsPerSide }: { rowsPerSide: number groupingMultiplier, modifyGroupingMultiplier, }; - }, [ - rowsPerSide, - orderbook, - subaccountOrderSizeBySideAndPrice, - groupingMultiplier, - modifyGroupingMultiplier, - ]); -}; - -export const useOrderbookValuesForDepthChart = () => { - const orderbook = useAppSelector(getCurrentMarketOrderbook, shallowEqual); - - return useMemo(() => { - const bids = (orderbook?.bids?.toArray() ?? []) - .filter(Boolean) - .map((datum): DepthChartDatum => safeAssign({}, datum, { seriesKey: DepthChartSeries.Bids })); - - const asks = (orderbook?.asks?.toArray() ?? []) - .filter(Boolean) - .map((datum): DepthChartDatum => safeAssign({}, datum, { seriesKey: DepthChartSeries.Asks })); - - const lowestBid = bids[bids.length - 1]; - const highestBid = bids[0]; - const lowestAsk = asks[0]; - const highestAsk = asks[asks.length - 1]; - - const midMarketPrice = orderbook?.midPrice; - const spread = MustBigNumber(lowestAsk?.price ?? 0).minus(highestBid?.price ?? 0); - const spreadPercent = orderbook?.spreadPercent; - - return { - bids, - asks, - lowestBid, - highestBid, - lowestAsk, - highestAsk, - midMarketPrice, - spread, - spreadPercent, - orderbook, - }; - }, [orderbook]); + }, [rowsPerSide, orderbook, groupingMultiplier, modifyGroupingMultiplier]); }; diff --git a/src/views/CanvasOrderbook/CanvasOrderbook.tsx b/src/views/CanvasOrderbook/CanvasOrderbook.tsx index 52aadb30e..7183ef9ae 100644 --- a/src/views/CanvasOrderbook/CanvasOrderbook.tsx +++ b/src/views/CanvasOrderbook/CanvasOrderbook.tsx @@ -85,7 +85,7 @@ export const CanvasOrderbook = forwardRef( ? new Array(rowsPerSide - asks.length).fill(undefined) : []; - const newAsksSlice: Array = [...emptyAskRows, ...asks]; + const newAsksSlice: Array = [...asks, ...emptyAskRows]; const emptyBidRows = bids.length < rowsPerSide @@ -94,7 +94,7 @@ export const CanvasOrderbook = forwardRef( const newBidsSlice: Array = [...bids, ...emptyBidRows]; return { - asksSlice: layout === 'horizontal' ? newAsksSlice : newAsksSlice.reverse(), + asksSlice: layout === 'horizontal' ? newAsksSlice.reverse() : newAsksSlice, bidsSlice: newBidsSlice, }; }, [asks, bids, layout, rowsPerSide]); diff --git a/src/views/MidMarketPrice.tsx b/src/views/MidMarketPrice.tsx index 2bfa4a542..d9b8e3cd6 100644 --- a/src/views/MidMarketPrice.tsx +++ b/src/views/MidMarketPrice.tsx @@ -1,12 +1,9 @@ import { useEffect, useRef } from 'react'; import { BonsaiHelpers } from '@/bonsai/ontology'; +import BigNumber from 'bignumber.js'; import styled, { css } from 'styled-components'; -import { Nullable } from '@/constants/abacus'; - -import { useParameterizedSelector } from '@/hooks/useParameterizedSelector'; - import { layoutMixins } from '@/styles/layoutMixins'; import { LoadingDots } from '@/components/Loading/LoadingDots'; @@ -14,23 +11,24 @@ import { Output, OutputType } from '@/components/Output'; import { useAppSelector } from '@/state/appTypes'; -import { MustBigNumber } from '@/lib/numbers'; import { orEmptyObj } from '@/lib/typeUtils'; const getMidMarketPriceColor = ({ midMarketPrice, lastMidMarketPrice, }: { - midMarketPrice: Nullable; - lastMidMarketPrice: Nullable; + midMarketPrice?: BigNumber; + lastMidMarketPrice?: BigNumber; }) => { - if (MustBigNumber(midMarketPrice).lt(MustBigNumber(lastMidMarketPrice))) { + if (lastMidMarketPrice == null || midMarketPrice == null) { + return 'var(--color-text-2)'; + } + if (midMarketPrice.lt(lastMidMarketPrice)) { return 'var(--color-negative)'; } - if (MustBigNumber(midMarketPrice).gt(MustBigNumber(lastMidMarketPrice))) { + if (midMarketPrice.gt(lastMidMarketPrice)) { return 'var(--color-positive)'; } - return 'var(--color-text-2)'; }; @@ -40,13 +38,10 @@ export const MidMarketPrice = () => { ); const midMarketPriceLoading = ['pending', 'idle'].includes( - useAppSelector(BonsaiHelpers.currentMarket.orderbook.loading) + useAppSelector(BonsaiHelpers.currentMarket.midPrice.loading) ); - const midMarketPrice = useParameterizedSelector( - BonsaiHelpers.currentMarket.orderbook.createSelectGroupedData, - undefined - )?.midPrice; + const midMarketPrice = useAppSelector(BonsaiHelpers.currentMarket.midPrice.data); const lastMidMarketPrice = useRef(midMarketPrice); diff --git a/src/views/charts/DepthChart/Tooltip.tsx b/src/views/charts/DepthChart/Tooltip.tsx index 7c6f60eb0..ff701ef51 100644 --- a/src/views/charts/DepthChart/Tooltip.tsx +++ b/src/views/charts/DepthChart/Tooltip.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react'; +import { BonsaiHelpers } from '@/bonsai/ontology'; import { OrderSide } from '@dydxprotocol/v4-client-js'; import type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip'; import { shallowEqual } from 'react-redux'; @@ -13,7 +14,6 @@ import { } from '@/constants/charts'; import { STRING_KEYS } from '@/constants/localization'; -import { useOrderbookValuesForDepthChart } from '@/hooks/Orderbook/useOrderbookValues'; import { useStringGetter } from '@/hooks/useStringGetter'; import { Details } from '@/components/Details'; @@ -24,6 +24,7 @@ import { useAppSelector } from '@/state/appTypes'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; import { MustBigNumber } from '@/lib/numbers'; +import { orEmptyObj } from '@/lib/typeUtils'; type DepthChartTooltipProps = { chartPointAtPointer: DepthChartPoint; @@ -42,20 +43,22 @@ export const DepthChartTooltipContent = ({ }: DepthChartTooltipProps) => { const { nearestDatum } = tooltipData ?? {}; const stringGetter = useStringGetter(); - const { spread, spreadPercent, midMarketPrice } = useOrderbookValuesForDepthChart(); const { id = '' } = useAppSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; + const { spread, spreadPercent, midPrice } = orEmptyObj( + useAppSelector(BonsaiHelpers.currentMarket.depthChart.data) + ); const priceImpact = useMemo(() => { - if (nearestDatum && midMarketPrice) { + if (nearestDatum && midPrice) { const depthChartSeries = nearestDatum.key as DepthChartSeries; return { - [DepthChartSeries.Bids]: MustBigNumber(midMarketPrice) + [DepthChartSeries.Bids]: MustBigNumber(midPrice) .minus(nearestDatum.datum.price) - .div(midMarketPrice), + .div(midPrice), [DepthChartSeries.Asks]: MustBigNumber(nearestDatum.datum.price) - .minus(midMarketPrice) - .div(midMarketPrice), + .minus(midPrice) + .div(midPrice), [DepthChartSeries.MidMarket]: undefined, }[depthChartSeries]; } @@ -137,11 +140,9 @@ export const DepthChartTooltipContent = ({ : nearestDatum?.key === DepthChartSeries.MidMarket ? [ { - key: 'midMarketPrice', + key: 'midPrice', label: stringGetter({ key: STRING_KEYS.PRICE }), - value: ( - - ), + value: , }, { key: 'spread', diff --git a/src/views/charts/DepthChart/index.tsx b/src/views/charts/DepthChart/index.tsx index 86bb8151e..e5418c1b6 100644 --- a/src/views/charts/DepthChart/index.tsx +++ b/src/views/charts/DepthChart/index.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; +import { BonsaiHelpers } from '@/bonsai/ontology'; import { OrderSide } from '@dydxprotocol/v4-client-js'; import { curveStepAfter } from '@visx/curve'; import { LinearGradient } from '@visx/gradient'; @@ -26,7 +27,6 @@ import { } from '@/constants/charts'; import { StringGetterFunction } from '@/constants/localization'; -import { useOrderbookValuesForDepthChart } from '@/hooks/Orderbook/useOrderbookValues'; import { useBreakpoints } from '@/hooks/useBreakpoints'; import { useLocaleSeparators } from '@/hooks/useLocaleSeparators'; @@ -38,9 +38,9 @@ import { XYChartWithPointerEvents } from '@/components/visx/XYChartWithPointerEv import { useAppSelector } from '@/state/appTypes'; import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; -import { getCurrentMarketConfig } from '@/state/perpetualsSelectors'; import { MustBigNumber } from '@/lib/numbers'; +import { orEmptyObj } from '@/lib/typeUtils'; import { DepthChartTooltipContent } from './Tooltip'; @@ -69,11 +69,13 @@ export const DepthChart = ({ // Chart data const { id = '' } = useAppSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; - const { stepSizeDecimals, tickSizeDecimals } = - useAppSelector(getCurrentMarketConfig, shallowEqual) ?? {}; + const { stepSizeDecimals, tickSizeDecimals } = orEmptyObj( + useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) + ); - const { bids, asks, lowestBid, highestBid, lowestAsk, highestAsk, midMarketPrice, orderbook } = - useOrderbookValuesForDepthChart(); + const { bids, asks, lowestBid, highestBid, lowestAsk, highestAsk, midPrice } = orEmptyObj( + useAppSelector(BonsaiHelpers.currentMarket.depthChart.data) + ); // Chart state @@ -85,44 +87,44 @@ export const DepthChart = ({ const [zoomDomain, setZoomDomain] = useState(); useEffect(() => { - if (!midMarketPrice) { + if (!midPrice) { setZoomDomain(undefined); } else if (!zoomDomain) { setZoomDomain( // Start by showing smallest of: Math.min( // Mid-market price ± 1.5% - midMarketPrice * 0.015, + midPrice * 0.015, // Mid-market price ± halfway to lowest bid - (midMarketPrice - (lowestBid?.price ?? 0)) / 2, + (midPrice - (lowestBid?.price ?? 0)) / 2, // Mid-market price ± halfway to highest ask - ((highestAsk?.price ?? midMarketPrice) - midMarketPrice) / 2 + ((highestAsk?.price ?? midPrice) - midPrice) / 2 ) ); } - }, [midMarketPrice]); + }, [midPrice]); // Computations const { domain, range } = useMemo(() => { - if (!(zoomDomain && midMarketPrice && asks.length && bids.length)) + if (!(zoomDomain && midPrice && asks?.length && bids?.length)) return { domain: [0, 0] as const, range: [0, 0] as const }; const newDomain = [ - clamp(midMarketPrice - zoomDomain, 0, highestBid?.price ?? 0), - clamp(midMarketPrice + zoomDomain, lowestAsk?.price ?? 0, highestAsk?.price ?? 0), + clamp(midPrice - zoomDomain, 0, highestBid?.price ?? 0), + clamp(midPrice + zoomDomain, lowestAsk?.price ?? 0, highestAsk?.price ?? 0), ] as const; const newRange = [ 0, [...bids, ...asks] .filter((datum) => datum.price >= newDomain[0] && datum.price <= newDomain[1]) - .map((datum) => datum.depth ?? 0) + .map((datum) => datum.depth) .reduce((a, b) => Math.max(a, b), 0), ] as const; return { domain: newDomain, range: newRange }; - }, [orderbook, zoomDomain]); + }, [asks, bids, highestAsk?.price, highestBid?.price, lowestAsk?.price, midPrice, zoomDomain]); const getChartPoint = useCallback( (point: Point | EventHandlerParams) => { @@ -139,12 +141,12 @@ export const DepthChart = ({ } return { - side: MustBigNumber(price).lt(midMarketPrice!) ? OrderSide.BUY : OrderSide.SELL, + side: MustBigNumber(price).lt(midPrice!) ? OrderSide.BUY : OrderSide.SELL, price, size, } satisfies DepthChartPoint; }, - [midMarketPrice] + [midPrice] ); const formatNumber = useCallback( @@ -160,7 +162,7 @@ export const DepthChart = ({ // Render conditions - if (!(zoomDomain && midMarketPrice && asks.length && bids.length)) + if (!(zoomDomain && midPrice && asks?.length && bids?.length)) return ; // Events @@ -175,14 +177,8 @@ export const DepthChart = ({ 1e-320, Math.min(Number.MAX_SAFE_INTEGER, zoomDomain * Math.exp(wheelDelta / 1000)) ), - Math.min( - midMarketPrice - (highestBid?.price ?? 0), - (lowestAsk?.price ?? 0) - midMarketPrice - ), - Math.max( - midMarketPrice - (lowestBid?.price ?? 0), - (highestAsk?.price ?? 0) - midMarketPrice - ) + Math.min(midPrice - (highestBid?.price ?? 0), (lowestAsk?.price ?? 0) - midPrice), + Math.max(midPrice - (lowestBid?.price ?? 0), (highestAsk?.price ?? 0) - midPrice) ) ); }; @@ -216,8 +212,8 @@ export const DepthChart = ({ top: 0, bottom: 32, }} - onPointerUp={(point) => point && onChartClick?.(getChartPoint(point))} - onPointerMove={(point) => point && setChartPointAtPointer(getChartPoint(point))} + onPointerUp={(point) => onChartClick?.(getChartPoint(point))} + onPointerMove={(point) => setChartPointAtPointer(getChartPoint(point))} onPointerPressedChange={(pointerPressed) => setIsPointerPressed(pointerPressed)} > @@ -236,15 +232,15 @@ export const DepthChart = ({ bids.length > 0 ? [ { - ...highestBid!, + ...lowestBid!, depth: 0, }, ...bids, { - ...lowestBid!, - price: 0, + ...highestBid!, + depth: 0, }, - ].reverse() + ] : [] } xAccessor={(datum: DepthChartDatum | undefined) => datum?.price ?? 0} @@ -271,11 +267,15 @@ export const DepthChart = ({ depth: 0, }, ...asks, + { + ...highestAsk!, + depth: 0, + }, ] : [] } - xAccessor={(datum: DepthChartDatum) => datum.price} - yAccessor={(datum: DepthChartDatum) => datum.depth} + xAccessor={(datum: DepthChartDatum | undefined) => datum?.price ?? 0} + yAccessor={(datum: DepthChartDatum | undefined) => datum?.depth ?? 0} curve={curveStepAfter} lineProps={{ strokeWidth: 1.5 }} fillOpacity={0.2} @@ -292,21 +292,21 @@ export const DepthChart = ({ dataKey={DepthChartSeries.MidMarket} data={[ { - price: midMarketPrice, + price: midPrice, depth: lerp(1.2, ...range), }, { - price: midMarketPrice, + price: midPrice, depth: lerp(0.5, ...range), }, { - price: midMarketPrice, + price: midPrice, depth: lerp(-0.1, ...range), }, ]} strokeWidth={0.25} - xAccessor={(datum) => datum.price} - yAccessor={(datum) => datum.depth} + xAccessor={(datum) => datum?.price} + yAccessor={(datum) => datum?.depth} /> @@ -323,7 +323,7 @@ export const DepthChart = ({ value={ isEditingOrder && chartPointAtPointer ? chartPointAtPointer.price - : tooltipData!.nearestDatum?.datum.price + : tooltipData!.nearestDatum?.datum?.price } useGrouping={false} accentColor={ From ed70d71df9358af8bf8f6c1b75348c696c73a01d Mon Sep 17 00:00:00 2001 From: jaredvu Date: Thu, 30 Jan 2025 12:09:23 -0800 Subject: [PATCH 08/13] lax type import restriction --- .eslintrc.json | 7 +---- src/bonsai/calculators/orderbook.ts | 4 +-- src/bonsai/selectors/orderbook.ts | 7 +++-- src/bonsai/types/orderbookTypes.ts | 28 ++++++++++++++----- src/bonsai/types/summaryTypes.ts | 8 ------ src/constants/charts.ts | 8 ++++-- src/constants/markets.ts | 6 ---- src/hooks/Orderbook/useDrawOrderbook.ts | 12 +++----- src/hooks/Orderbook/useOrderbookValues.ts | 3 +- src/views/CanvasOrderbook/CanvasOrderbook.tsx | 6 ++-- .../CanvasOrderbook/OrderbookControls.tsx | 21 ++++++-------- src/views/charts/FundingChart/Tooltip.tsx | 7 +++-- src/views/charts/FundingChart/index.tsx | 7 +++-- 13 files changed, 63 insertions(+), 61 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index c736bca82..c6558ea04 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -52,12 +52,7 @@ "no-restricted-imports": [ "error", { - "patterns": [ - "@/bonsai/*/*", - "!@/bonsai/ontology", - "!@/bonsai/lib/*", - "!@/bonsai/types/summaryTypes" - ] + "patterns": ["@/bonsai/*/*", "!@/bonsai/ontology", "!@/bonsai/lib/*", "!@/bonsai/types/*"] } ], "no-return-assign": "off", diff --git a/src/bonsai/calculators/orderbook.ts b/src/bonsai/calculators/orderbook.ts index b0b5f8a0a..89b673704 100644 --- a/src/bonsai/calculators/orderbook.ts +++ b/src/bonsai/calculators/orderbook.ts @@ -11,9 +11,9 @@ import { BIG_NUMBERS, MustBigNumber, roundToNearestFactor } from '@/lib/numbers' import { objectEntries } from '@/lib/objectHelpers'; import { orEmptyRecord } from '@/lib/typeUtils'; -import { OrderbookLine } from '../types/orderbookTypes'; +import { OrderbookLine, SubaccountOpenOrderPriceMap } from '../types/orderbookTypes'; import { OrderbookData } from '../types/rawTypes'; -import { SubaccountOpenOrderPriceMap, SubaccountOrder } from '../types/summaryTypes'; +import { SubaccountOrder } from '../types/summaryTypes'; type OrderbookLineBN = { price: BigNumber; diff --git a/src/bonsai/selectors/orderbook.ts b/src/bonsai/selectors/orderbook.ts index bd3b2e265..0e81efa4f 100644 --- a/src/bonsai/selectors/orderbook.ts +++ b/src/bonsai/selectors/orderbook.ts @@ -14,6 +14,7 @@ import { getGroupingTickSize, getSubaccountOpenOrdersPriceMap, } from '../calculators/orderbook'; +import { CanvasOrderbookLine } from '../types/orderbookTypes'; import { selectCurrentMarketOpenOrders } from './account'; import { selectCurrentMarketOrderbook } from './markets'; import { selectCurrentMarketInfoStable } from './summary'; @@ -59,7 +60,7 @@ export const createSelectCurrentMarketOrderbook = () => asksSortOrder: 'asc', }); - const bids = + const bids: CanvasOrderbookLine[] = formattedOrderbook?.bids.map((line) => ({ ...line, side: 'bid', @@ -72,7 +73,7 @@ export const createSelectCurrentMarketOrderbook = () => }), })) ?? EMPTY_ARR; - const asks = + const asks: CanvasOrderbookLine[] = formattedOrderbook?.asks.map((line) => ({ ...line, side: 'ask', @@ -93,6 +94,8 @@ export const createSelectCurrentMarketOrderbook = () => midPrice, spread, spreadPercent, + groupingTickSize, + groupingTickSizeDecimals, }; } ); diff --git a/src/bonsai/types/orderbookTypes.ts b/src/bonsai/types/orderbookTypes.ts index b4d85dea8..313fad889 100644 --- a/src/bonsai/types/orderbookTypes.ts +++ b/src/bonsai/types/orderbookTypes.ts @@ -1,4 +1,16 @@ +import BigNumber from 'bignumber.js'; + import { DepthChartSeries } from '@/constants/charts'; +import { IndexerOrderSide } from '@/types/indexer/indexerApiGen'; + +export interface SubaccountOpenOrderPriceMap { + [IndexerOrderSide.BUY]: { + [price: string]: BigNumber; + }; + [IndexerOrderSide.SELL]: { + [price: string]: BigNumber; + }; +} export type OrderbookLine = { price: number; @@ -9,17 +21,19 @@ export type OrderbookLine = { offset: number; }; +export type CanvasOrderbookLine = OrderbookLine & { + mine: number | undefined; + side: 'ask' | 'bid'; +}; + export type OrderbookProcessedData = { - asks: OrderbookLine[] | CanvasOrderbookLine[]; - bids: OrderbookLine[] | CanvasOrderbookLine[]; + asks: CanvasOrderbookLine[]; + bids: CanvasOrderbookLine[]; midPrice: number | undefined; spread: number | undefined; spreadPercent: number | undefined; -}; - -export type CanvasOrderbookLine = OrderbookLine & { - mine: number | undefined; - side: 'ask' | 'bid'; + groupingTickSize: number; + groupingTickSizeDecimals: number; }; type DepthChartDatum = OrderbookLine & { diff --git a/src/bonsai/types/summaryTypes.ts b/src/bonsai/types/summaryTypes.ts index d1f3fdb23..1df0d8ace 100644 --- a/src/bonsai/types/summaryTypes.ts +++ b/src/bonsai/types/summaryTypes.ts @@ -295,14 +295,6 @@ export interface UserStats { takerVolume30D?: number; } -export interface SubaccountOpenOrderPriceMap { - [IndexerOrderSide.BUY]: { - [price: string]: BigNumber; - }; - [IndexerOrderSide.SELL]: { - [price: string]: BigNumber; - }; -} export type AccountBalances = { usdcAmount?: string; chainTokenAmount?: string; diff --git a/src/constants/charts.ts b/src/constants/charts.ts index 6656f9478..7be30d2db 100644 --- a/src/constants/charts.ts +++ b/src/constants/charts.ts @@ -1,8 +1,6 @@ import { Nullable } from '@dydxprotocol/v4-abacus'; import { OrderSide } from '@dydxprotocol/v4-client-js'; -import { FundingDirection } from './markets'; - export const TOGGLE_ACTIVE_CLASS_NAME = 'toggle-active'; // ------ Depth Chart ------ // @@ -32,6 +30,12 @@ export const SERIES_KEY_FOR_ORDER_SIDE = { }; // ------ Funding Chart ------ // +export enum FundingDirection { + ToShort = 'ToShort', + ToLong = 'ToLong', + None = 'None', +} + export enum FundingRateResolution { OneHour = 'OneHour', EightHour = 'EightHour', diff --git a/src/constants/markets.ts b/src/constants/markets.ts index d353e7e33..720f4c388 100644 --- a/src/constants/markets.ts +++ b/src/constants/markets.ts @@ -127,12 +127,6 @@ export const MARKET_FILTER_OPTIONS: Record< export const DEFAULT_MARKETID = 'BTC-USD'; export const DEFAULT_QUOTE_ASSET = 'USD'; -export enum FundingDirection { - ToShort = 'ToShort', - ToLong = 'ToLong', - None = 'None', -} - export const PREDICTION_MARKET = { TRUMPWIN: 'TRUMPWIN-USD', }; diff --git a/src/hooks/Orderbook/useDrawOrderbook.ts b/src/hooks/Orderbook/useDrawOrderbook.ts index d436cb29a..6a2a10917 100644 --- a/src/hooks/Orderbook/useDrawOrderbook.ts +++ b/src/hooks/Orderbook/useDrawOrderbook.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { BonsaiHelpers } from '@/bonsai/ontology'; +import { CanvasOrderbookLine } from '@/bonsai/types/orderbookTypes'; import { shallowEqual } from 'react-redux'; import { SMALL_USD_DECIMALS, TOKEN_DECIMALS } from '@/constants/numbers'; @@ -33,12 +34,11 @@ import { generateFadedColorVariant } from '@/lib/styles'; import { orEmptyObj } from '@/lib/typeUtils'; import { useLocaleSeparators } from '../useLocaleSeparators'; -import { OrderbookLineWithMine } from './useOrderbookValues'; type ElementProps = { - data: Array; + data: Array; histogramRange: number; - side: OrderbookLineWithMine['side']; + side: CanvasOrderbookLine['side']; displayUnit: DisplayUnit; }; @@ -325,7 +325,7 @@ export const useDrawOrderbook = ({ }: { ctx: CanvasRenderingContext2D; idx: number; - rowToRender?: OrderbookLineWithMine; + rowToRender?: CanvasOrderbookLine; animationType?: OrderbookRowAnimationType; }) => { if (!rowToRender) return; @@ -434,9 +434,5 @@ export const useDrawOrderbook = ({ drawOrderbookRow, ]); - // if (side === 'ask') { - // console.log(data); - // } - return { canvasRef }; }; diff --git a/src/hooks/Orderbook/useOrderbookValues.ts b/src/hooks/Orderbook/useOrderbookValues.ts index ea348c31e..2ab532c33 100644 --- a/src/hooks/Orderbook/useOrderbookValues.ts +++ b/src/hooks/Orderbook/useOrderbookValues.ts @@ -9,7 +9,6 @@ import { safeAssign } from '@/lib/objectHelpers'; import { useParameterizedSelector } from '../useParameterizedSelector'; export const useCalculateOrderbookData = ({ rowsPerSide }: { rowsPerSide: number }) => { - // const orderbook = useAppSelector(getCurrentMarketOrderbook, shallowEqual); const [groupingMultiplier, setGroupingMultiplier] = useState(GroupingMultiplier.ONE); const orderbook = useParameterizedSelector( @@ -79,6 +78,8 @@ export const useCalculateOrderbookData = ({ rowsPerSide }: { rowsPerSide: number // Orderbook grouping groupingMultiplier, + groupingTickSize: orderbook?.groupingTickSize, + groupingTickSizeDecimals: orderbook?.groupingTickSizeDecimals, modifyGroupingMultiplier, }; }, [rowsPerSide, orderbook, groupingMultiplier, modifyGroupingMultiplier]); diff --git a/src/views/CanvasOrderbook/CanvasOrderbook.tsx b/src/views/CanvasOrderbook/CanvasOrderbook.tsx index a62d5c78d..f07567c3b 100644 --- a/src/views/CanvasOrderbook/CanvasOrderbook.tsx +++ b/src/views/CanvasOrderbook/CanvasOrderbook.tsx @@ -59,7 +59,8 @@ export const CanvasOrderbook = forwardRef( midMarketPrice, hasOrderbook, histogramRange, - groupingMultiplier, + groupingTickSize, + groupingTickSizeDecimals, modifyGroupingMultiplier, } = useCalculateOrderbookData({ rowsPerSide, @@ -206,7 +207,8 @@ export const CanvasOrderbook = forwardRef( {!hideHeader && ( )} diff --git a/src/views/CanvasOrderbook/OrderbookControls.tsx b/src/views/CanvasOrderbook/OrderbookControls.tsx index 4dbd13e9c..8b002ee88 100644 --- a/src/views/CanvasOrderbook/OrderbookControls.tsx +++ b/src/views/CanvasOrderbook/OrderbookControls.tsx @@ -1,12 +1,10 @@ import { useCallback } from 'react'; -import { BonsaiHelpers } from '@/bonsai/ontology'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { ButtonShape, ButtonSize, ButtonStyle } from '@/constants/buttons'; import { USD_DECIMALS } from '@/constants/numbers'; -import { GroupingMultiplier } from '@/constants/orderbook'; import { DisplayUnit } from '@/constants/trade'; import { Button } from '@/components/Button'; @@ -19,29 +17,26 @@ import { setDisplayUnit } from '@/state/appUiConfigs'; import { getSelectedDisplayUnit } from '@/state/appUiConfigsSelectors'; import { getDisplayableAssetFromBaseAsset } from '@/lib/assetUtils'; -import { MustBigNumber } from '@/lib/numbers'; -import { orEmptyObj } from '@/lib/typeUtils'; type OrderbookControlsProps = { className?: string; assetId?: string; - grouping: GroupingMultiplier; + groupingTickSize?: number; + groupingTickSizeDecimals?: number; modifyGrouping: (increase: boolean) => void; }; export const OrderbookControls = ({ className, assetId, - grouping, + groupingTickSize, + groupingTickSizeDecimals, modifyGrouping, }: OrderbookControlsProps) => { const dispatch = useDispatch(); const displayUnit = useAppSelector(getSelectedDisplayUnit); - const { tickSize, tickSizeDecimals = USD_DECIMALS } = orEmptyObj( - useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) - ); - - const displayTickSize = tickSize && MustBigNumber(tickSize).times(grouping).toNumber(); + const fractionDigits = + (groupingTickSizeDecimals ?? 0) <= 1 ? USD_DECIMALS : groupingTickSizeDecimals; const onToggleDisplayUnit = useCallback( (newValue: DisplayUnit) => { @@ -83,9 +78,9 @@ export const OrderbookControls = ({ {assetId && ( diff --git a/src/views/charts/FundingChart/Tooltip.tsx b/src/views/charts/FundingChart/Tooltip.tsx index 68d686fc4..1551da8b4 100644 --- a/src/views/charts/FundingChart/Tooltip.tsx +++ b/src/views/charts/FundingChart/Tooltip.tsx @@ -1,8 +1,11 @@ import type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip'; -import { FundingRateResolution, type FundingChartDatum } from '@/constants/charts'; +import { + FundingDirection, + FundingRateResolution, + type FundingChartDatum, +} from '@/constants/charts'; import { STRING_KEYS } from '@/constants/localization'; -import { FundingDirection } from '@/constants/markets'; import { FUNDING_DECIMALS } from '@/constants/numbers'; import { useStringGetter } from '@/hooks/useStringGetter'; diff --git a/src/views/charts/FundingChart/index.tsx b/src/views/charts/FundingChart/index.tsx index 3acd219e8..55b7cce3d 100644 --- a/src/views/charts/FundingChart/index.tsx +++ b/src/views/charts/FundingChart/index.tsx @@ -6,9 +6,12 @@ import type { TooltipContextType } from '@visx/xychart'; import styled, { css } from 'styled-components'; import { ButtonSize } from '@/constants/buttons'; -import { FundingRateResolution, type FundingChartDatum } from '@/constants/charts'; +import { + FundingDirection, + FundingRateResolution, + type FundingChartDatum, +} from '@/constants/charts'; import { STRING_KEYS } from '@/constants/localization'; -import { FundingDirection } from '@/constants/markets'; import { FUNDING_DECIMALS } from '@/constants/numbers'; import { EMPTY_ARR } from '@/constants/objects'; import { timeUnits } from '@/constants/time'; From 1b151f5c55883c0a9c72613e0d2d1dd1cfad01d5 Mon Sep 17 00:00:00 2001 From: jaredvu Date: Thu, 30 Jan 2025 12:27:41 -0800 Subject: [PATCH 09/13] add type and fix import --- src/bonsai/calculators/funding.ts | 2 +- src/views/charts/DepthChart/index.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bonsai/calculators/funding.ts b/src/bonsai/calculators/funding.ts index 716bbd2f2..cff15aa71 100644 --- a/src/bonsai/calculators/funding.ts +++ b/src/bonsai/calculators/funding.ts @@ -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'; diff --git a/src/views/charts/DepthChart/index.tsx b/src/views/charts/DepthChart/index.tsx index 55d44e1a8..3b2b4cbc6 100644 --- a/src/views/charts/DepthChart/index.tsx +++ b/src/views/charts/DepthChart/index.tsx @@ -304,8 +304,8 @@ export const DepthChart = ({ }, ]} strokeWidth={0.25} - xAccessor={(datum) => datum?.price} - yAccessor={(datum) => datum?.depth} + xAccessor={(datum: { price: number; depth: number } | undefined) => datum?.price ?? 0} + yAccessor={(datum: { price: number; depth: number } | undefined) => datum?.depth ?? 0} /> From d996dd8442c7fafc9af0ac612984513da00b6e22 Mon Sep 17 00:00:00 2001 From: jaredvu Date: Thu, 30 Jan 2025 14:08:28 -0800 Subject: [PATCH 10/13] bye bye orderbook --- .eslintrc.json | 8 +++- src/hooks/Orderbook/useDrawOrderbook.ts | 17 ++++--- src/hooks/useClosePositionFormInputs.ts | 6 ++- src/lib/abacus/stateNotification.ts | 19 +------- src/state/accountSelectors.ts | 46 ------------------- src/state/perpetuals.ts | 35 +------------- src/state/perpetualsSelectors.ts | 36 +-------------- src/views/CanvasOrderbook/CanvasOrderbook.tsx | 2 +- src/views/MarketStatsDetails.tsx | 10 ---- src/views/forms/TradeForm/TradeFormInputs.tsx | 3 +- 10 files changed, 28 insertions(+), 154 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index c6558ea04..2f4308da0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -52,7 +52,13 @@ "no-restricted-imports": [ "error", { - "patterns": ["@/bonsai/*/*", "!@/bonsai/ontology", "!@/bonsai/lib/*", "!@/bonsai/types/*"] + "patterns": [ + "@/bonsai/*/*", + "!@/bonsai/ontology", + "!@/bonsai/lib/*", + "!@/bonsai/types/summaryTypes", + "!@/bonsai/types/orderbookTypes" + ] } ], "no-return-assign": "off", diff --git a/src/hooks/Orderbook/useDrawOrderbook.ts b/src/hooks/Orderbook/useDrawOrderbook.ts index 6a2a10917..b804d61f4 100644 --- a/src/hooks/Orderbook/useDrawOrderbook.ts +++ b/src/hooks/Orderbook/useDrawOrderbook.ts @@ -2,7 +2,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { BonsaiHelpers } from '@/bonsai/ontology'; import { CanvasOrderbookLine } from '@/bonsai/types/orderbookTypes'; -import { shallowEqual } from 'react-redux'; import { SMALL_USD_DECIMALS, TOKEN_DECIMALS } from '@/constants/numbers'; import { @@ -20,7 +19,6 @@ import { OutputType, formatNumberOutput } from '@/components/Output'; import { useAppSelector } from '@/state/appTypes'; import { getSelectedLocale } from '@/state/localizationSelectors'; -import { getCurrentMarketOrderbookMap } from '@/state/perpetualsSelectors'; import { getConsistentAssetSizeString } from '@/lib/consistentAssetSize'; import { MaybeBigNumber } from '@/lib/numbers'; @@ -64,7 +62,14 @@ export const useDrawOrderbook = ({ }: ElementProps & StyleProps) => { const canvasRef = useRef(null); const canvas = canvasRef.current; - const currentOrderbookMap = useAppSelector(getCurrentMarketOrderbookMap, shallowEqual); + const priceSizeMap = data.reduce( + (acc, row) => { + if (!row) return acc; + acc[row.price.toString()] = row.size; + return acc; + }, + {} as Record + ); const { decimal: decimalSeparator, group: groupSeparator } = useLocaleSeparators(); const selectedLocale = useAppSelector(getSelectedLocale); @@ -397,14 +402,12 @@ export const useDrawOrderbook = ({ ctx.clearRect(0, 0, canvasWidth, canvasHeight); // Animate row removal (do not animate update) - const mapOfOrderbookPriceLevels = - side && currentOrderbookMap?.[side === 'ask' ? 'asks' : 'bids']; prevData.current.forEach((row, idx) => { if (!row) return; const animationType = - mapOfOrderbookPriceLevels?.[row.price] === 0 + priceSizeMap[row.price] == null ? OrderbookRowAnimationType.REMOVE : OrderbookRowAnimationType.NONE; @@ -428,7 +431,7 @@ export const useDrawOrderbook = ({ histogramSide, side, theme, - currentOrderbookMap, + priceSizeMap, displayUnit, canvas, drawOrderbookRow, diff --git a/src/hooks/useClosePositionFormInputs.ts b/src/hooks/useClosePositionFormInputs.ts index 3d006f3a2..fda4b6fa3 100644 --- a/src/hooks/useClosePositionFormInputs.ts +++ b/src/hooks/useClosePositionFormInputs.ts @@ -10,7 +10,6 @@ import { TOKEN_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; import { useAppSelector } from '@/state/appTypes'; import { setClosePositionFormInputs } from '@/state/inputs'; import { getClosePositionFormInputs, getInputClosePositionData } from '@/state/inputsSelectors'; -import { getCurrentMarketMidMarketPrice } from '@/state/perpetualsSelectors'; import abacusStateManager from '@/lib/abacus'; import { MustBigNumber } from '@/lib/numbers'; @@ -35,7 +34,10 @@ export const useClosePositionFormInputs = () => { useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) ); - const midMarketPrice = useAppSelector(getCurrentMarketMidMarketPrice, shallowEqual); + const midMarketPrice = useAppSelector( + BonsaiHelpers.currentMarket.midPrice.data, + shallowEqual + )?.toNumber(); // when useLimit is toggled to true, set limit price input to use the mid price set in abacus useEffect(() => { diff --git a/src/lib/abacus/stateNotification.ts b/src/lib/abacus/stateNotification.ts index b385ecb83..4d6c1070a 100644 --- a/src/lib/abacus/stateNotification.ts +++ b/src/lib/abacus/stateNotification.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line max-classes-per-file import { kollections } from '@dydxprotocol/v4-abacus'; -import { fromPairs, throttle } from 'lodash'; +import { fromPairs } from 'lodash'; import type { AbacusNotification, @@ -17,7 +17,6 @@ import type { import { Changes } from '@/constants/abacus'; import { NUM_PARENT_SUBACCOUNTS } from '@/constants/account'; import { AnalyticsEvents } from '@/constants/analytics'; -import { timeUnits } from '@/constants/time'; import { type RootStore } from '@/state/_store'; import { @@ -37,7 +36,7 @@ import { import { setInputs } from '@/state/inputs'; import { setLatestOrder, updateFilledOrders, updateOrders } from '@/state/localOrders'; import { updateNotifications } from '@/state/notifications'; -import { setMarkets, setOrderbook } from '@/state/perpetuals'; +import { setMarkets } from '@/state/perpetuals'; import { track } from '../analytics/analytics'; import { isTruthy } from '../isTruthy'; @@ -179,20 +178,6 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { dispatch(setChildSubaccount(childSubaccountUpdate)); } }); - - marketIds?.forEach((market: string) => { - if (changes.has(Changes.orderbook)) { - this.throttledOrderbookUpdateByMarketId[market] ??= throttle( - (orderbook) => this.store?.dispatch(setOrderbook({ orderbook, marketId: market })), - timeUnits.second / 3 - ); - - const orderbook = updatedState.marketOrderbook(market); - if (orderbook) { - this.throttledOrderbookUpdateByMarketId[market](orderbook); - } - } - }); } } diff --git a/src/state/accountSelectors.ts b/src/state/accountSelectors.ts index 37994eea5..701383d7f 100644 --- a/src/state/accountSelectors.ts +++ b/src/state/accountSelectors.ts @@ -1,15 +1,11 @@ import { BonsaiCore } from '@/bonsai/ontology'; import { PositionUniqueId } from '@/bonsai/types/summaryTypes'; -import { OrderSide } from '@dydxprotocol/v4-client-js'; -import BigNumber from 'bignumber.js'; import { groupBy, keyBy, mapValues, sum } from 'lodash'; import { AbacusMarginMode, - AbacusOrderStatus, AbacusPositionSide, HistoricalTradingRewardsPeriod, - ORDER_SIDES, type AbacusOrderStatuses, type SubaccountFill, type SubaccountOrder, @@ -20,7 +16,6 @@ import { EMPTY_ARR } from '@/constants/objects'; import { IndexerOrderSide, IndexerPositionSide } from '@/types/indexer/indexerApiGen'; import { mapIfPresent } from '@/lib/do'; -import { MustBigNumber } from '@/lib/numbers'; import { getAverageFillPrice, getHydratedFill, @@ -36,7 +31,6 @@ import { ALL_MARKETS_STRING } from './accountUiMemory'; import { getSelectedNetwork } from './appSelectors'; import { createAppSelector } from './appTypes'; import { getCurrentMarketId } from './currentMarketSelectors'; -import { getCurrentMarketOrderbook } from './perpetualsSelectors'; /** * @param state @@ -295,46 +289,6 @@ export const getSubaccountPositionByUniqueId = () => } ); -/** - * @param state - * @returns list of orders that are in the open status - */ -export const getSubaccountOpenOrdersForCurrentMarket = createAppSelector( - [getSubaccountOrders, getCurrentMarketId], - (orders, marketId) => - orders?.filter( - (order) => - order.status === AbacusOrderStatus.Open && marketId != null && order.marketId === marketId - ) -); - -export const getSubaccountOrderSizeBySideAndOrderbookLevel = createAppSelector( - [getSubaccountOpenOrdersForCurrentMarket, getCurrentMarketOrderbook], - (openOrders = [], book = undefined) => { - const tickSize = MustBigNumber(book?.grouping?.tickSize); - const orderSizeBySideAndPrice: Partial>> = {}; - openOrders.forEach((order: SubaccountOrder) => { - const side = ORDER_SIDES[order.side.name]; - const byPrice = (orderSizeBySideAndPrice[side] ??= {}); - - const priceOrderbookLevel = (() => { - if (tickSize.isEqualTo(0)) { - return order.price; - } - const tickLevelUnrounded = MustBigNumber(order.price).div(tickSize); - const tickLevel = - side === OrderSide.BUY - ? tickLevelUnrounded.decimalPlaces(0, BigNumber.ROUND_FLOOR) - : tickLevelUnrounded.decimalPlaces(0, BigNumber.ROUND_CEIL); - - return tickLevel.times(tickSize).toNumber(); - })(); - byPrice[priceOrderbookLevel] = (byPrice[priceOrderbookLevel] ?? 0) + order.size; - }); - return orderSizeBySideAndPrice; - } -); - /** * @param orderId * @returns order details with the given orderId diff --git a/src/state/perpetuals.ts b/src/state/perpetuals.ts index 2755e3319..539c15112 100644 --- a/src/state/perpetuals.ts +++ b/src/state/perpetuals.ts @@ -1,13 +1,12 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import merge from 'lodash/merge'; -import type { MarketOrderbook, Nullable, PerpetualMarket } from '@/constants/abacus'; +import type { PerpetualMarket } from '@/constants/abacus'; import { LaunchMarketStatus } from '@/constants/launchableMarkets'; import { LocalStorageKey } from '@/constants/localStorage'; import { DEFAULT_MARKETID, MarketFilters } from '@/constants/markets'; import { getLocalStorage } from '@/lib/localStorage'; -import { processOrderbookToCreateMap } from '@/lib/orderbookHelpers'; export interface PerpetualsState { currentMarketId?: string; @@ -17,22 +16,12 @@ export interface PerpetualsState { launchMarketIds: string[]; markets?: Record; - orderbooks?: Record; - orderbooksMap?: Record< - string, - { - asks: Record; - bids: Record; - } - >; } const initialState: PerpetualsState = { currentMarketId: undefined, currentMarketIdIfTradeable: undefined, markets: undefined, - orderbooks: undefined, - orderbooksMap: undefined, marketFilter: MarketFilters.ALL, launchMarketIds: [], }; @@ -62,27 +51,6 @@ export const perpetualsSlice = createSlice({ ? merge({}, state.markets, action.payload.markets) : action.payload.markets, }), - setOrderbook: ( - state: PerpetualsState, - action: PayloadAction<{ orderbook?: Nullable; marketId: string }> - ) => { - state.orderbooks = merge({}, state.orderbooks, { - [action.payload.marketId]: action.payload.orderbook, - }); - - const { newAsks, newBids } = processOrderbookToCreateMap({ - orderbookMap: state.orderbooksMap?.[action.payload.marketId], - newOrderbook: action.payload.orderbook, - }); - - state.orderbooksMap = { - ...(state.orderbooksMap ?? {}), - [action.payload.marketId]: { - asks: newAsks, - bids: newBids, - }, - }; - }, resetPerpetualsState: () => ({ ...initialState, @@ -107,7 +75,6 @@ export const { setCurrentMarketId, setCurrentMarketIdIfTradeable, setMarkets, - setOrderbook, resetPerpetualsState, setMarketFilter, setLaunchMarketIds, diff --git a/src/state/perpetualsSelectors.ts b/src/state/perpetualsSelectors.ts index b1256f82b..736bf56dc 100644 --- a/src/state/perpetualsSelectors.ts +++ b/src/state/perpetualsSelectors.ts @@ -9,7 +9,6 @@ import { isPresent, orEmptyObj } from '@/lib/typeUtils'; import { type RootState } from './_store'; import { createAppSelector } from './appTypes'; -import { getCurrentMarketId } from './currentMarketSelectors'; /** * @returns current market filter applied inside the markets page @@ -52,29 +51,6 @@ export const getPerpetualMarketsClobIds = createAppSelector( } ); -/** - * @returns Record of subscribed or previously subscribed Orderbook data, indexed by marketId. - */ -export const getOrderbooks = (state: RootState) => state.perpetuals.orderbooks; - -/** - * @returns Orderbook data for the market the user is currently viewing - */ -export const getCurrentMarketOrderbook = (state: RootState) => { - const orderbookData = getOrderbooks(state); - const currentMarketId = getCurrentMarketId(state); - return orderbookData?.[currentMarketId ?? '']; -}; - -/** - * @returns Orderbook data as a Map of price and size for the current market - */ -export const getCurrentMarketOrderbookMap = (state: RootState) => { - const orderbookMap = state.perpetuals.orderbooksMap; - const currentMarketId = getCurrentMarketId(state); - return orderbookMap?.[currentMarketId ?? '']; -}; - /** * @returns oracle price of the market the user is currently viewing */ @@ -83,17 +59,9 @@ export const getCurrentMarketOraclePrice = createAppSelector( (m) => m?.oraclePrice ); -/** - * @returns Mid market price for the market the user is currently viewing - */ -export const getCurrentMarketMidMarketPrice = (state: RootState) => { - const currentMarketOrderbook = getCurrentMarketOrderbook(state); - return currentMarketOrderbook?.midPrice; -}; - export const getCurrentMarketMidMarketPriceWithOraclePriceFallback = createAppSelector( - [getCurrentMarketMidMarketPrice, getCurrentMarketOraclePrice], - (midMarketPrice, oraclePrice) => midMarketPrice ?? oraclePrice + [BonsaiHelpers.currentMarket.midPrice.data, getCurrentMarketOraclePrice], + (midMarketPrice, oraclePrice) => midMarketPrice?.toNumber() ?? oraclePrice ); /** diff --git a/src/views/CanvasOrderbook/CanvasOrderbook.tsx b/src/views/CanvasOrderbook/CanvasOrderbook.tsx index f07567c3b..e9ad3371a 100644 --- a/src/views/CanvasOrderbook/CanvasOrderbook.tsx +++ b/src/views/CanvasOrderbook/CanvasOrderbook.tsx @@ -155,7 +155,7 @@ export const CanvasOrderbook = forwardRef( const asksOrderbook = ( <$OrderbookSideContainer $side="asks" $rows={rowsPerSide}> <$HoverRows $bottom={layout !== 'horizontal'}> - {asksSlice.map((row, idx) => + {asksSlice.toReversed().map((row, idx) => row ? ( <$Row // eslint-disable-next-line react/no-array-index-key diff --git a/src/views/MarketStatsDetails.tsx b/src/views/MarketStatsDetails.tsx index 46175d38a..a5b86b5e0 100644 --- a/src/views/MarketStatsDetails.tsx +++ b/src/views/MarketStatsDetails.tsx @@ -1,5 +1,3 @@ -import { useEffect, useRef } from 'react'; - import { BonsaiCore, BonsaiHelpers } from '@/bonsai/ontology'; import styled, { css } from 'styled-components'; @@ -25,7 +23,6 @@ import { NextFundingTimer } from '@/views/NextFundingTimer'; import { useAppSelector } from '@/state/appTypes'; import { getSelectedDisplayUnit } from '@/state/appUiConfigsSelectors'; -import { getCurrentMarketMidMarketPrice } from '@/state/perpetualsSelectors'; import { BIG_NUMBERS, MaybeBigNumber, MustBigNumber } from '@/lib/numbers'; import { orEmptyObj } from '@/lib/typeUtils'; @@ -69,13 +66,6 @@ export const MarketStatsDetails = ({ showMidMarketPrice = true }: ElementProps) volume24H, } = orEmptyObj(marketData); - const midMarketPrice = useAppSelector(getCurrentMarketMidMarketPrice); - const lastMidMarketPrice = useRef(midMarketPrice); - - useEffect(() => { - lastMidMarketPrice.current = midMarketPrice; - }, [midMarketPrice]); - const displayUnit = useAppSelector(getSelectedDisplayUnit); const valueMap = { diff --git a/src/views/forms/TradeForm/TradeFormInputs.tsx b/src/views/forms/TradeForm/TradeFormInputs.tsx index 55f8f3569..647dc2415 100644 --- a/src/views/forms/TradeForm/TradeFormInputs.tsx +++ b/src/views/forms/TradeForm/TradeFormInputs.tsx @@ -23,7 +23,6 @@ import { WithTooltip } from '@/components/WithTooltip'; import { useAppDispatch, useAppSelector } from '@/state/appTypes'; import { setTradeFormInputs } from '@/state/inputs'; import { getInputTradeData, getTradeFormInputs, useTradeFormData } from '@/state/inputsSelectors'; -import { getCurrentMarketMidMarketPrice } from '@/state/perpetualsSelectors'; import { MustBigNumber } from '@/lib/numbers'; import { orEmptyObj } from '@/lib/typeUtils'; @@ -53,7 +52,7 @@ export const TradeFormInputs = () => { useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) ); - const midMarketPrice = useAppSelector(getCurrentMarketMidMarketPrice, shallowEqual); + const midMarketPrice = useAppSelector(BonsaiHelpers.currentMarket.midPrice.data)?.toNumber(); const [hasSetMidMarketLimit, setHasSetMidMarketLimit] = useState(false); useEffect(() => { From a3a9b33432299601c9faf2c56627c707700258b3 Mon Sep 17 00:00:00 2001 From: jaredvu Date: Thu, 30 Jan 2025 14:35:09 -0800 Subject: [PATCH 11/13] fix comment --- src/bonsai/calculators/orderbook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/calculators/orderbook.ts b/src/bonsai/calculators/orderbook.ts index 89b673704..e786fe9e2 100644 --- a/src/bonsai/calculators/orderbook.ts +++ b/src/bonsai/calculators/orderbook.ts @@ -144,7 +144,7 @@ export const formatOrderbook = weakMapMemoize( group(bids, groupingTickSize, groupingTickSizeDecimals, true) ); - // Convert groupedAsks and groupedBids to list and sort by price. Asks will now be descending and bids will be remain descending + // 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], From 4772e13de65485f0f527a91e360af3e7b2ace2a7 Mon Sep 17 00:00:00 2001 From: jaredvu Date: Thu, 30 Jan 2025 15:45:08 -0800 Subject: [PATCH 12/13] highestBid and lowestAsk accessor --- src/bonsai/calculators/orderbook.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/calculators/orderbook.ts b/src/bonsai/calculators/orderbook.ts index e786fe9e2..04db7afd2 100644 --- a/src/bonsai/calculators/orderbook.ts +++ b/src/bonsai/calculators/orderbook.ts @@ -156,8 +156,8 @@ export const formatOrderbook = weakMapMemoize( [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, From 91d3f51cae6b49343f14657ff97e8eaba6924ae6 Mon Sep 17 00:00:00 2001 From: jaredvu Date: Mon, 3 Feb 2025 13:23:54 -0800 Subject: [PATCH 13/13] feedback --- src/bonsai/calculators/orderbook.ts | 12 +++----- src/bonsai/selectors/orderbook.ts | 28 +++++++++++++------ src/views/CanvasOrderbook/CanvasOrderbook.tsx | 2 +- .../CanvasOrderbook/OrderbookControls.tsx | 2 +- src/views/MidMarketPrice.tsx | 10 +++---- 5 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/bonsai/calculators/orderbook.ts b/src/bonsai/calculators/orderbook.ts index 04db7afd2..bf7bf7c3e 100644 --- a/src/bonsai/calculators/orderbook.ts +++ b/src/bonsai/calculators/orderbook.ts @@ -9,7 +9,7 @@ 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 { orEmptyObj, orEmptyRecord } from '@/lib/typeUtils'; import { OrderbookLine, SubaccountOpenOrderPriceMap } from '../types/orderbookTypes'; import { OrderbookData } from '../types/rawTypes'; @@ -103,24 +103,20 @@ export const formatOrderbook = weakMapMemoize( groupingMultiplier = GroupingMultiplier.ONE, asksSortOrder = 'desc', bidsSortOrder = 'desc', - } = options ?? { - groupingMultiplier: GroupingMultiplier.ONE, - asksSortOrder: 'desc', - bidsSortOrder: 'desc', - }; + } = orEmptyObj(options); // If groupingMultiplier is ONE, return the orderbook as is. if (groupingMultiplier === GroupingMultiplier.ONE) { const asks = ( asksSortOrder === 'desc' - ? currentMarketOrderbook.asks.toReversed() + ? [...currentMarketOrderbook.asks].reverse() : currentMarketOrderbook.asks ).map(mapOrderbookLineToNumber); const bids = ( bidsSortOrder === 'desc' ? currentMarketOrderbook.bids - : currentMarketOrderbook.bids.toReversed() + : [...currentMarketOrderbook.bids].reverse() ).map(mapOrderbookLineToNumber); return { diff --git a/src/bonsai/selectors/orderbook.ts b/src/bonsai/selectors/orderbook.ts index 0e81efa4f..977af1836 100644 --- a/src/bonsai/selectors/orderbook.ts +++ b/src/bonsai/selectors/orderbook.ts @@ -1,3 +1,5 @@ +import { weakMapMemoize } from 'reselect'; + import { DepthChartSeries } from '@/constants/charts'; import { EMPTY_ARR } from '@/constants/objects'; import { GroupingMultiplier } from '@/constants/orderbook'; @@ -19,6 +21,17 @@ import { selectCurrentMarketOpenOrders } from './account'; import { selectCurrentMarketOrderbook } from './markets'; import { selectCurrentMarketInfoStable } from './summary'; +const DEPTH_CHART_OPTIONS = { + groupingMultiplier: GroupingMultiplier.ONE, + asksSortOrder: 'asc', + bidsSortOrder: 'asc', +} as const; + +const getCanvasOrderbookOptions = weakMapMemoize((groupingMultiplier?: GroupingMultiplier) => ({ + groupingMultiplier: groupingMultiplier ?? GroupingMultiplier.ONE, + asksSortOrder: 'asc' as const, +})); + export const selectCurrentMarketMidPrice = createAppSelector( [selectCurrentMarketOrderbook], (orderbook) => { @@ -55,10 +68,11 @@ export const createSelectCurrentMarketOrderbook = () => groupingTickSizeDecimals ); - const formattedOrderbook = formatOrderbook(orderbookBN, stableInfo.tickSize, { - groupingMultiplier, - asksSortOrder: 'asc', - }); + const formattedOrderbook = formatOrderbook( + orderbookBN, + stableInfo.tickSize, + getCanvasOrderbookOptions(groupingMultiplier) + ); const bids: CanvasOrderbookLine[] = formattedOrderbook?.bids.map((line) => ({ @@ -108,11 +122,7 @@ export const selectCurrentMarketDepthChart = createAppSelector( } const { tickSize } = stableInfo; const calculatedOrderbook = calculateOrderbook(orderbook.data); - const formattedOrderbook = formatOrderbook(calculatedOrderbook, tickSize, { - groupingMultiplier: GroupingMultiplier.ONE, - asksSortOrder: 'asc', - bidsSortOrder: 'asc', - }); + const formattedOrderbook = formatOrderbook(calculatedOrderbook, tickSize, DEPTH_CHART_OPTIONS); const asks = formattedOrderbook?.asks.map((datum) => ({ diff --git a/src/views/CanvasOrderbook/CanvasOrderbook.tsx b/src/views/CanvasOrderbook/CanvasOrderbook.tsx index e9ad3371a..da6a39b76 100644 --- a/src/views/CanvasOrderbook/CanvasOrderbook.tsx +++ b/src/views/CanvasOrderbook/CanvasOrderbook.tsx @@ -155,7 +155,7 @@ export const CanvasOrderbook = forwardRef( const asksOrderbook = ( <$OrderbookSideContainer $side="asks" $rows={rowsPerSide}> <$HoverRows $bottom={layout !== 'horizontal'}> - {asksSlice.toReversed().map((row, idx) => + {[...asksSlice].reverse().map((row, idx) => row ? ( <$Row // eslint-disable-next-line react/no-array-index-key diff --git a/src/views/CanvasOrderbook/OrderbookControls.tsx b/src/views/CanvasOrderbook/OrderbookControls.tsx index 8b002ee88..64feaa26a 100644 --- a/src/views/CanvasOrderbook/OrderbookControls.tsx +++ b/src/views/CanvasOrderbook/OrderbookControls.tsx @@ -36,7 +36,7 @@ export const OrderbookControls = ({ const dispatch = useDispatch(); const displayUnit = useAppSelector(getSelectedDisplayUnit); const fractionDigits = - (groupingTickSizeDecimals ?? 0) <= 1 ? USD_DECIMALS : groupingTickSizeDecimals; + (groupingTickSizeDecimals ?? 0) === 1 ? USD_DECIMALS : groupingTickSizeDecimals; const onToggleDisplayUnit = useCallback( (newValue: DisplayUnit) => { diff --git a/src/views/MidMarketPrice.tsx b/src/views/MidMarketPrice.tsx index d9b8e3cd6..482b45c94 100644 --- a/src/views/MidMarketPrice.tsx +++ b/src/views/MidMarketPrice.tsx @@ -37,11 +37,11 @@ export const MidMarketPrice = () => { useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) ); - const midMarketPriceLoading = ['pending', 'idle'].includes( - useAppSelector(BonsaiHelpers.currentMarket.midPrice.loading) - ); - const midMarketPrice = useAppSelector(BonsaiHelpers.currentMarket.midPrice.data); + const midMarketPriceLoading = useAppSelector(BonsaiHelpers.currentMarket.midPrice.loading); + const isLoading = + midMarketPriceLoading === 'idle' || + (midMarketPriceLoading === 'pending' && midMarketPrice == null); const lastMidMarketPrice = useRef(midMarketPrice); @@ -54,7 +54,7 @@ export const MidMarketPrice = () => { lastMidMarketPrice.current = midMarketPrice; }, [midMarketPrice]); - if (midMarketPriceLoading) { + if (isLoading) { return ; }