diff --git a/.eslintrc.json b/.eslintrc.json index 504863778..a3cf3ea65 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -57,7 +57,8 @@ "!@/bonsai/ontology", "!@/bonsai/public-calculators/*", "!@/bonsai/lib/*", - "!@/bonsai/types/summaryTypes" + "!@/bonsai/types/summaryTypes", + "!@/bonsai/types/orderbookTypes" ] } ], 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/bonsai/calculators/orderbook.ts b/src/bonsai/calculators/orderbook.ts index 8bb62d4c7..bf7bf7c3e 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 { orEmptyObj, orEmptyRecord } from '@/lib/typeUtils'; +import { OrderbookLine, SubaccountOpenOrderPriceMap } from '../types/orderbookTypes'; import { OrderbookData } from '../types/rawTypes'; -import { OrderbookLine } from '../types/summaryTypes'; +import { SubaccountOrder } from '../types/summaryTypes'; type OrderbookLineBN = { price: BigNumber; @@ -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,67 +92,72 @@ 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', + } = orEmptyObj(options); + // 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].reverse() + : currentMarketOrderbook.asks + ).map(mapOrderbookLineToNumber); + + const bids = ( + bidsSortOrder === 'desc' + ? currentMarketOrderbook.bids + : [...currentMarketOrderbook.bids].reverse() + ).map(mapOrderbookLineToNumber); return { - asks: orderBy( - currentMarketOrderbook.asks.map(mapOrderbookLineToNumber), - (ask) => ask.price, - 'desc' - ), - bids: currentMarketOrderbook.bids.map(mapOrderbookLineToNumber), - asksMap: Object.fromEntries( - currentMarketOrderbook.asks.map((line) => [ - line.price.toFixed(tickSizeDecimals), - mapOrderbookLineToNumber(line), - ]) - ), - bidsMap: Object.fromEntries( - currentMarketOrderbook.bids.map((line) => [ - line.price.toFixed(tickSizeDecimals), - mapOrderbookLineToNumber(line), - ]) - ), + asks, + bids, spread: currentMarketOrderbook.spread?.toNumber(), spreadPercent: currentMarketOrderbook.spreadPercent?.toNumber(), midPrice: currentMarketOrderbook.midPrice?.toNumber(), }; } - const groupingTickSize = getGroupingTickSize(tickSize, groupingMultiplier); + const { groupingTickSize, groupingTickSizeDecimals } = getGroupingTickSize( + tickSize, + groupingMultiplier + ); + const { asks, bids } = currentMarketOrderbook; - const groupedAsks = orEmptyRecord(group(asks, groupingTickSize)); - const groupedBids = orEmptyRecord(group(bids, groupingTickSize, true)); - // Convert groupedAsks and groupedBids to list and sort by price. Asks will now be descending and bids will be remain descending + const groupedAsks = orEmptyRecord(group(asks, groupingTickSize, groupingTickSizeDecimals)); + const groupedBids = orEmptyRecord( + group(bids, groupingTickSize, groupingTickSizeDecimals, true) + ); + + // Convert groupedAsks and groupedBids to list and sort by price depending on options. Default is descending. const asksList = orderBy( Object.values(groupedAsks).map(mapOrderbookLineToNumber), - (ask) => ask.price, - 'desc' + [(ask) => ask.price], + [asksSortOrder] ); const bidsList = orderBy( Object.values(groupedBids).map(mapOrderbookLineToNumber), (bid) => bid.price, - 'desc' + [bidsSortOrder] ); - const lowestAsk = asksList.at(-1); - const highestBid = bidsList.at(0); + const lowestAsk = asksList.at(asksSortOrder === 'desc' ? -1 : 0); + const highestBid = bidsList.at(bidsSortOrder === 'desc' ? 0 : -1); return { asks: asksList, - asksMap: groupedAsks, bids: bidsList, - bidsMap: groupedBids, spread: currentMarketOrderbook.spread?.toNumber(), spreadPercent: currentMarketOrderbook.spreadPercent?.toNumber(), midPrice: roundMidPrice(lowestAsk, highestBid, groupingTickSize)?.toNumber(), @@ -153,6 +166,7 @@ export const formatOrderbook = weakMapMemoize( ); /** ------ .map Helper Functions ------ */ + function mapRawOrderbookLineToBN( rawOrderbookLineEntry: [string, { size: string; offset: number }] ) { @@ -256,9 +270,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 +286,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 +305,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 +348,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 9aecf3d8f..33306d419 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'; @@ -49,7 +51,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, @@ -61,6 +67,7 @@ import { StablePerpetualMarketSummary, } from './selectors/summary'; import { selectUserStats } from './selectors/userStats'; +import { DepthChartData, OrderbookProcessedData } from './types/orderbookTypes'; import { AccountBalances, AllAssetData, @@ -69,7 +76,6 @@ import { EquityTiersSummary, FeeTierSummary, GroupedSubaccountSummary, - OrderbookProcessedData, PendingIsolatedPosition, PerpetualMarketSummaries, PerpetualMarketSummary, @@ -228,7 +234,18 @@ interface BonsaiHelpersShape { fills: BasicSelector; }; orderbook: { - createSelectGroupedData: ParameterizedSelector; + createSelectGroupedData: ParameterizedSelector< + OrderbookProcessedData | undefined, + [GroupingMultiplier | undefined] + >; + loading: BasicSelector; + }; + midPrice: { + data: BasicSelector; + loading: BasicSelector; + }; + depthChart: { + data: BasicSelector; loading: BasicSelector; }; }; @@ -267,7 +284,15 @@ export const BonsaiHelpers: BonsaiHelpersShape = { assetLogo: selectCurrentMarketAssetLogoUrl, assetName: selectCurrentMarketAssetName, orderbook: { - createSelectGroupedData: createSelectCurrentMarketGroupedOrderbook, + createSelectGroupedData: createSelectCurrentMarketOrderbook, + loading: selectCurrentMarketOrderbookLoading, + }, + midPrice: { + data: selectCurrentMarketMidPrice, + loading: selectCurrentMarketOrderbookLoading, + }, + depthChart: { + data: selectCurrentMarketDepthChart, loading: selectCurrentMarketOrderbookLoading, }, account: { diff --git a/src/bonsai/selectors/orderbook.ts b/src/bonsai/selectors/orderbook.ts new file mode 100644 index 000000000..977af1836 --- /dev/null +++ b/src/bonsai/selectors/orderbook.ts @@ -0,0 +1,155 @@ +import { weakMapMemoize } from 'reselect'; + +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 { CanvasOrderbookLine } from '../types/orderbookTypes'; +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) => { + 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, + getCanvasOrderbookOptions(groupingMultiplier) + ); + + const bids: CanvasOrderbookLine[] = + formattedOrderbook?.bids.map((line) => ({ + ...line, + side: 'bid', + mine: findMine({ + price: line.price, + side: IndexerOrderSide.BUY, + orderMap: subaccountOpenOrdersPriceMap, + groupingTickSize, + groupingTickSizeDecimals, + }), + })) ?? EMPTY_ARR; + + const asks: CanvasOrderbookLine[] = + 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, + groupingTickSize, + groupingTickSizeDecimals, + }; + } + ); + +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, DEPTH_CHART_OPTIONS); + + 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 a04f54e53..9813d6664 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/currentMarketSelectors'; 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], @@ -105,20 +98,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..313fad889 --- /dev/null +++ b/src/bonsai/types/orderbookTypes.ts @@ -0,0 +1,53 @@ +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; + size: number; + depth: number; + sizeCost: number; + depthCost: number; + offset: number; +}; + +export type CanvasOrderbookLine = OrderbookLine & { + mine: number | undefined; + side: 'ask' | 'bid'; +}; + +export type OrderbookProcessedData = { + asks: CanvasOrderbookLine[]; + bids: CanvasOrderbookLine[]; + midPrice: number | undefined; + spread: number | undefined; + spreadPercent: number | undefined; + groupingTickSize: number; + groupingTickSizeDecimals: number; +}; + +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 b23bd6894..5d9031ffa 100644 --- a/src/bonsai/types/summaryTypes.ts +++ b/src/bonsai/types/summaryTypes.ts @@ -301,21 +301,4 @@ export type AccountBalances = { chainTokenAmount?: string; }; -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 type SubaccountTransfer = IndexerTransferResponseObject; 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 7ffce77f5..b804d61f4 100644 --- a/src/hooks/Orderbook/useDrawOrderbook.ts +++ b/src/hooks/Orderbook/useDrawOrderbook.ts @@ -1,9 +1,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { BonsaiHelpers } from '@/bonsai/ontology'; -import { shallowEqual } from 'react-redux'; +import { CanvasOrderbookLine } from '@/bonsai/types/orderbookTypes'; -import type { PerpetualMarketOrderbookLevel } from '@/constants/abacus'; import { SMALL_USD_DECIMALS, TOKEN_DECIMALS } from '@/constants/numbers'; import { ORDERBOOK_ANIMATION_DURATION, @@ -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'; @@ -36,9 +34,9 @@ import { orEmptyObj } from '@/lib/typeUtils'; import { useLocaleSeparators } from '../useLocaleSeparators'; type ElementProps = { - data: Array; + data: Array; histogramRange: number; - side: PerpetualMarketOrderbookLevel['side']; + side: CanvasOrderbookLine['side']; displayUnit: DisplayUnit; }; @@ -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); @@ -325,7 +330,7 @@ export const useDrawOrderbook = ({ }: { ctx: CanvasRenderingContext2D; idx: number; - rowToRender?: PerpetualMarketOrderbookLevel; + rowToRender?: CanvasOrderbookLine; animationType?: OrderbookRowAnimationType; }) => { if (!rowToRender) return; @@ -358,7 +363,7 @@ export const useDrawOrderbook = ({ drawText({ animationType, ctx, - depth: depth ?? undefined, + depth, depthCost, sizeCost, price, @@ -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/Orderbook/useOrderbookValues.ts b/src/hooks/Orderbook/useOrderbookValues.ts index 30a8eb5a9..2ab532c33 100644 --- a/src/hooks/Orderbook/useOrderbookValues.ts +++ b/src/hooks/Orderbook/useOrderbookValues.ts @@ -1,58 +1,61 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo, useState } from 'react'; -import { OrderSide } from '@dydxprotocol/v4-client-js'; -import { shallowEqual } from 'react-redux'; +import { BonsaiHelpers } from '@/bonsai/ontology'; -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'; -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 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 ); + 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() ?? [] - ) - .map( - (row: OrderbookLine, idx: number): PerpetualMarketOrderbookLevel => - 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(0, rowsPerSide); - const bids: Array = ( - orderbook?.bids?.toArray() ?? [] - ) - .map( - (row: OrderbookLine, idx: number): PerpetualMarketOrderbookLevel => - 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); @@ -67,47 +70,17 @@ export const useCalculateOrderbookData = ({ rowsPerSide }: { rowsPerSide: number return { asks, bids, + midMarketPrice: orderbook?.midPrice, spread, spreadPercent, histogramRange, hasOrderbook: !!orderbook, - currentGrouping: orderbook?.grouping, - }; - }, [rowsPerSide, orderbook, subaccountOrderSizeBySideAndPrice]); -}; - -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 grouping + groupingMultiplier, + groupingTickSize: orderbook?.groupingTickSize, + groupingTickSizeDecimals: orderbook?.groupingTickSizeDecimals, + modifyGroupingMultiplier, }; - }, [orderbook]); + }, [rowsPerSide, orderbook, groupingMultiplier, modifyGroupingMultiplier]); }; 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 6382b678d..d4367ac59 100644 --- a/src/lib/abacus/stateNotification.ts +++ b/src/lib/abacus/stateNotification.ts @@ -1,12 +1,11 @@ // 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, AbacusStateNotificationProtocol, AccountBalance, - MarketOrderbook, Nullable, ParsingErrors, PerpetualState, @@ -16,7 +15,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 { @@ -34,7 +32,6 @@ import { import { setInputs } from '@/state/inputs'; import { setLatestOrder, updateFilledOrders, updateOrders } from '@/state/localOrders'; import { updateNotifications } from '@/state/notifications'; -import { setOrderbook } from '@/state/perpetuals'; import { track } from '../analytics/analytics'; @@ -45,10 +42,6 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { this.store = undefined; } - private throttledOrderbookUpdateByMarketId: { - [marketId: string]: (orderbook: MarketOrderbook) => void; - } = {}; - environmentsChanged(): void {} notificationsChanged(notifications: kollections.List): void { @@ -62,7 +55,6 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { if (!this.store) return; const { dispatch } = this.store; const changes = new Set(incomingChanges?.changes.toArray() ?? []); - const marketIds = incomingChanges?.markets?.toArray(); const subaccountNumbers = incomingChanges?.subaccountNumbers?.toArray(); if (updatedState) { @@ -129,20 +121,6 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { } } }); - - 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 fc8c345cf..801519551 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 33331946c..6080a6df7 100644 --- a/src/state/perpetuals.ts +++ b/src/state/perpetuals.ts @@ -1,13 +1,10 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import merge from 'lodash/merge'; -import type { MarketOrderbook, Nullable } 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; @@ -15,22 +12,11 @@ export interface PerpetualsState { currentMarketIdIfTradeable?: string; marketFilter: MarketFilters; launchMarketIds: string[]; - - orderbooks?: Record; - orderbooksMap?: Record< - string, - { - asks: Record; - bids: Record; - } - >; } const initialState: PerpetualsState = { currentMarketId: undefined, currentMarketIdIfTradeable: undefined, - orderbooks: undefined, - orderbooksMap: undefined, marketFilter: MarketFilters.ALL, launchMarketIds: [], }; @@ -51,27 +37,6 @@ export const perpetualsSlice = createSlice({ ) => { state.currentMarketIdIfTradeable = action.payload; }, - 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, @@ -95,7 +60,6 @@ export const perpetualsSlice = createSlice({ export const { setCurrentMarketId, setCurrentMarketIdIfTradeable, - 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 ea6917726..da6a39b76 100644 --- a/src/views/CanvasOrderbook/CanvasOrderbook.tsx +++ b/src/views/CanvasOrderbook/CanvasOrderbook.tsx @@ -4,9 +4,9 @@ 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'; @@ -22,7 +22,6 @@ import { Tag } from '@/components/Tag'; import { useAppDispatch, useAppSelector } from '@/state/appTypes'; import { getSelectedDisplayUnit } from '@/state/appUiConfigsSelectors'; -import { getCurrentMarketId } from '@/state/currentMarketSelectors'; import { setTradeFormInputs } from '@/state/inputs'; import { getCurrentInput } from '@/state/inputsSelectors'; @@ -54,17 +53,26 @@ export const CanvasOrderbook = forwardRef( }: ElementProps & StyleProps, ref: React.ForwardedRef ) => { - const { asks, bids, hasOrderbook, histogramRange, currentGrouping } = useCalculateOrderbookData( - { - rowsPerSide, - } - ); + const { + asks, + bids, + midMarketPrice, + hasOrderbook, + histogramRange, + groupingTickSize, + groupingTickSizeDecimals, + modifyGroupingMultiplier, + } = useCalculateOrderbookData({ + rowsPerSide, + }); + + const { + assetId: id, + ticker, + tickSizeDecimals = USD_DECIMALS, + } = orEmptyObj(useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo)); const stringGetter = useStringGetter(); - const currentMarket = useAppSelector(getCurrentMarketId) ?? ''; - const { assetId: id, tickSizeDecimals = USD_DECIMALS } = orEmptyObj( - useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) - ); /** * Slice asks and bids to rowsPerSide using empty rows @@ -75,30 +83,25 @@ export const CanvasOrderbook = forwardRef( ? new Array(rowsPerSide - asks.length).fill(undefined) : []; - const newAsksSlice: Array = [ - ...emptyAskRows, - ...asks.reverse(), - ]; + const newAsksSlice = [...asks, ...emptyAskRows]; const emptyBidRows = bids.length < rowsPerSide ? new Array(rowsPerSide - bids.length).fill(undefined) : []; - const newBidsSlice: Array = [ - ...bids, - ...emptyBidRows, - ]; + const newBidsSlice = [...bids, ...emptyBidRows]; return { - asksSlice: layout === 'horizontal' ? newAsksSlice : newAsksSlice.reverse(), + asksSlice: layout === 'horizontal' ? newAsksSlice.reverse() : newAsksSlice, bidsSlice: newBidsSlice, }; }, [asks, bids, layout, rowsPerSide]); const orderbookRef = useRef(null); + useCenterOrderbook({ orderbookRef, - marketId: currentMarket, + marketId: ticker ?? '', disabled: layout === 'horizontal', }); @@ -123,7 +126,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), }) ); } @@ -152,7 +155,7 @@ export const CanvasOrderbook = forwardRef( const asksOrderbook = ( <$OrderbookSideContainer $side="asks" $rows={rowsPerSide}> <$HoverRows $bottom={layout !== 'horizontal'}> - {[...asksSlice].reverse().map((row: PerpetualMarketOrderbookLevel | undefined, idx) => + {[...asksSlice].reverse().map((row, idx) => row ? ( <$Row // eslint-disable-next-line react/no-array-index-key @@ -174,7 +177,7 @@ export const CanvasOrderbook = forwardRef( const bidsOrderbook = ( <$OrderbookSideContainer $side="bids" $rows={rowsPerSide}> <$HoverRows> - {bidsSlice.map((row: PerpetualMarketOrderbookLevel | undefined, idx) => + {bidsSlice.map((row, idx) => row ? ( <$Row // eslint-disable-next-line react/no-array-index-key @@ -201,7 +204,14 @@ export const CanvasOrderbook = forwardRef( return (
<$OrderbookContent $isLoading={!hasOrderbook}> - {!hideHeader && } + {!hideHeader && ( + + )} {!hideHeader && ( <$OrderbookRow tw="h-1.75 text-color-text-0"> @@ -221,6 +231,7 @@ export const CanvasOrderbook = forwardRef( {(displaySide === 'top' || layout === 'horizontal') && ( <$OrderbookMiddleRow side="top" + midMarketPrice={midMarketPrice} tickSizeDecimals={tickSizeDecimals} isHeader={layout === 'horizontal'} /> @@ -230,13 +241,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 609229289..64feaa26a 100644 --- a/src/views/CanvasOrderbook/OrderbookControls.tsx +++ b/src/views/CanvasOrderbook/OrderbookControls.tsx @@ -1,11 +1,8 @@ import { useCallback } from 'react'; -import { BonsaiHelpers } from '@/bonsai/ontology'; -import { clamp } from 'lodash'; 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 { DisplayUnit } from '@/constants/trade'; @@ -19,32 +16,27 @@ import { useAppSelector } from '@/state/appTypes'; import { setDisplayUnit } from '@/state/appUiConfigs'; import { getSelectedDisplayUnit } from '@/state/appUiConfigsSelectors'; -import abacusStateManager from '@/lib/abacus'; import { getDisplayableAssetFromBaseAsset } from '@/lib/assetUtils'; type OrderbookControlsProps = { className?: string; assetId?: string; - grouping: Nullable; + groupingTickSize?: number; + groupingTickSizeDecimals?: number; + modifyGrouping: (increase: boolean) => void; }; -export const OrderbookControls = ({ className, assetId, grouping }: OrderbookControlsProps) => { +export const OrderbookControls = ({ + className, + assetId, + groupingTickSize, + groupingTickSizeDecimals, + 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 tickSizeDecimals = - useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo)?.tickSizeDecimals ?? USD_DECIMALS; + const fractionDigits = + (groupingTickSizeDecimals ?? 0) === 1 ? USD_DECIMALS : groupingTickSizeDecimals; const onToggleDisplayUnit = useCallback( (newValue: DisplayUnit) => { @@ -70,7 +62,7 @@ export const OrderbookControls = ({ className, assetId, grouping }: OrderbookCon size={ButtonSize.XSmall} shape={ButtonShape.Square} buttonStyle={ButtonStyle.WithoutBackground} - onClick={() => modifyScale(-1)} + onClick={() => modifyGrouping(false)} > - @@ -78,7 +70,7 @@ export const OrderbookControls = ({ className, assetId, grouping }: OrderbookCon size={ButtonSize.XSmall} shape={ButtonShape.Square} buttonStyle={ButtonStyle.WithoutBackground} - onClick={() => modifyScale(1)} + onClick={() => modifyGrouping(true)} > + @@ -86,9 +78,9 @@ export const OrderbookControls = ({ className, assetId, grouping }: OrderbookCon
{assetId && ( 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 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/MidMarketPrice.tsx b/src/views/MidMarketPrice.tsx index fc2d7a90b..482b45c94 100644 --- a/src/views/MidMarketPrice.tsx +++ b/src/views/MidMarketPrice.tsx @@ -1,35 +1,34 @@ 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 { layoutMixins } from '@/styles/layoutMixins'; import { LoadingDots } from '@/components/Loading/LoadingDots'; import { Output, OutputType } from '@/components/Output'; import { useAppSelector } from '@/state/appTypes'; -import { getCurrentMarketMidMarketPrice } from '@/state/perpetualsSelectors'; -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)'; }; @@ -37,7 +36,13 @@ export const MidMarketPrice = () => { const { tickSizeDecimals } = orEmptyObj( useAppSelector(BonsaiHelpers.currentMarket.stableMarketInfo) ); - const midMarketPrice = useAppSelector(getCurrentMarketMidMarketPrice); + + 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); const midMarketColor = getMidMarketPriceColor({ @@ -49,7 +54,11 @@ export const MidMarketPrice = () => { lastMidMarketPrice.current = midMarketPrice; }, [midMarketPrice]); - return midMarketPrice !== undefined ? ( + if (isLoading) { + return ; + } + + return ( <$Output withSubscript type={OutputType.Fiat} @@ -57,10 +66,9 @@ export const MidMarketPrice = () => { color={midMarketColor} fractionDigits={tickSizeDecimals} /> - ) : ( - ); }; + const $Output = styled(Output)<{ color?: string }>` ${layoutMixins.row} diff --git a/src/views/charts/DepthChart/Tooltip.tsx b/src/views/charts/DepthChart/Tooltip.tsx index f03b8e9e7..d738b1c2e 100644 --- a/src/views/charts/DepthChart/Tooltip.tsx +++ b/src/views/charts/DepthChart/Tooltip.tsx @@ -13,7 +13,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'; @@ -23,6 +22,7 @@ import { TooltipContent } from '@/components/visx/TooltipContent'; import { useAppSelector } from '@/state/appTypes'; import { MustBigNumber } from '@/lib/numbers'; +import { orEmptyObj } from '@/lib/typeUtils'; type DepthChartTooltipProps = { chartPointAtPointer: DepthChartPoint; @@ -41,20 +41,22 @@ export const DepthChartTooltipContent = ({ }: DepthChartTooltipProps) => { const { nearestDatum } = tooltipData ?? {}; const stringGetter = useStringGetter(); - const { spread, spreadPercent, midMarketPrice } = useOrderbookValuesForDepthChart(); const id = useAppSelector(BonsaiHelpers.currentMarket.assetId) ?? ''; + 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]; } @@ -136,11 +138,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 8eaf725fe..3b2b4cbc6 100644 --- a/src/views/charts/DepthChart/index.tsx +++ b/src/views/charts/DepthChart/index.tsx @@ -26,7 +26,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'; @@ -73,8 +72,9 @@ export const DepthChart = ({ assetId: id, } = 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 @@ -86,44 +86,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) => { @@ -140,12 +140,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( @@ -161,7 +161,7 @@ export const DepthChart = ({ // Render conditions - if (!(zoomDomain && midMarketPrice && asks.length && bids.length)) + if (!(zoomDomain && midPrice && asks?.length && bids?.length)) return ; // Events @@ -176,14 +176,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) ) ); }; @@ -217,8 +211,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)} > @@ -237,15 +231,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} @@ -272,11 +266,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} @@ -293,21 +291,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: { price: number; depth: number } | undefined) => datum?.price ?? 0} + yAccessor={(datum: { price: number; depth: number } | undefined) => datum?.depth ?? 0} /> @@ -324,7 +322,7 @@ export const DepthChart = ({ value={ isEditingOrder && chartPointAtPointer ? chartPointAtPointer.price - : tooltipData!.nearestDatum?.datum.price + : tooltipData!.nearestDatum?.datum?.price } useGrouping={false} accentColor={ 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'; 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(() => {