mango-v4-ui/components/trade/Orderbook.tsx

966 lines
31 KiB
TypeScript
Raw Normal View History

import { AccountInfo, PublicKey } from '@solana/web3.js'
2022-09-13 23:24:26 -07:00
import Big from 'big.js'
import mangoStore from '@store/mangoStore'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2022-09-13 23:24:26 -07:00
import { Market, Orderbook as SpotOrderBook } from '@project-serum/serum'
2022-09-20 13:05:50 -07:00
import isEqual from 'lodash/isEqual'
2022-09-13 23:24:26 -07:00
import useLocalStorageState from 'hooks/useLocalStorageState'
2023-02-08 18:08:17 -08:00
import {
floorToDecimal,
formatNumericValue,
getDecimalCount,
} from 'utils/numbers'
import { ANIMATION_SETTINGS_KEY, USE_ORDERBOOK_FEED_KEY } from 'utils/constants'
2022-09-13 23:24:26 -07:00
import { useTranslation } from 'next-i18next'
import Decimal from 'decimal.js'
import { OrderbookL2 } from 'types'
2022-09-18 05:53:28 -07:00
import OrderbookIcon from '@components/icons/OrderbookIcon'
import Tooltip from '@components/shared/Tooltip'
import GroupSize from './GroupSize'
2022-09-26 20:51:31 -07:00
import { breakpoints } from '../../utils/theme'
import { useViewport } from 'hooks/useViewport'
2022-10-10 19:16:13 -07:00
import {
BookSide,
BookSideType,
MangoClient,
PerpMarket,
2023-06-22 11:29:40 -07:00
Serum3Market,
2022-10-10 19:16:13 -07:00
} from '@blockworks-foundation/mango-v4'
2022-11-20 12:20:27 -08:00
import useSelectedMarket from 'hooks/useSelectedMarket'
2022-11-24 18:39:14 -08:00
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
2023-01-02 16:30:19 -08:00
import { ArrowPathIcon } from '@heroicons/react/20/solid'
2023-01-02 14:41:16 -08:00
import { sleep } from 'utils'
2023-04-22 10:35:57 -07:00
import { OrderbookFeed } from '@blockworks-foundation/mango-feeds'
2022-09-13 23:24:26 -07:00
const sizeCompacter = Intl.NumberFormat('en', {
maximumFractionDigits: 6,
notation: 'compact',
})
const SHOW_EXPONENTIAL_THRESHOLD = 0.00001
2023-03-25 11:47:37 -07:00
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)
}
2022-11-30 07:46:20 -08:00
export const decodeBookL2 = (book: SpotOrderBook | BookSide): number[][] => {
2023-02-03 07:28:39 -08:00
const depth = 300
2022-11-30 07:46:20 -08:00
if (book instanceof SpotOrderBook) {
2022-12-02 15:57:36 -08:00
return book.getL2(depth).map(([price, size]) => [price, size])
2022-11-30 07:46:20 -08:00
} else if (book instanceof BookSide) {
return book.getL2Ui(depth)
}
return []
}
export function decodeBook(
2022-10-10 19:16:13 -07:00
client: MangoClient,
market: Market | PerpMarket,
accInfo: AccountInfo<Buffer>,
side: 'bids' | 'asks'
2022-11-30 07:46:20 -08:00
): 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
2022-09-13 23:24:26 -07:00
}
}
type cumOrderbookSide = {
price: number
size: number
cumulativeSize: number
sizePercent: number
maxSizePercent: number
2022-09-29 13:00:36 -07:00
cumulativeSizePercent: number
isUsersOrder: boolean
2022-09-13 23:24:26 -07:00
}
const getCumulativeOrderbookSide = (
orders: number[][],
2022-09-13 23:24:26 -07:00
totalSize: number,
maxSize: number,
depth: number,
usersOpenOrderPrices: number[],
grouping: number,
isGrouped: boolean
2022-09-13 23:24:26 -07:00
): 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
),
}
})
2022-09-13 23:24:26 -07:00
}
const groupBy = (
ordersArray: number[][],
2022-10-10 19:16:13 -07:00
market: PerpMarket | Market,
2022-09-13 23:24:26 -07:00
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
}
2022-09-20 18:18:04 -07:00
2022-11-01 11:13:02 -07:00
const hasOpenOrderForPriceGroup = (
openOrderPrices: number[],
price: number,
grouping: number,
isGrouped: boolean
2022-11-01 11:13:02 -07:00
) => {
if (!isGrouped) {
return !!openOrderPrices.find((ooPrice) => {
return ooPrice === price
})
}
2022-11-01 11:13:02 -07:00
return !!openOrderPrices.find((ooPrice) => {
return ooPrice >= price - grouping && ooPrice <= price + grouping
2022-11-01 11:13:02 -07:00
})
}
2023-02-21 18:24:46 -08:00
const updatePerpMarketOnGroup = (book: BookSide, side: 'bids' | 'asks') => {
2023-03-08 16:34:13 -08:00
const group = mangoStore.getState().group
const perpMarket = group?.getPerpMarketByMarketIndex(
book.perpMarket.perpMarketIndex
)
if (perpMarket) {
perpMarket[`_${side}`] = book
// mangoStore.getState().actions.fetchOpenOrders()
}
2023-02-21 18:24:46 -08:00
}
type OrderbookData = {
bids: cumOrderbookSide[]
asks: cumOrderbookSide[]
spread: number
spreadPercentage: number
}
2022-09-20 18:18:04 -07:00
2022-09-20 13:05:50 -07:00
const Orderbook = () => {
2022-10-03 03:38:05 -07:00
const { t } = useTranslation(['common', 'trade'])
2023-01-14 21:01:30 -08:00
const {
serumOrPerpMarket: market,
baseSymbol,
quoteSymbol,
} = useSelectedMarket()
const connection = mangoStore((s) => s.connection)
2022-09-13 23:24:26 -07:00
const [isScrolled, setIsScrolled] = useState(false)
const [orderbookData, setOrderbookData] = useState<OrderbookData | null>(null)
2022-09-13 23:24:26 -07:00
const [grouping, setGrouping] = useState(0.01)
2023-02-21 21:11:56 -08:00
const [tickSize, setTickSize] = useState(0)
2023-03-08 12:23:25 -08:00
const [showBids, setShowBids] = useState(true)
const [showAsks, setShowAsks] = useState(true)
const [useOrderbookFeed, setUseOrderbookFeed] = useState(
localStorage.getItem(USE_ORDERBOOK_FEED_KEY) !== null
? localStorage.getItem(USE_ORDERBOOK_FEED_KEY) === 'true'
: true
)
2022-09-13 23:24:26 -07:00
const currentOrderbookData = useRef<OrderbookL2>()
const orderbookElRef = useRef<HTMLDivElement>(null)
2022-09-26 20:51:31 -07:00
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
2022-09-13 23:24:26 -07:00
2023-04-18 19:01:58 -07:00
const depth = useMemo(() => {
return isMobile ? 9 : 40
2022-10-10 19:16:13 -07:00
}, [isMobile])
2023-04-18 19:01:58 -07:00
const depthArray: number[] = useMemo(() => {
return Array(depth).fill(0)
}, [depth])
const orderbookFeed = useRef<OrderbookFeed | null>(null)
2022-09-13 23:24:26 -07:00
useEffect(() => {
2023-02-21 21:11:56 -08:00
if (market && market.tickSize !== tickSize) {
setTickSize(market.tickSize)
setGrouping(market.tickSize)
}
}, [market, tickSize])
2022-09-13 23:24:26 -07:00
2023-02-19 22:05:07 -08:00
const verticallyCenterOrderbook = useCallback(() => {
const element = orderbookElRef.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
}
}
}, [])
2022-09-13 23:24:26 -07:00
useEffect(
() =>
mangoStore.subscribe(
(state) => state.selectedMarket.orderbook,
(newOrderbook) => {
if (
newOrderbook &&
market &&
2023-02-19 22:05:07 -08:00
!isEqual(currentOrderbookData.current, newOrderbook)
) {
// check if user has open orders so we can highlight them on orderbook
const openOrders = mangoStore.getState().mangoAccount.openOrders
2023-02-19 22:05:07 -08:00
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) {
2023-03-25 14:16:42 -07:00
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)
}
}
}
2022-09-13 23:24:26 -07:00
),
[grouping, market, isScrolled, verticallyCenterOrderbook]
2022-09-13 23:24:26 -07:00
)
const bidAccountAddress = useMemo(() => {
if (!market) return ''
const bidsPk =
market instanceof Market ? market['_decoded'].bids : market.bids
return bidsPk.toString()
}, [market])
const askAccountAddress = useMemo(() => {
if (!market) return ''
const asksPk =
market instanceof Market ? market['_decoded'].asks : market.asks
return asksPk.toString()
}, [market])
// subscribe to the bids and asks orderbook accounts
2022-09-13 23:24:26 -07:00
useEffect(() => {
const set = mangoStore.getState().set
2022-10-10 19:16:13 -07:00
const client = mangoStore.getState().client
const group = mangoStore.getState().group
2023-03-25 11:47:37 -07:00
const market = getMarket()
if (!group || !market) return
2022-12-05 18:53:59 -08:00
2023-04-22 10:35:57 -07:00
if (useOrderbookFeed) {
if (!orderbookFeed.current) {
orderbookFeed.current = new OrderbookFeed(
`wss://api.mngo.cloud/orderbook/v1/`,
{
reconnectionIntervalMs: 5_000,
reconnectionMaxAttempts: 6,
}
)
}
2023-04-22 10:35:57 -07:00
let hasConnected = false
orderbookFeed.current.onConnect(() => {
if (!orderbookFeed.current) return
2023-04-22 10:35:57 -07:00
console.log('[OrderbookFeed] connected')
hasConnected = true
orderbookFeed.current.subscribe({
2023-04-22 10:35:57 -07:00
marketId: market.publicKey.toBase58(),
2022-11-15 20:12:51 -08:00
})
2023-04-22 10:35:57 -07:00
})
orderbookFeed.current.onDisconnect((reconnectionAttemptsExhausted) => {
2023-04-22 10:35:57 -07:00
// fallback to rpc if we couldn't reconnect or if we never connected
if (reconnectionAttemptsExhausted || !hasConnected) {
console.warn('[OrderbookFeed] disconnected')
setUseOrderbookFeed(false)
} else {
console.log('[OrderbookFeed] reconnecting...')
}
})
let lastWriteVersion = 0
orderbookFeed.current.onL2Update((update) => {
2023-04-22 10:35:57 -07:00
const selectedMarket = mangoStore.getState().selectedMarket
if (!useOrderbookFeed || !selectedMarket || !selectedMarket.current)
return
2023-06-22 11:29:40 -07:00
const selectedMarketKey =
selectedMarket.current instanceof Serum3Market
? selectedMarket.current['serumMarketExternal']
: selectedMarket.current.publicKey
if (update.market != selectedMarketKey.toBase58()) return
2023-04-22 10:35:57 -07:00
// ensure updates are applied in the correct order by checking slot and writeVersion
const lastSeenSlot =
update.side == 'bid'
? mangoStore.getState().selectedMarket.lastSeenSlot.bids
: mangoStore.getState().selectedMarket.lastSeenSlot.asks
if (update.slot < lastSeenSlot) return
if (
update.slot == lastSeenSlot &&
update.writeVersion < lastWriteVersion
)
return
lastWriteVersion = update.writeVersion
const bookside =
update.side == 'bid'
? selectedMarket.orderbook.bids
: selectedMarket.orderbook.asks
const new_bookside = Array.from(bookside)
for (const diff of update.update) {
// find existing level for each update
const levelIndex = new_bookside.findIndex(
(level) => level && level.length && level[0] === diff[0]
)
if (diff[1] > 0) {
// level being added or updated
if (levelIndex !== -1) {
new_bookside[levelIndex] = diff
} else {
// add new level and resort
new_bookside.push(diff)
new_bookside.sort((a, b) => {
return update.side == 'bid' ? b[0] - a[0] : a[0] - b[0]
})
2023-02-21 18:24:46 -08:00
}
2023-04-22 10:35:57 -07:00
} else {
// level being removed if zero size
if (levelIndex !== -1) {
new_bookside.splice(levelIndex, 1)
} else {
console.warn('[OrderbookFeed] tried to remove missing level')
2023-04-22 10:35:57 -07:00
}
}
}
set((state) => {
if (update.side == 'bid') {
state.selectedMarket.bidsAccount = undefined
state.selectedMarket.orderbook.bids = new_bookside
state.selectedMarket.lastSeenSlot.bids = update.slot
} else {
state.selectedMarket.asksAccount = undefined
state.selectedMarket.orderbook.asks = new_bookside
state.selectedMarket.lastSeenSlot.asks = update.slot
}
})
})
orderbookFeed.current.onL2Checkpoint((checkpoint) => {
if (
!useOrderbookFeed ||
checkpoint.market !== market.publicKey.toBase58()
)
return
2023-04-22 10:35:57 -07:00
set((state) => {
state.selectedMarket.lastSeenSlot.bids = checkpoint.slot
state.selectedMarket.lastSeenSlot.asks = checkpoint.slot
state.selectedMarket.bidsAccount = undefined
state.selectedMarket.asksAccount = undefined
state.selectedMarket.orderbook.bids = checkpoint.bids
state.selectedMarket.orderbook.asks = checkpoint.asks
})
})
return () => {
if (!orderbookFeed.current) return
console.log(
`[OrderbookFeed] unsubscribe ${market.publicKey.toBase58()}`
)
orderbookFeed.current.unsubscribe(market.publicKey.toBase58())
2023-04-22 10:35:57 -07:00
}
} else {
console.log(`[OrderbookRPC] subscribe ${market.publicKey.toBase58()}`)
2023-04-22 10:35:57 -07:00
let bidSubscriptionId: number | undefined = undefined
let askSubscriptionId: number | undefined = undefined
const bidsPk = new PublicKey(bidAccountAddress)
if (bidsPk) {
connection
.getAccountInfoAndContext(bidsPk)
.then(({ context, value: info }) => {
if (!info) return
const decodedBook = decodeBook(client, market, info, 'bids')
2022-11-15 20:12:51 -08:00
set((state) => {
2023-04-22 10:35:57 -07:00
state.selectedMarket.lastSeenSlot.bids = context.slot
2022-11-30 07:46:20 -08:00
state.selectedMarket.bidsAccount = decodedBook
state.selectedMarket.orderbook.bids = decodeBookL2(decodedBook)
2022-11-15 20:12:51 -08:00
})
})
2023-04-22 10:35:57 -07:00
bidSubscriptionId = connection.onAccountChange(
bidsPk,
(info, context) => {
const lastSeenSlot =
mangoStore.getState().selectedMarket.lastSeenSlot.bids
if (context.slot > lastSeenSlot) {
const market = getMarket()
if (!market) return
const decodedBook = decodeBook(client, market, info, 'bids')
if (decodedBook instanceof BookSide) {
updatePerpMarketOnGroup(decodedBook, 'bids')
}
set((state) => {
state.selectedMarket.bidsAccount = decodedBook
state.selectedMarket.orderbook.bids = decodeBookL2(decodedBook)
state.selectedMarket.lastSeenSlot.bids = context.slot
})
2023-02-21 18:24:46 -08:00
}
2023-04-22 10:35:57 -07:00
},
'processed'
)
}
const asksPk = new PublicKey(askAccountAddress)
if (asksPk) {
connection
.getAccountInfoAndContext(asksPk)
.then(({ context, value: info }) => {
if (!info) return
const decodedBook = decodeBook(client, market, info, 'asks')
2022-11-15 20:12:51 -08:00
set((state) => {
2022-11-30 07:46:20 -08:00
state.selectedMarket.asksAccount = decodedBook
state.selectedMarket.orderbook.asks = decodeBookL2(decodedBook)
state.selectedMarket.lastSeenSlot.asks = context.slot
2022-11-15 20:12:51 -08:00
})
2023-04-22 10:35:57 -07:00
})
askSubscriptionId = connection.onAccountChange(
asksPk,
(info, context) => {
const lastSeenSlot =
mangoStore.getState().selectedMarket.lastSeenSlot.asks
if (context.slot > lastSeenSlot) {
const market = getMarket()
if (!market) return
const decodedBook = decodeBook(client, market, info, 'asks')
if (decodedBook instanceof BookSide) {
updatePerpMarketOnGroup(decodedBook, 'asks')
}
set((state) => {
state.selectedMarket.asksAccount = decodedBook
state.selectedMarket.orderbook.asks = decodeBookL2(decodedBook)
state.selectedMarket.lastSeenSlot.asks = context.slot
})
}
},
'processed'
)
2022-11-15 20:12:51 -08:00
}
2023-04-22 10:35:57 -07:00
return () => {
console.log(`[OrderbookRPC] unsubscribe ${market.publicKey.toBase58()}`)
2023-04-22 10:35:57 -07:00
if (typeof bidSubscriptionId !== 'undefined') {
connection.removeAccountChangeListener(bidSubscriptionId)
}
if (typeof askSubscriptionId !== 'undefined') {
connection.removeAccountChangeListener(askSubscriptionId)
}
2022-11-15 20:12:51 -08:00
}
2022-09-13 23:24:26 -07:00
}
2023-04-22 10:35:57 -07:00
}, [bidAccountAddress, askAccountAddress, connection, useOrderbookFeed])
2022-09-13 23:24:26 -07:00
useEffect(() => {
const market = getMarket()
if (!orderbookFeed.current || !market) return
console.log(`[OrderbookFeed] subscribe ${market.publicKey.toBase58()}`)
orderbookFeed.current.subscribe({
marketId: market.publicKey.toBase58(),
})
}, [bidAccountAddress])
useEffect(() => {
2022-09-25 22:32:48 -07:00
window.addEventListener('resize', verticallyCenterOrderbook)
}, [verticallyCenterOrderbook])
2023-01-02 14:41:16 -08:00
const resetOrderbook = useCallback(async () => {
2023-03-08 12:23:25 -08:00
setShowBids(true)
setShowAsks(true)
2023-01-02 14:41:16 -08:00
await sleep(300)
verticallyCenterOrderbook()
}, [verticallyCenterOrderbook])
2023-01-02 14:41:16 -08:00
const onGroupSizeChange = useCallback((groupSize: number) => {
2022-09-18 05:53:28 -07:00
setGrouping(groupSize)
}, [])
2022-09-18 05:53:28 -07:00
2022-09-20 22:22:50 -07:00
const handleScroll = useCallback(() => {
setIsScrolled(true)
}, [])
const toggleSides = (side: string) => {
if (side === 'bids') {
2023-03-08 12:23:25 -08:00
setShowBids(true)
setShowAsks(false)
} else {
2023-03-08 12:23:25 -08:00
setShowBids(false)
setShowAsks(true)
}
}
2022-09-13 23:24:26 -07:00
return (
2022-09-20 13:05:50 -07:00
<div className="flex h-full flex-col">
<div className="flex items-center justify-between border-b border-th-bkg-3 px-4 py-2">
2023-04-18 19:01:58 -07:00
<div
id="trade-step-three"
className="hidden items-center space-x-1.5 md:flex"
>
2022-09-20 13:05:50 -07:00
<Tooltip
2023-04-18 19:01:58 -07:00
className={`${!showAsks ? 'hidden' : ''}`}
content={t('trade:show-bids')}
2023-01-02 14:48:05 -08:00
placement="bottom"
2022-09-20 13:05:50 -07:00
>
<button
className={`rounded ${
2023-03-08 12:23:25 -08:00
showAsks ? 'bg-th-bkg-3' : 'bg-th-bkg-2'
2023-04-19 18:12:45 -07:00
} flex h-6 w-6 items-center justify-center hover:border-th-fgd-4 focus:outline-none focus-visible:bg-th-bkg-4 disabled:cursor-not-allowed`}
onClick={() => toggleSides('bids')}
2022-09-19 17:26:59 -07:00
>
2022-09-20 13:05:50 -07:00
<OrderbookIcon className="h-4 w-4" side="buy" />
</button>
</Tooltip>
<Tooltip
2023-04-18 19:01:58 -07:00
className={`${!showBids ? 'hidden' : ''}`}
content={t('trade:show-asks')}
2023-01-02 14:48:05 -08:00
placement="bottom"
2022-09-20 13:05:50 -07:00
>
<button
className={`rounded ${
2023-03-08 12:23:25 -08:00
showBids ? 'bg-th-bkg-3' : 'bg-th-bkg-2'
2023-04-19 18:12:45 -07:00
} flex h-6 w-6 items-center justify-center hover:border-th-fgd-4 focus:outline-none focus-visible:bg-th-bkg-4 disabled:cursor-not-allowed`}
onClick={() => toggleSides('asks')}
2022-09-18 05:53:28 -07:00
>
2022-09-20 13:05:50 -07:00
<OrderbookIcon className="h-4 w-4" side="sell" />
</button>
2022-09-18 05:53:28 -07:00
</Tooltip>
2023-04-18 19:01:58 -07:00
<Tooltip content={'Reset and center orderbook'} placement="bottom">
2023-01-02 14:41:16 -08:00
<button
2023-04-19 18:12:45 -07:00
className="flex h-6 w-6 items-center justify-center rounded bg-th-bkg-3 hover:border-th-fgd-4 focus:outline-none focus-visible:bg-th-bkg-4 disabled:cursor-not-allowed"
2023-01-02 14:41:16 -08:00
onClick={resetOrderbook}
>
2023-01-02 16:30:19 -08:00
<ArrowPathIcon className="h-4 w-4" />
2023-01-02 14:41:16 -08:00
</button>
</Tooltip>
2022-09-18 05:53:28 -07:00
</div>
2022-10-10 19:16:13 -07:00
{market ? (
2023-04-18 19:01:58 -07:00
<>
<p className="text-xs md:hidden">{t('trade:grouping')}:</p>
<div id="trade-step-four">
<Tooltip
className="hidden md:block"
content={t('trade:grouping')}
placement="left"
delay={100}
>
<GroupSize
tickSize={market.tickSize}
onChange={onGroupSizeChange}
value={grouping}
/>
</Tooltip>
</div>
</>
2022-09-25 22:32:48 -07:00
) : null}
2022-09-20 13:05:50 -07:00
</div>
2022-09-21 21:46:30 -07:00
<div className="grid grid-cols-2 px-4 pt-2 pb-1 text-xxs text-th-fgd-4">
2023-01-14 21:01:30 -08:00
<div className="col-span-1 text-right">
{t('trade:size')} ({baseSymbol})
</div>
<div className="col-span-1 text-right">
{t('price')} ({quoteSymbol})
</div>
2022-09-20 13:05:50 -07:00
</div>
<div
className="hide-scroll relative h-full overflow-y-scroll"
ref={orderbookElRef}
2022-09-20 22:22:50 -07:00
onScroll={handleScroll}
2022-09-20 13:05:50 -07:00
>
2023-03-08 12:23:25 -08:00
{showAsks
? depthArray.map((_x, idx) => {
let index = idx
2023-03-08 12:23:25 -08:00
const reverse = showAsks && !showBids
if (orderbookData?.asks && !reverse) {
const lengthDiff = depthArray.length - orderbookData.asks.length
if (lengthDiff > 0) {
index = index < lengthDiff ? -1 : Math.abs(lengthDiff - index)
}
}
2022-09-20 13:05:50 -07:00
return (
<div className="h-[24px]" key={idx}>
2022-10-10 19:16:13 -07:00
{!!orderbookData?.asks[index] && market ? (
<MemoizedOrderbookRow
2022-10-10 19:16:13 -07:00
minOrderSize={market.minOrderSize}
tickSize={market.tickSize}
hasOpenOrder={orderbookData?.asks[index].isUsersOrder}
2022-09-20 13:05:50 -07:00
key={orderbookData?.asks[index].price}
price={orderbookData?.asks[index].price}
2022-09-26 12:56:06 -07:00
size={orderbookData?.asks[index].size}
2022-09-20 13:05:50 -07:00
side="sell"
2022-09-26 12:56:06 -07:00
sizePercent={orderbookData?.asks[index].sizePercent}
2022-09-29 13:00:36 -07:00
cumulativeSizePercent={
orderbookData?.asks[index].cumulativeSizePercent
}
grouping={grouping}
/>
) : null}
</div>
2022-09-20 13:05:50 -07:00
)
})
: null}
2023-03-08 12:23:25 -08:00
{showBids && showAsks ? (
2022-09-21 21:25:24 -07:00
<div
className="my-2 grid grid-cols-2 border-y border-th-bkg-3 py-2 px-4 text-xs text-th-fgd-4"
id="trade-step-nine"
>
2022-09-20 13:05:50 -07:00
<div className="col-span-1 flex justify-between">
2022-10-03 03:38:05 -07:00
<div className="text-xxs">{t('trade:spread')}</div>
2022-09-21 21:46:30 -07:00
<div className="font-mono">
2022-09-20 13:05:50 -07:00
{orderbookData?.spreadPercentage.toFixed(2)}%
</div>
</div>
2022-09-21 21:46:30 -07:00
<div className="col-span-1 text-right font-mono">
{orderbookData?.spread
? orderbookData.spread < SHOW_EXPONENTIAL_THRESHOLD
? orderbookData.spread.toExponential()
: formatNumericValue(
orderbookData.spread,
market ? getDecimalCount(market.tickSize) : undefined
)
: null}
2022-09-20 13:05:50 -07:00
</div>
</div>
) : null}
2023-03-08 12:23:25 -08:00
{showBids
2022-09-20 13:05:50 -07:00
? depthArray.map((_x, index) => (
2022-09-25 22:32:48 -07:00
<div className="h-[24px]" key={index}>
2022-10-10 19:16:13 -07:00
{!!orderbookData?.bids[index] && market ? (
2022-09-20 13:05:50 -07:00
<MemoizedOrderbookRow
2022-10-10 19:16:13 -07:00
minOrderSize={market.minOrderSize}
tickSize={market.tickSize}
hasOpenOrder={orderbookData?.bids[index].isUsersOrder}
2022-09-20 13:05:50 -07:00
price={orderbookData?.bids[index].price}
2022-09-26 12:56:06 -07:00
size={orderbookData?.bids[index].size}
2022-09-20 13:05:50 -07:00
side="buy"
2022-09-26 12:56:06 -07:00
sizePercent={orderbookData?.bids[index].sizePercent}
2022-09-29 13:00:36 -07:00
cumulativeSizePercent={
orderbookData?.bids[index].cumulativeSizePercent
2022-09-20 13:05:50 -07:00
}
grouping={grouping}
/>
) : null}
</div>
))
: null}
2022-09-13 23:24:26 -07:00
</div>
</div>
)
}
const OrderbookRow = ({
side,
price,
size,
sizePercent,
// invert,
2022-11-01 11:13:02 -07:00
hasOpenOrder,
2022-09-19 16:26:30 -07:00
minOrderSize,
2022-09-29 13:00:36 -07:00
cumulativeSizePercent,
2022-09-19 16:26:30 -07:00
tickSize,
2022-09-13 23:24:26 -07:00
grouping,
}: {
side: 'buy' | 'sell'
price: number
size: number
sizePercent: number
2022-09-29 13:00:36 -07:00
cumulativeSizePercent: number
2022-11-01 11:13:02 -07:00
hasOpenOrder: boolean
2022-09-13 23:24:26 -07:00
// invert: boolean
grouping: number
2022-09-19 16:26:30 -07:00
minOrderSize: number
tickSize: number
2022-09-13 23:24:26 -07:00
}) => {
const element = useRef<HTMLDivElement>(null)
2022-11-22 21:38:31 -08:00
const [animationSettings] = useLocalStorageState(
ANIMATION_SETTINGS_KEY,
INITIAL_ANIMATION_SETTINGS
)
2022-09-13 23:24:26 -07:00
const flashClassName = side === 'sell' ? 'red-flash' : 'green-flash'
useEffect(() => {
2022-11-22 21:38:31 -08:00
animationSettings['orderbook-flash'].active &&
2022-09-13 23:24:26 -07:00
!element.current?.classList.contains(`${flashClassName}`) &&
element.current?.classList.add(`${flashClassName}`)
const id = setTimeout(
() =>
element.current?.classList.contains(`${flashClassName}`) &&
element.current?.classList.remove(`${flashClassName}`),
2022-09-26 12:56:06 -07:00
500
2022-09-13 23:24:26 -07:00
)
return () => clearTimeout(id)
}, [price, size])
2022-09-26 12:56:06 -07:00
const formattedSize = useMemo(() => {
return minOrderSize && !isNaN(size)
2022-09-19 16:26:30 -07:00
? floorToDecimal(size, getDecimalCount(minOrderSize))
2023-04-22 10:35:57 -07:00
: new Decimal(size ?? -1)
2022-09-26 12:56:06 -07:00
}, [size, minOrderSize])
2022-09-13 23:24:26 -07:00
2022-09-26 12:56:06 -07:00
const formattedPrice = useMemo(() => {
return tickSize && !isNaN(price)
2022-09-19 16:26:30 -07:00
? floorToDecimal(price, getDecimalCount(tickSize))
2022-09-13 23:24:26 -07:00
: new Decimal(price)
2022-09-26 12:56:06 -07:00
}, [price, tickSize])
2022-09-13 23:24:26 -07:00
2022-09-26 12:56:06 -07:00
const handlePriceClick = useCallback(() => {
const set = mangoStore.getState().set
set((state) => {
state.tradeForm.price = formattedPrice.toFixed()
state.tradeForm.tradeType = 'Limit'
if (state.tradeForm.baseSize) {
const quoteSize = floorToDecimal(
formattedPrice.mul(new Decimal(state.tradeForm.baseSize)),
getDecimalCount(tickSize)
)
state.tradeForm.quoteSize = quoteSize.toFixed()
}
2022-09-26 12:56:06 -07:00
})
}, [formattedPrice, tickSize])
2022-09-13 23:24:26 -07:00
2022-12-08 15:56:36 -08:00
const handleSizeClick = useCallback(() => {
const set = mangoStore.getState().set
set((state) => {
state.tradeForm.baseSize = formattedSize.toString()
if (formattedSize && state.tradeForm.price) {
const quoteSize = floorToDecimal(
formattedSize.mul(new Decimal(state.tradeForm.price)),
getDecimalCount(tickSize)
)
state.tradeForm.quoteSize = quoteSize.toString()
}
2022-12-08 15:56:36 -08:00
})
}, [formattedSize, tickSize])
2022-09-13 23:24:26 -07:00
const groupingDecimalCount = useMemo(
() => getDecimalCount(grouping),
[grouping]
)
const minOrderSizeDecimals = useMemo(
2022-09-19 16:26:30 -07:00
() => getDecimalCount(minOrderSize),
[minOrderSize]
2022-09-13 23:24:26 -07:00
)
2022-09-19 16:26:30 -07:00
if (!minOrderSize) return null
2022-09-13 23:24:26 -07:00
return (
<div
className={`relative flex h-[24px] cursor-pointer justify-between border-b border-b-th-bkg-1 text-sm`}
2022-09-13 23:24:26 -07:00
ref={element}
>
<>
2022-12-08 15:56:36 -08:00
<div className="flex h-full w-full items-center justify-between text-th-fgd-3 hover:bg-th-bkg-2">
<div
className="flex h-full w-full items-center justify-start pl-2 hover:underline"
onClick={handleSizeClick}
>
2022-09-18 16:01:57 -07:00
<div
style={{ fontFeatureSettings: 'zero 1' }}
2022-09-29 13:00:36 -07:00
className={`z-10 w-full text-right font-mono text-xs ${
2022-11-30 19:32:32 -08:00
hasOpenOrder ? 'text-th-active' : ''
2022-09-18 16:01:57 -07:00
}`}
>
{size >= 1000000
? sizeCompacter.format(size)
: formattedSize.toFixed(minOrderSizeDecimals)}
2022-09-18 16:01:57 -07:00
</div>
2022-09-13 23:24:26 -07:00
</div>
2022-12-08 15:56:36 -08:00
<div
className={`z-10 flex h-full w-full items-center pr-4 hover:underline`}
onClick={handlePriceClick}
>
<div className="w-full text-right font-mono text-xs">
{price < SHOW_EXPONENTIAL_THRESHOLD
? formattedPrice.toExponential()
: formattedPrice.toFixed(groupingDecimalCount)}
2022-12-08 15:56:36 -08:00
</div>
2022-09-13 23:24:26 -07:00
</div>
</div>
<Line
2022-09-29 13:00:36 -07:00
className={`absolute left-0 opacity-40 brightness-125 ${
2022-11-30 19:32:32 -08:00
side === 'buy' ? `bg-th-up-muted` : `bg-th-down-muted`
2022-09-13 23:24:26 -07:00
}`}
2022-09-29 13:00:36 -07:00
data-width={Math.max(sizePercent, 0.5) + '%'}
/>
<Line
className={`absolute left-0 opacity-70 ${
2022-11-30 19:32:32 -08:00
side === 'buy' ? `bg-th-up` : `bg-th-down`
2022-09-29 13:00:36 -07:00
}`}
data-width={
Math.max((cumulativeSizePercent / 100) * sizePercent, 0.1) + '%'
}
2022-09-13 23:24:26 -07:00
/>
</>
</div>
)
}
2022-09-19 16:26:30 -07:00
const MemoizedOrderbookRow = React.memo(OrderbookRow)
2022-11-20 15:35:59 -08:00
const Line = (props: {
className: string
invert?: boolean
'data-width': string
}) => {
2022-09-13 23:24:26 -07:00
return (
<div
className={`${props.className}`}
style={{
textAlign: props.invert ? 'left' : 'right',
height: '100%',
width: `${props['data-width'] ? props['data-width'] : ''}`,
}}
/>
)
}
export default Orderbook