From a1fbe18bf11161bf900c19784612970bbb8509d3 Mon Sep 17 00:00:00 2001 From: saml33 Date: Tue, 18 Jul 2023 11:18:01 +1000 Subject: [PATCH] remove orderbook subscription hook and other fixes --- components/trade/DepthChart.tsx | 91 +++++-- components/trade/MarketCloseModal.tsx | 2 +- components/trade/MobileTradeAdvancedPage.tsx | 3 +- components/trade/Orderbook.tsx | 193 +++++++++------ components/trade/OrderbookAndTrades.tsx | 16 +- components/trade/TradeAdvancedPage.tsx | 5 +- hooks/useOrderbookSubscription.ts | 237 ------------------- public/locales/en/account.json | 5 - types/index.ts | 17 ++ utils/orderbook.ts | 164 +++++++++++++ 10 files changed, 375 insertions(+), 358 deletions(-) delete mode 100644 hooks/useOrderbookSubscription.ts create mode 100644 utils/orderbook.ts diff --git a/components/trade/DepthChart.tsx b/components/trade/DepthChart.tsx index 7f2b300d..b8a791b7 100644 --- a/components/trade/DepthChart.tsx +++ b/components/trade/DepthChart.tsx @@ -1,8 +1,5 @@ import Slider from '@components/forms/Slider' import useMarkPrice from 'hooks/useMarkPrice' -import useOrderbookSubscription, { - cumOrderbookSide, -} from 'hooks/useOrderbookSubscription' import useSelectedMarket from 'hooks/useSelectedMarket' import { useViewport } from 'hooks/useViewport' import { useTheme } from 'next-themes' @@ -22,6 +19,8 @@ import { COLORS } from 'styles/colors' import { floorToDecimal, getDecimalCount } from 'utils/numbers' import { gridBreakpoints } from './TradeAdvancedPage' import { CartesianViewBox } from 'recharts/types/util/types' +import { cumOrderbookSide } from 'types' +import mangoStore from '@store/mangoStore' type LabelPosition = | 'left' @@ -40,14 +39,6 @@ type LabelPosition = const Y_TICK_COUNT = 10 -// content: (props)=> {…} -// fill: "var(--fgd-2)" -// fontSize: 9 -// offset: 7 -// position: "left" -// value: "0.0" -// viewBox: {x: 52, y: 452, width: 82, height: 0} - interface CustomLabel extends LabelProps { viewBox?: CartesianViewBox } @@ -69,20 +60,52 @@ const MarkPriceLabel = ({ value, viewBox }: CustomLabel) => { } else return null } -const DepthChart = ({ grouping }: { grouping: number }) => { +type RawOrderbook = number[][] +type DepthOrderbookSide = { + price: number + size: number + cumulativeSize: number +} + +const DepthChart = () => { const { theme } = useTheme() const { serumOrPerpMarket } = useSelectedMarket() - const [priceRangePercent, setPriceRangePercentPercent] = useState('5') const [mouseData, setMouseData] = useState(null) const markPrice = useMarkPrice() - const orderbook = useOrderbookSubscription(100, grouping) + const orderbook = mangoStore((s) => s.selectedMarket.orderbook) + const [priceRangePercent, setPriceRangePercentPercent] = useState('10') const { width } = useViewport() const increaseHeight = width ? width > gridBreakpoints.xxxl : false + const formatOrderbookData = (orderbook: RawOrderbook, markPrice: number) => { + const maxPrice = markPrice * 4 + const minPrice = markPrice / 4 + const formattedBook = [] + + let cumulativeSize = 0 + + for (let i = 0; i < orderbook.length; i++) { + const [price, size] = orderbook[i] + + cumulativeSize += size + + const object = { + price: price, + size: size, + cumulativeSize: cumulativeSize, + } + + if (price >= minPrice && price <= maxPrice) { + formattedBook.push(object) + } + } + return formattedBook + } + // format chart data for the bids and asks series const mergeCumulativeData = ( - bids: cumOrderbookSide[], - asks: cumOrderbookSide[] + bids: DepthOrderbookSide[], + asks: DepthOrderbookSide[] ) => { const bidsWithSide = bids.map((b) => ({ ...b, bids: b.cumulativeSize })) const asksWithSide = asks.map((a) => ({ ...a, asks: a.cumulativeSize })) @@ -90,13 +113,15 @@ const DepthChart = ({ grouping }: { grouping: number }) => { } const chartData = useMemo(() => { - if (!orderbook) return [] - return mergeCumulativeData(orderbook.bids, orderbook.asks) - }, [orderbook]) + if (!orderbook || !serumOrPerpMarket || !markPrice) return [] + const formattedBids = formatOrderbookData(orderbook.bids, markPrice) + const formattedAsks = formatOrderbookData(orderbook.asks, markPrice) + return mergeCumulativeData(formattedBids, formattedAsks) + }, [markPrice, orderbook, serumOrPerpMarket]) // find the max value for the x-axis const findXDomainMax = ( - data: cumOrderbookSide[], + data: DepthOrderbookSide[], yMin: number, yMax: number ) => { @@ -177,7 +202,7 @@ const DepthChart = ({ grouping }: { grouping: number }) => { const priceFormatter = useCallback( (price: number) => { - if (!serumOrPerpMarket) return price.toFixed(1) + if (!serumOrPerpMarket) return price.toFixed() const tickDecimals = getDecimalCount(serumOrPerpMarket.tickSize) if (tickDecimals >= 7) { return price.toExponential(3) @@ -186,6 +211,15 @@ const DepthChart = ({ grouping }: { grouping: number }) => { [serumOrPerpMarket] ) + const xTickFormatter = useCallback( + (size: number) => { + if (!serumOrPerpMarket) return size.toFixed() + const minOrderDecimals = getDecimalCount(serumOrPerpMarket.minOrderSize) + return size.toFixed(minOrderDecimals) + }, + [serumOrPerpMarket] + ) + const isWithinRangeOfTick = useCallback( (value: number, baseValue: number) => { const difference = Math.abs(value - baseValue) @@ -265,7 +299,7 @@ const DepthChart = ({ grouping }: { grouping: number }) => { setMouseData(null) } - return ( + return chartData.length ? ( <>
@@ -274,7 +308,7 @@ const DepthChart = ({ grouping }: { grouping: number }) => { setPriceRangePercentPercent(p)} step={0.5} @@ -300,6 +334,7 @@ const DepthChart = ({ grouping }: { grouping: number }) => { type="number" tick={false} tickLine={false} + tickFormatter={(tick) => xTickFormatter(tick)} /> { tickFormatter={(tick) => yTickFormatter(tick)} /> {
- ) + ) : null } export default DepthChart diff --git a/components/trade/MarketCloseModal.tsx b/components/trade/MarketCloseModal.tsx index e525717c..aed127c2 100644 --- a/components/trade/MarketCloseModal.tsx +++ b/components/trade/MarketCloseModal.tsx @@ -16,7 +16,7 @@ import { SOUND_SETTINGS_KEY } from 'utils/constants' import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings' import { Howl } from 'howler' import { isMangoError } from 'types' -import { decodeBook, decodeBookL2 } from './Orderbook' +import { decodeBook, decodeBookL2 } from 'utils/orderbook' interface MarketCloseModalProps { onClose: () => void diff --git a/components/trade/MobileTradeAdvancedPage.tsx b/components/trade/MobileTradeAdvancedPage.tsx index 2734bdc4..a4920aa8 100644 --- a/components/trade/MobileTradeAdvancedPage.tsx +++ b/components/trade/MobileTradeAdvancedPage.tsx @@ -11,7 +11,6 @@ import FavoriteMarketsBar from './FavoriteMarketsBar' const MobileTradeAdvancedPage = () => { const [activeTab, setActiveTab] = useState('trade:book') - const [grouping, setGrouping] = useState(0.01) const [showChart, setShowChart] = useState(false) return (
@@ -42,7 +41,7 @@ const MobileTradeAdvancedPage = () => { />
- +
diff --git a/components/trade/Orderbook.tsx b/components/trade/Orderbook.tsx index b586072c..a1c37833 100644 --- a/components/trade/Orderbook.tsx +++ b/components/trade/Orderbook.tsx @@ -1,7 +1,7 @@ -import { AccountInfo, PublicKey } from '@solana/web3.js' +import { PublicKey } from '@solana/web3.js' import mangoStore from '@store/mangoStore' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Market, Orderbook as SpotOrderBook } from '@project-serum/serum' +import { Market } from '@project-serum/serum' import useLocalStorageState from 'hooks/useLocalStorageState' import { floorToDecimal, @@ -18,20 +18,23 @@ import Decimal from 'decimal.js' import Tooltip from '@components/shared/Tooltip' import GroupSize from './GroupSize' import { useViewport } from 'hooks/useViewport' -import { - BookSide, - BookSideType, - MangoClient, - PerpMarket, - Serum3Market, -} from '@blockworks-foundation/mango-v4' +import { BookSide, Serum3Market } from '@blockworks-foundation/mango-v4' import useSelectedMarket from 'hooks/useSelectedMarket' import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings' import { OrderbookFeed } from '@blockworks-foundation/mango-feeds' -import useOrderbookSubscription from 'hooks/useOrderbookSubscription' import Switch from '@components/forms/Switch' import { gridBreakpoints } from './TradeAdvancedPage' import { breakpoints } from 'utils/theme' +import { + decodeBook, + decodeBookL2, + getCumulativeOrderbookSide, + getMarket, + groupBy, + updatePerpMarketOnGroup, +} from 'utils/orderbook' +import { OrderbookData, OrderbookL2 } from 'types' +import { isEqual } from 'lodash' const sizeCompacter = Intl.NumberFormat('en', { maximumFractionDigits: 6, @@ -40,72 +43,13 @@ const sizeCompacter = Intl.NumberFormat('en', { const SHOW_EXPONENTIAL_THRESHOLD = 0.00001 -const getMarket = () => { - const group = mangoStore.getState().group - const selectedMarket = mangoStore.getState().selectedMarket.current - if (!group || !selectedMarket) return - return selectedMarket instanceof PerpMarket - ? selectedMarket - : group?.getSerum3ExternalMarket(selectedMarket.serumMarketExternal) -} - -export const decodeBookL2 = (book: SpotOrderBook | BookSide): number[][] => { - const depth = 300 - if (book instanceof SpotOrderBook) { - return book.getL2(depth).map(([price, size]) => [price, size]) - } else if (book instanceof BookSide) { - return book.getL2Ui(depth) - } - return [] -} - -export function decodeBook( - client: MangoClient, - market: Market | PerpMarket, - accInfo: AccountInfo, - side: 'bids' | 'asks' -): SpotOrderBook | BookSide { - if (market instanceof Market) { - const book = SpotOrderBook.decode(market, accInfo.data) - return book - } else { - const decodedAcc = client.program.coder.accounts.decode( - 'bookSide', - accInfo.data - ) - const book = BookSide.from( - client, - market, - side === 'bids' ? BookSideType.bids : BookSideType.asks, - decodedAcc - ) - return book - } -} - -const updatePerpMarketOnGroup = (book: BookSide, side: 'bids' | 'asks') => { - const group = mangoStore.getState().group - const perpMarket = group?.getPerpMarketByMarketIndex( - book.perpMarket.perpMarketIndex - ) - if (perpMarket) { - perpMarket[`_${side}`] = book - // mangoStore.getState().actions.fetchOpenOrders() - } -} - -const Orderbook = ({ - grouping, - setGrouping, -}: { - grouping: number - setGrouping: (g: number) => void -}) => { +const Orderbook = () => { const { t } = useTranslation(['common', 'trade']) const { serumOrPerpMarket: market } = useSelectedMarket() const connection = mangoStore((s) => s.connection) const [tickSize, setTickSize] = useState(0) + const [grouping, setGrouping] = useState(0.01) const [useOrderbookFeed, setUseOrderbookFeed] = useState( localStorage.getItem(USE_ORDERBOOK_FEED_KEY) !== null ? localStorage.getItem(USE_ORDERBOOK_FEED_KEY) === 'true' @@ -117,13 +61,13 @@ const Orderbook = ({ ) const { width } = useViewport() const isMobile = width ? width < breakpoints.lg : false + const [orderbookData, setOrderbookData] = useState(null) + const currentOrderbookData = useRef() const depth = useMemo(() => { return width > gridBreakpoints.xxxl ? 12 : 10 }, [width]) - const orderbookData = useOrderbookSubscription(depth, grouping) - const depthArray: number[] = useMemo(() => { return Array(depth).fill(0) }, [depth]) @@ -151,6 +95,109 @@ const Orderbook = ({ return asksPk.toString() }, [market]) + useEffect( + () => + mangoStore.subscribe( + (state) => state.selectedMarket.orderbook, + (newOrderbook) => { + if ( + newOrderbook && + market && + !isEqual(currentOrderbookData.current, newOrderbook) + ) { + // check if user has open orders so we can highlight them on orderbook + const openOrders = mangoStore.getState().mangoAccount.openOrders + const marketPk = market.publicKey.toString() + const bids2 = mangoStore.getState().selectedMarket.bidsAccount + const asks2 = mangoStore.getState().selectedMarket.asksAccount + const mangoAccount = mangoStore.getState().mangoAccount.current + let usersOpenOrderPrices: number[] = [] + if ( + mangoAccount && + bids2 && + asks2 && + bids2 instanceof BookSide && + asks2 instanceof BookSide + ) { + usersOpenOrderPrices = [...bids2.items(), ...asks2.items()] + .filter((order) => order.owner.equals(mangoAccount.publicKey)) + .map((order) => order.price) + } else { + usersOpenOrderPrices = + marketPk && openOrders[marketPk]?.length + ? openOrders[marketPk]?.map((order) => order.price) + : [] + } + + // updated orderbook data + const bids = + groupBy(newOrderbook?.bids, market, grouping, true) || [] + const asks = + groupBy(newOrderbook?.asks, market, grouping, false) || [] + + const sum = (total: number, [, size]: number[], index: number) => + index < depth ? total + size : total + const totalSize = bids.reduce(sum, 0) + asks.reduce(sum, 0) + + const maxSize = + Math.max( + ...bids.map((b: number[]) => { + return b[1] + }) + ) + + Math.max( + ...asks.map((a: number[]) => { + return a[1] + }) + ) + const isGrouped = grouping !== market.tickSize + const bidsToDisplay = getCumulativeOrderbookSide( + bids, + totalSize, + maxSize, + depth, + usersOpenOrderPrices, + grouping, + isGrouped + ) + const asksToDisplay = getCumulativeOrderbookSide( + asks, + totalSize, + maxSize, + depth, + usersOpenOrderPrices, + grouping, + isGrouped + ) + + currentOrderbookData.current = newOrderbook + if (bidsToDisplay[0] || asksToDisplay[0]) { + const bid = bidsToDisplay[0]?.price + const ask = asksToDisplay[0]?.price + let spread = 0, + spreadPercentage = 0 + if (bid && ask) { + spread = parseFloat( + (ask - bid).toFixed(getDecimalCount(market.tickSize)) + ) + spreadPercentage = (spread / ask) * 100 + } + + setOrderbookData({ + bids: bidsToDisplay, + asks: asksToDisplay.reverse(), + spread, + spreadPercentage, + }) + } else { + setOrderbookData(null) + } + } + } + ), + [depth, grouping, market] + ) + // subscribe to the bids and asks orderbook accounts useEffect(() => { const set = mangoStore.getState().set diff --git a/components/trade/OrderbookAndTrades.tsx b/components/trade/OrderbookAndTrades.tsx index db0d64ac..b51f5afb 100644 --- a/components/trade/OrderbookAndTrades.tsx +++ b/components/trade/OrderbookAndTrades.tsx @@ -11,13 +11,7 @@ export const TABS: [string, number][] = [ ['trade:trades', 0], ] -const OrderbookAndTrades = ({ - grouping, - setGrouping, -}: { - grouping: number - setGrouping: (g: number) => void -}) => { +const OrderbookAndTrades = () => { const [activeTab, setActiveTab] = useState('trade:book') const [showDepthChart] = useLocalStorageState(DEPTH_CHART_KEY, false) return ( @@ -34,12 +28,12 @@ const OrderbookAndTrades = ({ className={`flex ${activeTab === 'trade:book' ? 'visible' : 'hidden'}`} > {showDepthChart ? ( -
- +
+
) : null} -
- +
+
{ const { height, width } = useViewport() const { uiLocked } = mangoStore((s) => s.settings) const showMobileView = width <= breakpoints.md - const [grouping, setGrouping] = useState(0.01) // const tourSettings = mangoStore((s) => s.settings.tours) // const { connected } = useWallet() // const [isOnboarded] = useLocalStorageState(IS_ONBOARDED_KEY) @@ -308,7 +307,7 @@ const TradeAdvancedPage = () => { : '' }`} > - +
{/* {!tourSettings?.trade_tour_seen && isOnboarded && connected ? ( diff --git a/hooks/useOrderbookSubscription.ts b/hooks/useOrderbookSubscription.ts deleted file mode 100644 index 84298bce..00000000 --- a/hooks/useOrderbookSubscription.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { BookSide, PerpMarket } from '@blockworks-foundation/mango-v4' -import { Market } from '@project-serum/serum' -import mangoStore from '@store/mangoStore' -import Big from 'big.js' -import { useEffect, useRef, useState } from 'react' -import { getDecimalCount } from 'utils/numbers' -import useSelectedMarket from './useSelectedMarket' -import { OrderbookL2 } from 'types' -import isEqual from 'lodash/isEqual' - -export type cumOrderbookSide = { - price: number - size: number - cumulativeSize: number - sizePercent: number - maxSizePercent: number - cumulativeSizePercent: number - isUsersOrder: boolean -} - -type OrderbookData = { - bids: cumOrderbookSide[] - asks: cumOrderbookSide[] - spread: number - spreadPercentage: number -} - -const getCumulativeOrderbookSide = ( - orders: number[][], - totalSize: number, - maxSize: number, - depth: number, - usersOpenOrderPrices: number[], - grouping: number, - isGrouped: boolean -): cumOrderbookSide[] => { - let cumulativeSize = 0 - return orders.slice(0, depth).map(([price, size]) => { - cumulativeSize += size - return { - price: Number(price), - size, - cumulativeSize, - sizePercent: Math.round((cumulativeSize / (totalSize || 1)) * 100), - cumulativeSizePercent: Math.round((size / (cumulativeSize || 1)) * 100), - maxSizePercent: Math.round((size / (maxSize || 1)) * 100), - isUsersOrder: hasOpenOrderForPriceGroup( - usersOpenOrderPrices, - price, - grouping, - isGrouped - ), - } - }) -} - -const groupBy = ( - ordersArray: number[][], - market: PerpMarket | Market, - grouping: number, - isBids: boolean -) => { - if (!ordersArray || !market || !grouping || grouping == market?.tickSize) { - return ordersArray || [] - } - const groupFloors: Record = {} - for (let i = 0; i < ordersArray.length; i++) { - if (typeof ordersArray[i] == 'undefined') { - break - } - const bigGrouping = Big(grouping) - const bigOrder = Big(ordersArray[i][0]) - - const floor = isBids - ? bigOrder - .div(bigGrouping) - .round(0, Big.roundDown) - .times(bigGrouping) - .toNumber() - : bigOrder - .div(bigGrouping) - .round(0, Big.roundUp) - .times(bigGrouping) - .toNumber() - if (typeof groupFloors[floor] == 'undefined') { - groupFloors[floor] = ordersArray[i][1] - } else { - groupFloors[floor] = ordersArray[i][1] + groupFloors[floor] - } - } - const sortedGroups = Object.entries(groupFloors) - .map((entry) => { - return [ - +parseFloat(entry[0]).toFixed(getDecimalCount(grouping)), - entry[1], - ] - }) - .sort((a: number[], b: number[]) => { - if (!a || !b) { - return -1 - } - return isBids ? b[0] - a[0] : a[0] - b[0] - }) - return sortedGroups -} - -const hasOpenOrderForPriceGroup = ( - openOrderPrices: number[], - price: number, - grouping: number, - isGrouped: boolean -) => { - if (!isGrouped) { - return !!openOrderPrices.find((ooPrice) => { - return ooPrice === price - }) - } - return !!openOrderPrices.find((ooPrice) => { - return ooPrice >= price - grouping && ooPrice <= price + grouping - }) -} - -const useOrderbookSubscription = (depth: number, grouping: number) => { - const { serumOrPerpMarket } = useSelectedMarket() - const [orderbookData, setOrderbookData] = useState(null) - const currentOrderbookData = useRef() - - useEffect( - () => - mangoStore.subscribe( - (state) => state.selectedMarket.orderbook, - (newOrderbook) => { - if ( - newOrderbook && - serumOrPerpMarket && - !isEqual(currentOrderbookData.current, newOrderbook) - ) { - // check if user has open orders so we can highlight them on orderbook - const openOrders = mangoStore.getState().mangoAccount.openOrders - const marketPk = serumOrPerpMarket.publicKey.toString() - const bids2 = mangoStore.getState().selectedMarket.bidsAccount - const asks2 = mangoStore.getState().selectedMarket.asksAccount - const mangoAccount = mangoStore.getState().mangoAccount.current - let usersOpenOrderPrices: number[] = [] - if ( - mangoAccount && - bids2 && - asks2 && - bids2 instanceof BookSide && - asks2 instanceof BookSide - ) { - usersOpenOrderPrices = [...bids2.items(), ...asks2.items()] - .filter((order) => order.owner.equals(mangoAccount.publicKey)) - .map((order) => order.price) - } else { - usersOpenOrderPrices = - marketPk && openOrders[marketPk]?.length - ? openOrders[marketPk]?.map((order) => order.price) - : [] - } - - // updated orderbook data - const bids = - groupBy(newOrderbook?.bids, serumOrPerpMarket, grouping, true) || - [] - const asks = - groupBy(newOrderbook?.asks, serumOrPerpMarket, grouping, false) || - [] - - const sum = (total: number, [, size]: number[], index: number) => - index < depth ? total + size : total - const totalSize = bids.reduce(sum, 0) + asks.reduce(sum, 0) - - const maxSize = - Math.max( - ...bids.map((b: number[]) => { - return b[1] - }) - ) + - Math.max( - ...asks.map((a: number[]) => { - return a[1] - }) - ) - const isGrouped = grouping !== serumOrPerpMarket.tickSize - const bidsToDisplay = getCumulativeOrderbookSide( - bids, - totalSize, - maxSize, - depth, - usersOpenOrderPrices, - grouping, - isGrouped - ) - const asksToDisplay = getCumulativeOrderbookSide( - asks, - totalSize, - maxSize, - depth, - usersOpenOrderPrices, - grouping, - isGrouped - ) - - currentOrderbookData.current = newOrderbook - if (bidsToDisplay[0] || asksToDisplay[0]) { - const bid = bidsToDisplay[0]?.price - const ask = asksToDisplay[0]?.price - let spread = 0, - spreadPercentage = 0 - if (bid && ask) { - spread = parseFloat( - (ask - bid).toFixed( - getDecimalCount(serumOrPerpMarket.tickSize) - ) - ) - spreadPercentage = (spread / ask) * 100 - } - - setOrderbookData({ - bids: bidsToDisplay, - asks: asksToDisplay.reverse(), - spread, - spreadPercentage, - }) - } else { - setOrderbookData(null) - } - } - } - ), - [grouping, serumOrPerpMarket] - ) - return orderbookData -} - -export default useOrderbookSubscription diff --git a/public/locales/en/account.json b/public/locales/en/account.json index 5c8a1373..bc735e9b 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -3,17 +3,12 @@ "assets-liabilities": "Assets & Liabilities", "daily-volume": "24h Volume", "export": "Export {{dataType}}", -<<<<<<< Updated upstream - "liabilities": "Liabilities", - "lifetime-volume": "Lifetime Trade Volume", -======= "funding-chart": "Funding Chart", "init-health": "Init Health", "liabilities": "Liabilities", "lifetime-volume": "Lifetime Trade Volume", "maint-health": "Maint Health", "no-data": "No data to display", ->>>>>>> Stashed changes "no-pnl-history": "No PnL History", "pnl-chart": "PnL Chart", "pnl-history": "PnL History", diff --git a/types/index.ts b/types/index.ts index 481a2864..b7b9f26a 100644 --- a/types/index.ts +++ b/types/index.ts @@ -413,3 +413,20 @@ export type TickerData = { target_volume: string ticker_id: string } + +export type cumOrderbookSide = { + price: number + size: number + cumulativeSize: number + sizePercent: number + maxSizePercent: number + cumulativeSizePercent: number + isUsersOrder: boolean +} + +export type OrderbookData = { + bids: cumOrderbookSide[] + asks: cumOrderbookSide[] + spread: number + spreadPercentage: number +} diff --git a/utils/orderbook.ts b/utils/orderbook.ts new file mode 100644 index 00000000..3514bc61 --- /dev/null +++ b/utils/orderbook.ts @@ -0,0 +1,164 @@ +import { + BookSide, + BookSideType, + MangoClient, + PerpMarket, +} from '@blockworks-foundation/mango-v4' +import { Market, Orderbook as SpotOrderBook } from '@project-serum/serum' +import { AccountInfo } from '@solana/web3.js' +import mangoStore from '@store/mangoStore' +import Big from 'big.js' +import { cumOrderbookSide } from 'types' +import { getDecimalCount } from './numbers' + +export const getMarket = () => { + const group = mangoStore.getState().group + const selectedMarket = mangoStore.getState().selectedMarket.current + if (!group || !selectedMarket) return + return selectedMarket instanceof PerpMarket + ? selectedMarket + : group?.getSerum3ExternalMarket(selectedMarket.serumMarketExternal) +} + +export const decodeBookL2 = (book: SpotOrderBook | BookSide): number[][] => { + const depth = 300 + if (book instanceof SpotOrderBook) { + return book.getL2(depth).map(([price, size]) => [price, size]) + } else if (book instanceof BookSide) { + return book.getL2Ui(depth) + } + return [] +} + +export function decodeBook( + client: MangoClient, + market: Market | PerpMarket, + accInfo: AccountInfo, + side: 'bids' | 'asks' +): SpotOrderBook | BookSide { + if (market instanceof Market) { + const book = SpotOrderBook.decode(market, accInfo.data) + return book + } else { + const decodedAcc = client.program.coder.accounts.decode( + 'bookSide', + accInfo.data + ) + const book = BookSide.from( + client, + market, + side === 'bids' ? BookSideType.bids : BookSideType.asks, + decodedAcc + ) + return book + } +} + +export const updatePerpMarketOnGroup = ( + book: BookSide, + side: 'bids' | 'asks' +) => { + const group = mangoStore.getState().group + const perpMarket = group?.getPerpMarketByMarketIndex( + book.perpMarket.perpMarketIndex + ) + if (perpMarket) { + perpMarket[`_${side}`] = book + // mangoStore.getState().actions.fetchOpenOrders() + } +} + +export const hasOpenOrderForPriceGroup = ( + openOrderPrices: number[], + price: number, + grouping: number, + isGrouped: boolean +) => { + if (!isGrouped) { + return !!openOrderPrices.find((ooPrice) => { + return ooPrice === price + }) + } + return !!openOrderPrices.find((ooPrice) => { + return ooPrice >= price - grouping && ooPrice <= price + grouping + }) +} + +export const getCumulativeOrderbookSide = ( + orders: number[][], + totalSize: number, + maxSize: number, + depth: number, + usersOpenOrderPrices: number[], + grouping: number, + isGrouped: boolean +): cumOrderbookSide[] => { + let cumulativeSize = 0 + return orders.slice(0, depth).map(([price, size]) => { + cumulativeSize += size + return { + price: Number(price), + size, + cumulativeSize, + sizePercent: Math.round((cumulativeSize / (totalSize || 1)) * 100), + cumulativeSizePercent: Math.round((size / (cumulativeSize || 1)) * 100), + maxSizePercent: Math.round((size / (maxSize || 1)) * 100), + isUsersOrder: hasOpenOrderForPriceGroup( + usersOpenOrderPrices, + price, + grouping, + isGrouped + ), + } + }) +} + +export const groupBy = ( + ordersArray: number[][], + market: PerpMarket | Market, + grouping: number, + isBids: boolean +) => { + if (!ordersArray || !market || !grouping || grouping == market?.tickSize) { + return ordersArray || [] + } + const groupFloors: Record = {} + for (let i = 0; i < ordersArray.length; i++) { + if (typeof ordersArray[i] == 'undefined') { + break + } + const bigGrouping = Big(grouping) + const bigOrder = Big(ordersArray[i][0]) + + const floor = isBids + ? bigOrder + .div(bigGrouping) + .round(0, Big.roundDown) + .times(bigGrouping) + .toNumber() + : bigOrder + .div(bigGrouping) + .round(0, Big.roundUp) + .times(bigGrouping) + .toNumber() + if (typeof groupFloors[floor] == 'undefined') { + groupFloors[floor] = ordersArray[i][1] + } else { + groupFloors[floor] = ordersArray[i][1] + groupFloors[floor] + } + } + const sortedGroups = Object.entries(groupFloors) + .map((entry) => { + return [ + +parseFloat(entry[0]).toFixed(getDecimalCount(grouping)), + entry[1], + ] + }) + .sort((a: number[], b: number[]) => { + if (!a || !b) { + return -1 + } + return isBids ? b[0] - a[0] : a[0] - b[0] + }) + return sortedGroups +}