remove orderbook subscription hook and other fixes

This commit is contained in:
saml33 2023-07-18 11:18:01 +10:00
parent c2aa4676b0
commit a1fbe18bf1
10 changed files with 375 additions and 358 deletions

View File

@ -1,8 +1,5 @@
import Slider from '@components/forms/Slider' import Slider from '@components/forms/Slider'
import useMarkPrice from 'hooks/useMarkPrice' import useMarkPrice from 'hooks/useMarkPrice'
import useOrderbookSubscription, {
cumOrderbookSide,
} from 'hooks/useOrderbookSubscription'
import useSelectedMarket from 'hooks/useSelectedMarket' import useSelectedMarket from 'hooks/useSelectedMarket'
import { useViewport } from 'hooks/useViewport' import { useViewport } from 'hooks/useViewport'
import { useTheme } from 'next-themes' import { useTheme } from 'next-themes'
@ -22,6 +19,8 @@ import { COLORS } from 'styles/colors'
import { floorToDecimal, getDecimalCount } from 'utils/numbers' import { floorToDecimal, getDecimalCount } from 'utils/numbers'
import { gridBreakpoints } from './TradeAdvancedPage' import { gridBreakpoints } from './TradeAdvancedPage'
import { CartesianViewBox } from 'recharts/types/util/types' import { CartesianViewBox } from 'recharts/types/util/types'
import { cumOrderbookSide } from 'types'
import mangoStore from '@store/mangoStore'
type LabelPosition = type LabelPosition =
| 'left' | 'left'
@ -40,14 +39,6 @@ type LabelPosition =
const Y_TICK_COUNT = 10 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 { interface CustomLabel extends LabelProps {
viewBox?: CartesianViewBox viewBox?: CartesianViewBox
} }
@ -69,20 +60,52 @@ const MarkPriceLabel = ({ value, viewBox }: CustomLabel) => {
} else return null } 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 { theme } = useTheme()
const { serumOrPerpMarket } = useSelectedMarket() const { serumOrPerpMarket } = useSelectedMarket()
const [priceRangePercent, setPriceRangePercentPercent] = useState('5')
const [mouseData, setMouseData] = useState<cumOrderbookSide | null>(null) const [mouseData, setMouseData] = useState<cumOrderbookSide | null>(null)
const markPrice = useMarkPrice() const markPrice = useMarkPrice()
const orderbook = useOrderbookSubscription(100, grouping) const orderbook = mangoStore((s) => s.selectedMarket.orderbook)
const [priceRangePercent, setPriceRangePercentPercent] = useState('10')
const { width } = useViewport() const { width } = useViewport()
const increaseHeight = width ? width > gridBreakpoints.xxxl : false 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 // format chart data for the bids and asks series
const mergeCumulativeData = ( const mergeCumulativeData = (
bids: cumOrderbookSide[], bids: DepthOrderbookSide[],
asks: cumOrderbookSide[] asks: DepthOrderbookSide[]
) => { ) => {
const bidsWithSide = bids.map((b) => ({ ...b, bids: b.cumulativeSize })) const bidsWithSide = bids.map((b) => ({ ...b, bids: b.cumulativeSize }))
const asksWithSide = asks.map((a) => ({ ...a, asks: a.cumulativeSize })) const asksWithSide = asks.map((a) => ({ ...a, asks: a.cumulativeSize }))
@ -90,13 +113,15 @@ const DepthChart = ({ grouping }: { grouping: number }) => {
} }
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!orderbook) return [] if (!orderbook || !serumOrPerpMarket || !markPrice) return []
return mergeCumulativeData(orderbook.bids, orderbook.asks) const formattedBids = formatOrderbookData(orderbook.bids, markPrice)
}, [orderbook]) const formattedAsks = formatOrderbookData(orderbook.asks, markPrice)
return mergeCumulativeData(formattedBids, formattedAsks)
}, [markPrice, orderbook, serumOrPerpMarket])
// find the max value for the x-axis // find the max value for the x-axis
const findXDomainMax = ( const findXDomainMax = (
data: cumOrderbookSide[], data: DepthOrderbookSide[],
yMin: number, yMin: number,
yMax: number yMax: number
) => { ) => {
@ -177,7 +202,7 @@ const DepthChart = ({ grouping }: { grouping: number }) => {
const priceFormatter = useCallback( const priceFormatter = useCallback(
(price: number) => { (price: number) => {
if (!serumOrPerpMarket) return price.toFixed(1) if (!serumOrPerpMarket) return price.toFixed()
const tickDecimals = getDecimalCount(serumOrPerpMarket.tickSize) const tickDecimals = getDecimalCount(serumOrPerpMarket.tickSize)
if (tickDecimals >= 7) { if (tickDecimals >= 7) {
return price.toExponential(3) return price.toExponential(3)
@ -186,6 +211,15 @@ const DepthChart = ({ grouping }: { grouping: number }) => {
[serumOrPerpMarket] [serumOrPerpMarket]
) )
const xTickFormatter = useCallback(
(size: number) => {
if (!serumOrPerpMarket) return size.toFixed()
const minOrderDecimals = getDecimalCount(serumOrPerpMarket.minOrderSize)
return size.toFixed(minOrderDecimals)
},
[serumOrPerpMarket]
)
const isWithinRangeOfTick = useCallback( const isWithinRangeOfTick = useCallback(
(value: number, baseValue: number) => { (value: number, baseValue: number) => {
const difference = Math.abs(value - baseValue) const difference = Math.abs(value - baseValue)
@ -265,7 +299,7 @@ const DepthChart = ({ grouping }: { grouping: number }) => {
setMouseData(null) setMouseData(null)
} }
return ( return chartData.length ? (
<> <>
<div className="flex h-10 items-center border-b border-th-bkg-3 py-1 px-2"> <div className="flex h-10 items-center border-b border-th-bkg-3 py-1 px-2">
<div className="flex items-center"> <div className="flex items-center">
@ -274,7 +308,7 @@ const DepthChart = ({ grouping }: { grouping: number }) => {
</span> </span>
<Slider <Slider
amount={parseFloat(priceRangePercent)} amount={parseFloat(priceRangePercent)}
max="50" max="100"
min="0.5" min="0.5"
onChange={(p) => setPriceRangePercentPercent(p)} onChange={(p) => setPriceRangePercentPercent(p)}
step={0.5} step={0.5}
@ -300,6 +334,7 @@ const DepthChart = ({ grouping }: { grouping: number }) => {
type="number" type="number"
tick={false} tick={false}
tickLine={false} tickLine={false}
tickFormatter={(tick) => xTickFormatter(tick)}
/> />
<YAxis <YAxis
dataKey="price" dataKey="price"
@ -315,16 +350,20 @@ const DepthChart = ({ grouping }: { grouping: number }) => {
tickFormatter={(tick) => yTickFormatter(tick)} tickFormatter={(tick) => yTickFormatter(tick)}
/> />
<Area <Area
type="step" type="stepBefore"
dataKey="bids" dataKey="bids"
stroke={COLORS.UP[theme]} stroke={COLORS.UP[theme]}
fill="url(#bidsGradient)" fill="url(#bidsGradient)"
isAnimationActive={false}
strokeWidth={1}
/> />
<Area <Area
type="step" type="stepBefore"
dataKey="asks" dataKey="asks"
stroke={COLORS.DOWN[theme]} stroke={COLORS.DOWN[theme]}
fill="url(#asksGradient)" fill="url(#asksGradient)"
isAnimationActive={false}
strokeWidth={1}
/> />
<ReferenceLine <ReferenceLine
y={mouseData?.price} y={mouseData?.price}
@ -471,7 +510,7 @@ const DepthChart = ({ grouping }: { grouping: number }) => {
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
</> </>
) ) : null
} }
export default DepthChart export default DepthChart

View File

@ -16,7 +16,7 @@ import { SOUND_SETTINGS_KEY } from 'utils/constants'
import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings' import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
import { Howl } from 'howler' import { Howl } from 'howler'
import { isMangoError } from 'types' import { isMangoError } from 'types'
import { decodeBook, decodeBookL2 } from './Orderbook' import { decodeBook, decodeBookL2 } from 'utils/orderbook'
interface MarketCloseModalProps { interface MarketCloseModalProps {
onClose: () => void onClose: () => void

View File

@ -11,7 +11,6 @@ import FavoriteMarketsBar from './FavoriteMarketsBar'
const MobileTradeAdvancedPage = () => { const MobileTradeAdvancedPage = () => {
const [activeTab, setActiveTab] = useState('trade:book') const [activeTab, setActiveTab] = useState('trade:book')
const [grouping, setGrouping] = useState(0.01)
const [showChart, setShowChart] = useState(false) const [showChart, setShowChart] = useState(false)
return ( return (
<div className="grid grid-cols-2 sm:grid-cols-3"> <div className="grid grid-cols-2 sm:grid-cols-3">
@ -42,7 +41,7 @@ const MobileTradeAdvancedPage = () => {
/> />
</div> </div>
<div className={activeTab === 'trade:book' ? 'visible' : 'hidden'}> <div className={activeTab === 'trade:book' ? 'visible' : 'hidden'}>
<Orderbook grouping={grouping} setGrouping={setGrouping} /> <Orderbook />
</div> </div>
<div className={activeTab === 'trade:trades' ? 'visible' : 'hidden'}> <div className={activeTab === 'trade:trades' ? 'visible' : 'hidden'}>
<RecentTrades /> <RecentTrades />

View File

@ -1,7 +1,7 @@
import { AccountInfo, PublicKey } from '@solana/web3.js' import { PublicKey } from '@solana/web3.js'
import mangoStore from '@store/mangoStore' import mangoStore from '@store/mangoStore'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' 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 useLocalStorageState from 'hooks/useLocalStorageState'
import { import {
floorToDecimal, floorToDecimal,
@ -18,20 +18,23 @@ import Decimal from 'decimal.js'
import Tooltip from '@components/shared/Tooltip' import Tooltip from '@components/shared/Tooltip'
import GroupSize from './GroupSize' import GroupSize from './GroupSize'
import { useViewport } from 'hooks/useViewport' import { useViewport } from 'hooks/useViewport'
import { import { BookSide, Serum3Market } from '@blockworks-foundation/mango-v4'
BookSide,
BookSideType,
MangoClient,
PerpMarket,
Serum3Market,
} from '@blockworks-foundation/mango-v4'
import useSelectedMarket from 'hooks/useSelectedMarket' import useSelectedMarket from 'hooks/useSelectedMarket'
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings' import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
import { OrderbookFeed } from '@blockworks-foundation/mango-feeds' import { OrderbookFeed } from '@blockworks-foundation/mango-feeds'
import useOrderbookSubscription from 'hooks/useOrderbookSubscription'
import Switch from '@components/forms/Switch' import Switch from '@components/forms/Switch'
import { gridBreakpoints } from './TradeAdvancedPage' import { gridBreakpoints } from './TradeAdvancedPage'
import { breakpoints } from 'utils/theme' 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', { const sizeCompacter = Intl.NumberFormat('en', {
maximumFractionDigits: 6, maximumFractionDigits: 6,
@ -40,72 +43,13 @@ const sizeCompacter = Intl.NumberFormat('en', {
const SHOW_EXPONENTIAL_THRESHOLD = 0.00001 const SHOW_EXPONENTIAL_THRESHOLD = 0.00001
const getMarket = () => { const Orderbook = () => {
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<Buffer>,
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 { t } = useTranslation(['common', 'trade']) const { t } = useTranslation(['common', 'trade'])
const { serumOrPerpMarket: market } = useSelectedMarket() const { serumOrPerpMarket: market } = useSelectedMarket()
const connection = mangoStore((s) => s.connection) const connection = mangoStore((s) => s.connection)
const [tickSize, setTickSize] = useState(0) const [tickSize, setTickSize] = useState(0)
const [grouping, setGrouping] = useState(0.01)
const [useOrderbookFeed, setUseOrderbookFeed] = useState( const [useOrderbookFeed, setUseOrderbookFeed] = useState(
localStorage.getItem(USE_ORDERBOOK_FEED_KEY) !== null localStorage.getItem(USE_ORDERBOOK_FEED_KEY) !== null
? localStorage.getItem(USE_ORDERBOOK_FEED_KEY) === 'true' ? localStorage.getItem(USE_ORDERBOOK_FEED_KEY) === 'true'
@ -117,13 +61,13 @@ const Orderbook = ({
) )
const { width } = useViewport() const { width } = useViewport()
const isMobile = width ? width < breakpoints.lg : false const isMobile = width ? width < breakpoints.lg : false
const [orderbookData, setOrderbookData] = useState<OrderbookData | null>(null)
const currentOrderbookData = useRef<OrderbookL2>()
const depth = useMemo(() => { const depth = useMemo(() => {
return width > gridBreakpoints.xxxl ? 12 : 10 return width > gridBreakpoints.xxxl ? 12 : 10
}, [width]) }, [width])
const orderbookData = useOrderbookSubscription(depth, grouping)
const depthArray: number[] = useMemo(() => { const depthArray: number[] = useMemo(() => {
return Array(depth).fill(0) return Array(depth).fill(0)
}, [depth]) }, [depth])
@ -151,6 +95,109 @@ const Orderbook = ({
return asksPk.toString() return asksPk.toString()
}, [market]) }, [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 // subscribe to the bids and asks orderbook accounts
useEffect(() => { useEffect(() => {
const set = mangoStore.getState().set const set = mangoStore.getState().set

View File

@ -11,13 +11,7 @@ export const TABS: [string, number][] = [
['trade:trades', 0], ['trade:trades', 0],
] ]
const OrderbookAndTrades = ({ const OrderbookAndTrades = () => {
grouping,
setGrouping,
}: {
grouping: number
setGrouping: (g: number) => void
}) => {
const [activeTab, setActiveTab] = useState('trade:book') const [activeTab, setActiveTab] = useState('trade:book')
const [showDepthChart] = useLocalStorageState<boolean>(DEPTH_CHART_KEY, false) const [showDepthChart] = useLocalStorageState<boolean>(DEPTH_CHART_KEY, false)
return ( return (
@ -34,12 +28,12 @@ const OrderbookAndTrades = ({
className={`flex ${activeTab === 'trade:book' ? 'visible' : 'hidden'}`} className={`flex ${activeTab === 'trade:book' ? 'visible' : 'hidden'}`}
> >
{showDepthChart ? ( {showDepthChart ? (
<div className="z-20 w-1/2 border-r border-th-bkg-3"> <div className="hidden w-1/2 border-r border-th-bkg-3 lg:block">
<DepthChart grouping={grouping} /> <DepthChart />
</div> </div>
) : null} ) : null}
<div className={showDepthChart ? 'w-1/2' : 'w-full'}> <div className={showDepthChart ? 'w-full lg:w-1/2' : 'w-full'}>
<Orderbook grouping={grouping} setGrouping={setGrouping} /> <Orderbook />
</div> </div>
</div> </div>
<div <div

View File

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react' import { useMemo } from 'react'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import ReactGridLayout, { Responsive, WidthProvider } from 'react-grid-layout' import ReactGridLayout, { Responsive, WidthProvider } from 'react-grid-layout'
import mangoStore from '@store/mangoStore' import mangoStore from '@store/mangoStore'
@ -51,7 +51,6 @@ const TradeAdvancedPage = () => {
const { height, width } = useViewport() const { height, width } = useViewport()
const { uiLocked } = mangoStore((s) => s.settings) const { uiLocked } = mangoStore((s) => s.settings)
const showMobileView = width <= breakpoints.md const showMobileView = width <= breakpoints.md
const [grouping, setGrouping] = useState(0.01)
// const tourSettings = mangoStore((s) => s.settings.tours) // const tourSettings = mangoStore((s) => s.settings.tours)
// const { connected } = useWallet() // const { connected } = useWallet()
// const [isOnboarded] = useLocalStorageState(IS_ONBOARDED_KEY) // const [isOnboarded] = useLocalStorageState(IS_ONBOARDED_KEY)
@ -308,7 +307,7 @@ const TradeAdvancedPage = () => {
: '' : ''
}`} }`}
> >
<OrderbookAndTrades grouping={grouping} setGrouping={setGrouping} /> <OrderbookAndTrades />
</div> </div>
</ResponsiveGridLayout> </ResponsiveGridLayout>
{/* {!tourSettings?.trade_tour_seen && isOnboarded && connected ? ( {/* {!tourSettings?.trade_tour_seen && isOnboarded && connected ? (

View File

@ -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<number, number> = {}
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<OrderbookData | null>(null)
const currentOrderbookData = useRef<OrderbookL2>()
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

View File

@ -3,17 +3,12 @@
"assets-liabilities": "Assets & Liabilities", "assets-liabilities": "Assets & Liabilities",
"daily-volume": "24h Volume", "daily-volume": "24h Volume",
"export": "Export {{dataType}}", "export": "Export {{dataType}}",
<<<<<<< Updated upstream
"liabilities": "Liabilities",
"lifetime-volume": "Lifetime Trade Volume",
=======
"funding-chart": "Funding Chart", "funding-chart": "Funding Chart",
"init-health": "Init Health", "init-health": "Init Health",
"liabilities": "Liabilities", "liabilities": "Liabilities",
"lifetime-volume": "Lifetime Trade Volume", "lifetime-volume": "Lifetime Trade Volume",
"maint-health": "Maint Health", "maint-health": "Maint Health",
"no-data": "No data to display", "no-data": "No data to display",
>>>>>>> Stashed changes
"no-pnl-history": "No PnL History", "no-pnl-history": "No PnL History",
"pnl-chart": "PnL Chart", "pnl-chart": "PnL Chart",
"pnl-history": "PnL History", "pnl-history": "PnL History",

View File

@ -413,3 +413,20 @@ export type TickerData = {
target_volume: string target_volume: string
ticker_id: 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
}

164
utils/orderbook.ts Normal file
View File

@ -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<Buffer>,
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<number, number> = {}
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
}