import React, { useRef, useEffect, useState } from 'react' import Big from 'big.js' import { isEqual as isEqualLodash } from 'lodash' import useInterval from '../hooks/useInterval' import usePrevious from '../hooks/usePrevious' import { isEqual, getDecimalCount, usdFormatter } from '../utils/' import { ArrowUpIcon, ArrowDownIcon, SwitchHorizontalIcon, } from '@heroicons/react/solid' import { CumulativeSizeIcon, StepSizeIcon } from './icons' import useMarkPrice from '../hooks/useMarkPrice' import { ElementTitle } from './styles' import useMangoStore from '../stores/useMangoStore' import Tooltip from './Tooltip' import GroupSize from './GroupSize' import { useViewport } from '../hooks/useViewport' import { breakpoints } from './TradePageGrid' import { FlipCard, FlipCardBack, FlipCardFront, FlipCardInner, } from './FlipCard' import { useTranslation } from 'next-i18next' import FloatingElement from './FloatingElement' import useLocalStorageState from '../hooks/useLocalStorageState' import { ORDERBOOK_FLASH_KEY } from './SettingsModal' import { mangoGroupConfigSelector, marketConfigSelector, marketSelector, orderbookSelector, setStoreSelector, } from '../stores/selectors' import { Market } from '@project-serum/serum' import { PerpMarket } from '@blockworks-foundation/mango-client' const Line = (props) => { return (
) } const groupBy = (ordersArray, market, grouping: number, isBids: boolean) => { if (!ordersArray || !market || !grouping || grouping == market?.tickSize) { return ordersArray || [] } const groupFloors = {} 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) : bigOrder.div(bigGrouping).round(0, Big.roundUp).times(bigGrouping) 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(function (a: number[], b: number[]) { if (!a || !b) { return -1 } return isBids ? b[0] - a[0] : a[0] - b[0] }) return sortedGroups } const getCumulativeOrderbookSide = ( orders, totalSize, maxSize, depth, backwards = false ) => { let cumulative = orders .slice(0, depth) .reduce((cumulative, [price, size], i) => { const cumulativeSize = (cumulative[i - 1]?.cumulativeSize || 0) + size cumulative.push({ price, size, cumulativeSize, sizePercent: Math.round((cumulativeSize / (totalSize || 1)) * 100), maxSizePercent: Math.round((size / (maxSize || 1)) * 100), }) return cumulative }, []) if (backwards) { cumulative = cumulative.reverse() } return cumulative } const hasOpenOrderForPriceGroup = (openOrderPrices, price, grouping) => { return !!openOrderPrices.find((ooPrice) => { return ooPrice >= parseFloat(price) && ooPrice < price + grouping }) } export default function Orderbook({ depth = 8 }) { const { t } = useTranslation('common') const groupConfig = useMangoStore(mangoGroupConfigSelector) const marketConfig = useMangoStore(marketConfigSelector) const orderbook = useMangoStore(orderbookSelector) const market = useMangoStore(marketSelector) const markPrice = useMarkPrice() const { width } = useViewport() const isMobile = width ? width < breakpoints.sm : false const currentOrderbookData = useRef(null) const nextOrderbookData = useRef(null) const previousDepth = usePrevious(depth) const [openOrderPrices, setOpenOrderPrices] = useState([]) const [orderbookData, setOrderbookData] = useState(null) const [defaultLayout, setDefaultLayout] = useState(true) const [displayCumulativeSize, setDisplayCumulativeSize] = useState(false) const [grouping, setGrouping] = useState(0.01) const [tickSize, setTickSize] = useState(0) const previousGrouping = usePrevious(grouping) useEffect(() => { if (market && market.tickSize !== tickSize) { setTickSize(market.tickSize) setGrouping(market.tickSize) } }, [market]) useInterval(() => { if ( !currentOrderbookData.current || !isEqualLodash( currentOrderbookData.current.bids, nextOrderbookData.current.bids ) || !isEqualLodash( currentOrderbookData.current.asks, nextOrderbookData.current.asks ) || previousDepth !== depth || previousGrouping !== grouping ) { // check if user has open orders so we can highlight them on orderbook const openOrders = useMangoStore.getState().selectedMangoAccount.openOrders const newOpenOrderPrices = openOrders?.length ? openOrders .filter(({ market }) => market.account.publicKey.equals(marketConfig.publicKey) ) .map(({ order }) => order.price) : [] if (!isEqualLodash(newOpenOrderPrices, openOrderPrices)) { setOpenOrderPrices(newOpenOrderPrices) } // updated orderbook data const bids = groupBy(orderbook?.bids, market, grouping, true) || [] const asks = groupBy(orderbook?.asks, market, grouping, false) || [] const sum = (total, [, size], index) => index < depth ? total + size : total const totalSize = bids.reduce(sum, 0) + asks.reduce(sum, 0) const maxSize = Math.max( ...asks.map(function (a) { return a[1] }) ) + Math.max( ...bids.map(function (b) { return b[1] }) ) const bidsToDisplay = defaultLayout ? getCumulativeOrderbookSide(bids, totalSize, maxSize, depth, false) : getCumulativeOrderbookSide(bids, totalSize, maxSize, depth / 2, false) const asksToDisplay = defaultLayout ? getCumulativeOrderbookSide(asks, totalSize, maxSize, depth, false) : getCumulativeOrderbookSide( asks, totalSize, maxSize, (depth + 1) / 2, true ) currentOrderbookData.current = { bids: orderbook?.bids, asks: orderbook?.asks, } if (bidsToDisplay[0] || asksToDisplay[0]) { const bid = bidsToDisplay[0]?.price const ask = defaultLayout ? asksToDisplay[0]?.price : asksToDisplay[asksToDisplay.length - 1]?.price let spread = 0, spreadPercentage = 0 if (bid && ask) { spread = ask - bid spreadPercentage = (spread / ask) * 100 } setOrderbookData({ bids: bidsToDisplay, asks: isMobile ? asksToDisplay.reverse() : asksToDisplay, spread, spreadPercentage, }) } else { setOrderbookData(null) } } }, 500) useEffect(() => { nextOrderbookData.current = { bids: orderbook?.bids, asks: orderbook?.asks, } }, [orderbook]) const handleLayoutChange = () => { setDefaultLayout(!defaultLayout) setOrderbookData((prevState) => ({ ...orderbookData, asks: prevState.asks.reverse(), })) } const onGroupSizeChange = (groupSize) => { setGrouping(groupSize) } return !isMobile ? (