subscribe to orderbook for depth chart
This commit is contained in:
parent
03fdc2ae10
commit
aa4958f35a
|
@ -1,79 +1,154 @@
|
|||
import mangoStore from '@store/mangoStore'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts'
|
||||
import useOrderbookSubscription, {
|
||||
cumOrderbookSide,
|
||||
} from 'hooks/useOrderbookSubscription'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import { useTheme } from 'next-themes'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { XAxis, YAxis, ResponsiveContainer, AreaChart, Area } from 'recharts'
|
||||
import { COLORS } from 'styles/colors'
|
||||
import { getDecimalCount } from 'utils/numbers'
|
||||
|
||||
type DepthData = {
|
||||
price: number
|
||||
[key: string]: number
|
||||
cumulative: number
|
||||
}
|
||||
const DepthChart = ({ grouping }: { grouping: number }) => {
|
||||
const { theme } = useTheme()
|
||||
const { serumOrPerpMarket } = useSelectedMarket()
|
||||
const [isScrolled, setIsScrolled] = useState(false)
|
||||
const depthChartElRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
function DepthChart() {
|
||||
const orderbook = mangoStore((s) => s.selectedMarket.orderbook)
|
||||
const [chartData, setChartData] = useState<DepthData[]>([])
|
||||
console.log(orderbook)
|
||||
useEffect(() => {
|
||||
const bidsCumulative = calculateCumulative(orderbook.bids, 'bids')
|
||||
const asksCumulative = calculateCumulative(orderbook.asks, 'asks')
|
||||
const handleScroll = useCallback(() => {
|
||||
setIsScrolled(true)
|
||||
}, [])
|
||||
|
||||
const mergedData = mergeCumulativeData(bidsCumulative, asksCumulative)
|
||||
setChartData(mergedData)
|
||||
const verticallyCenterChart = useCallback(() => {
|
||||
const element = depthChartElRef.current
|
||||
if (element) {
|
||||
if (element.scrollHeight > window.innerHeight) {
|
||||
element.scrollTop =
|
||||
(element.scrollHeight - element.scrollHeight) / 2 +
|
||||
(element.scrollHeight - window.innerHeight) / 2 +
|
||||
94
|
||||
} else {
|
||||
element.scrollTop = (element.scrollHeight - element.offsetHeight) / 2
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const orderbook = useOrderbookSubscription(
|
||||
40,
|
||||
grouping,
|
||||
isScrolled,
|
||||
verticallyCenterChart
|
||||
)
|
||||
|
||||
const mergeCumulativeData = (
|
||||
bids: cumOrderbookSide[],
|
||||
asks: cumOrderbookSide[]
|
||||
) => {
|
||||
const bidsWithSide = bids.map((b) => ({ ...b, bids: b.cumulativeSize }))
|
||||
const asksWithSide = asks.map((a) => ({ ...a, asks: a.cumulativeSize }))
|
||||
return [...bidsWithSide, ...asksWithSide].sort((a, b) => a.price - b.price)
|
||||
}
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!orderbook) return []
|
||||
return mergeCumulativeData(orderbook.bids, orderbook.asks)
|
||||
}, [orderbook])
|
||||
|
||||
const calculateCumulative = (levels: number[][], type: 'bids' | 'asks') => {
|
||||
let cumulative = 0
|
||||
return levels.map((level) => {
|
||||
cumulative += level[1]
|
||||
return { price: level[0], [type]: cumulative, cumulative: cumulative }
|
||||
})
|
||||
}
|
||||
// useEffect(() => {
|
||||
// verticallyCenterChart()
|
||||
// }, [])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', verticallyCenterChart)
|
||||
}, [verticallyCenterChart])
|
||||
|
||||
// useEffect(() => {
|
||||
// const bidsCumulative = calculateCumulative(orderbook.bids, 'bids')
|
||||
// const asksCumulative = calculateCumulative(orderbook.asks, 'asks')
|
||||
|
||||
// const mergedData = mergeCumulativeData(bidsCumulative, asksCumulative)
|
||||
// setChartData(mergedData.slice(40, -40))
|
||||
// }, [orderbook])
|
||||
|
||||
// const calculateCumulative = (levels: number[][], type: 'bids' | 'asks') => {
|
||||
// let cumulative = 0
|
||||
// return levels.map((level) => {
|
||||
// cumulative += level[1]
|
||||
// return { price: level[0], [type]: cumulative, cumulative: cumulative }
|
||||
// })
|
||||
// }
|
||||
|
||||
const mergeCumulativeData = (bids: DepthData[], asks: DepthData[]) => {
|
||||
return [...bids, ...asks].sort((a, b) => a.price - b.price)
|
||||
}
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={chartData.slice(40, -40)} layout="vertical">
|
||||
<XAxis type="number" />
|
||||
<YAxis
|
||||
dataKey="price"
|
||||
reversed={true}
|
||||
domain={['dataMin', 'dataMax']}
|
||||
/>
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="bids"
|
||||
stroke="#008000" // Green stroke for bids
|
||||
fill="url(#bidsGradient)"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="asks"
|
||||
stroke="#FF0000" // Red stroke for asks
|
||||
fill="url(#asksGradient)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="bidsGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#008000" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="#008000" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="asksGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#FF0000" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="#FF0000" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
<div
|
||||
className="hide-scroll relative h-full overflow-y-scroll"
|
||||
ref={depthChartElRef}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<ResponsiveContainer width="100%" height={2000}>
|
||||
<AreaChart data={chartData} layout="vertical">
|
||||
<XAxis
|
||||
axisLine={false}
|
||||
type="number"
|
||||
tick={{
|
||||
fill: 'var(--fgd-4)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey="price"
|
||||
reversed={true}
|
||||
domain={['dataMin', 'dataMax']}
|
||||
tick={{
|
||||
fill: 'var(--fgd-4)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
ticks={chartData.map((d) => d.price)}
|
||||
tickLine={false}
|
||||
tickFormatter={(d) =>
|
||||
serumOrPerpMarket
|
||||
? d.toFixed(getDecimalCount(serumOrPerpMarket.tickSize))
|
||||
: d
|
||||
}
|
||||
/>
|
||||
<Area
|
||||
type="step"
|
||||
dataKey="bids"
|
||||
stroke={COLORS.UP[theme]}
|
||||
fill="url(#bidsGradient)"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Area
|
||||
type="step"
|
||||
dataKey="asks"
|
||||
stroke={COLORS.DOWN[theme]}
|
||||
fill="url(#asksGradient)"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="bidsGradient" x1="1" y1="0" x2="0" y2="0">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={COLORS.UP[theme]}
|
||||
stopOpacity={0.15}
|
||||
/>
|
||||
<stop offset="99%" stopColor={COLORS.UP[theme]} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="asksGradient" x1="1" y1="0" x2="0" y2="0">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={COLORS.DOWN[theme]}
|
||||
stopOpacity={0.15}
|
||||
/>
|
||||
<stop
|
||||
offset="99%"
|
||||
stopColor={COLORS.DOWN[theme]}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import FavoriteMarketsBar from './FavoriteMarketsBar'
|
|||
|
||||
const MobileTradeAdvancedPage = () => {
|
||||
const [activeTab, setActiveTab] = useState('trade:book')
|
||||
const [grouping, setGrouping] = useState(0.01)
|
||||
const [showChart, setShowChart] = useState(false)
|
||||
return (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3">
|
||||
|
@ -41,7 +42,7 @@ const MobileTradeAdvancedPage = () => {
|
|||
/>
|
||||
</div>
|
||||
<div className={activeTab === 'trade:book' ? 'visible' : 'hidden'}>
|
||||
<Orderbook />
|
||||
<Orderbook grouping={grouping} setGrouping={setGrouping} />
|
||||
</div>
|
||||
<div className={activeTab === 'trade:trades' ? 'visible' : 'hidden'}>
|
||||
<RecentTrades />
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { AccountInfo, PublicKey } from '@solana/web3.js'
|
||||
import Big from 'big.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 isEqual from 'lodash/isEqual'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
import {
|
||||
floorToDecimal,
|
||||
|
@ -13,7 +11,6 @@ import {
|
|||
import { ANIMATION_SETTINGS_KEY, USE_ORDERBOOK_FEED_KEY } from 'utils/constants'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Decimal from 'decimal.js'
|
||||
import { OrderbookL2 } from 'types'
|
||||
import OrderbookIcon from '@components/icons/OrderbookIcon'
|
||||
import Tooltip from '@components/shared/Tooltip'
|
||||
import GroupSize from './GroupSize'
|
||||
|
@ -31,6 +28,7 @@ import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettin
|
|||
import { ArrowPathIcon } from '@heroicons/react/20/solid'
|
||||
import { sleep } from 'utils'
|
||||
import { OrderbookFeed } from '@blockworks-foundation/mango-feeds'
|
||||
import useOrderbookSubscription from 'hooks/useOrderbookSubscription'
|
||||
|
||||
const sizeCompacter = Intl.NumberFormat('en', {
|
||||
maximumFractionDigits: 6,
|
||||
|
@ -82,111 +80,6 @@ export function decodeBook(
|
|||
}
|
||||
}
|
||||
|
||||
type cumOrderbookSide = {
|
||||
price: number
|
||||
size: number
|
||||
cumulativeSize: number
|
||||
sizePercent: number
|
||||
maxSizePercent: number
|
||||
cumulativeSizePercent: number
|
||||
isUsersOrder: boolean
|
||||
}
|
||||
|
||||
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 updatePerpMarketOnGroup = (book: BookSide, side: 'bids' | 'asks') => {
|
||||
const group = mangoStore.getState().group
|
||||
const perpMarket = group?.getPerpMarketByMarketIndex(
|
||||
|
@ -198,14 +91,13 @@ const updatePerpMarketOnGroup = (book: BookSide, side: 'bids' | 'asks') => {
|
|||
}
|
||||
}
|
||||
|
||||
type OrderbookData = {
|
||||
bids: cumOrderbookSide[]
|
||||
asks: cumOrderbookSide[]
|
||||
spread: number
|
||||
spreadPercentage: number
|
||||
}
|
||||
|
||||
const Orderbook = () => {
|
||||
const Orderbook = ({
|
||||
grouping,
|
||||
setGrouping,
|
||||
}: {
|
||||
grouping: number
|
||||
setGrouping: (g: number) => void
|
||||
}) => {
|
||||
const { t } = useTranslation(['common', 'trade'])
|
||||
const {
|
||||
serumOrPerpMarket: market,
|
||||
|
@ -215,8 +107,6 @@ const Orderbook = () => {
|
|||
const connection = mangoStore((s) => s.connection)
|
||||
|
||||
const [isScrolled, setIsScrolled] = useState(false)
|
||||
const [orderbookData, setOrderbookData] = useState<OrderbookData | null>(null)
|
||||
const [grouping, setGrouping] = useState(0.01)
|
||||
const [tickSize, setTickSize] = useState(0)
|
||||
const [showBids, setShowBids] = useState(true)
|
||||
const [showAsks, setShowAsks] = useState(true)
|
||||
|
@ -226,7 +116,6 @@ const Orderbook = () => {
|
|||
: true
|
||||
)
|
||||
|
||||
const currentOrderbookData = useRef<OrderbookL2>()
|
||||
const orderbookElRef = useRef<HTMLDivElement>(null)
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.md : false
|
||||
|
@ -235,19 +124,6 @@ const Orderbook = () => {
|
|||
return isMobile ? 9 : 40
|
||||
}, [isMobile])
|
||||
|
||||
const depthArray: number[] = useMemo(() => {
|
||||
return Array(depth).fill(0)
|
||||
}, [depth])
|
||||
|
||||
const orderbookFeed = useRef<OrderbookFeed | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (market && market.tickSize !== tickSize) {
|
||||
setTickSize(market.tickSize)
|
||||
setGrouping(market.tickSize)
|
||||
}
|
||||
}, [market, tickSize])
|
||||
|
||||
const verticallyCenterOrderbook = useCallback(() => {
|
||||
const element = orderbookElRef.current
|
||||
if (element) {
|
||||
|
@ -262,112 +138,26 @@ const Orderbook = () => {
|
|||
}
|
||||
}, [])
|
||||
|
||||
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,
|
||||
})
|
||||
if (!isScrolled) {
|
||||
verticallyCenterOrderbook()
|
||||
}
|
||||
} else {
|
||||
setOrderbookData(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
[grouping, market, isScrolled, verticallyCenterOrderbook]
|
||||
const orderbookData = useOrderbookSubscription(
|
||||
depth,
|
||||
grouping,
|
||||
isScrolled,
|
||||
verticallyCenterOrderbook
|
||||
)
|
||||
|
||||
const depthArray: number[] = useMemo(() => {
|
||||
return Array(depth).fill(0)
|
||||
}, [depth])
|
||||
|
||||
const orderbookFeed = useRef<OrderbookFeed | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (market && market.tickSize !== tickSize) {
|
||||
setTickSize(market.tickSize)
|
||||
setGrouping(market.tickSize)
|
||||
}
|
||||
}, [market, tickSize])
|
||||
|
||||
const bidAccountAddress = useMemo(() => {
|
||||
if (!market) return ''
|
||||
const bidsPk =
|
||||
|
|
|
@ -12,6 +12,7 @@ export const TABS: [string, number][] = [
|
|||
|
||||
const OrderbookAndTrades = () => {
|
||||
const [activeTab, setActiveTab] = useState('trade:book')
|
||||
const [grouping, setGrouping] = useState(0.01)
|
||||
return (
|
||||
<div className="hide-scroll h-full">
|
||||
<div className="border-b border-th-bkg-3">
|
||||
|
@ -27,14 +28,14 @@ const OrderbookAndTrades = () => {
|
|||
activeTab === 'trade:book' ? 'visible' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<Orderbook />
|
||||
<Orderbook grouping={grouping} setGrouping={setGrouping} />
|
||||
</div>
|
||||
<div
|
||||
className={`h-full ${
|
||||
activeTab === 'trade:depth' ? 'visible' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<DepthChart />
|
||||
<DepthChart grouping={grouping} />
|
||||
</div>
|
||||
<div
|
||||
className={`h-full ${
|
||||
|
|
|
@ -0,0 +1,245 @@
|
|||
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,
|
||||
isScrolled: boolean,
|
||||
centerVertically: () => void
|
||||
) => {
|
||||
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,
|
||||
})
|
||||
if (!isScrolled) {
|
||||
centerVertically()
|
||||
}
|
||||
} else {
|
||||
setOrderbookData(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
[grouping, serumOrPerpMarket, isScrolled, centerVertically]
|
||||
)
|
||||
return orderbookData
|
||||
}
|
||||
|
||||
export default useOrderbookSubscription
|
Loading…
Reference in New Issue