import React, { useRef, useEffect, useState } from 'react' import styled from '@emotion/styled' import { css, keyframes } from '@emotion/react' 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 FloatingElement from './FloatingElement' import { useOpenOrders } from '../hooks/useOpenOrders' const Line = styled.div` text-align: ${(props) => (props.invert ? 'left' : 'right')}; height: 100%; filter: opacity(40%); ${(props) => props['data-width'] && `width: ${props['data-width']};`} ` const fadeIn = keyframes` from { opacity: 0; } to { opacity: 1; } ` const FlipCard = styled.div` background-color: transparent; height: 100%; perspective: 1000px; ` const FlipCardInner = styled.div` position: relative; width: 100%; height: 100%; text-align: center; transition: transform 0.8s ease-out; transform-style: preserve-3d; transform: ${({ flip }) => (flip ? 'rotateY(0deg)' : 'rotateY(180deg)')}; ` const FlipCardFront = styled.div` position: absolute; width: 100%; height: 100%; ` const FlipCardBack = styled.div` position: absolute; width: 100%; height: 100%; transform: rotateY(180deg); ` const StyledFloatingElement = styled(FloatingElement)` animation: ${css` ${fadeIn} 1s linear `}; overflow: hidden; ` 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 } export default function Orderbook({ depth = 8 }) { const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config) const marketConfig = useMangoStore((s) => s.selectedMarket.config) const orderbook = useMangoStore((s) => s.selectedMarket.orderBook) const market = useMangoStore((s) => s.selectedMarket.current) const markPrice = useMarkPrice() const openOrders = useOpenOrders() const openOrderPrices = openOrders?.length ? openOrders.map(({ order }) => order.price) : [] const currentOrderbookData = useRef(null) const lastOrderbookData = useRef(null) const previousDepth = usePrevious(depth) const [orderbookData, setOrderbookData] = useState(null) const [defaultLayout, setDefaultLayout] = useState(true) const [displayCumulativeSize, setDisplayCumulativeSize] = useState(false) useInterval(() => { if ( !currentOrderbookData.current || JSON.stringify(currentOrderbookData.current) !== JSON.stringify(lastOrderbookData.current) || previousDepth !== depth ) { const bids = orderbook?.bids || [] const asks = orderbook?.asks || [] 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, spreadPercentage if (bid && ask) { spread = ask - bid spreadPercentage = (spread / ask) * 100 } setOrderbookData({ bids: bidsToDisplay, asks: asksToDisplay, spread, spreadPercentage, }) } else { setOrderbookData(null) } } }, 250) useEffect(() => { lastOrderbookData.current = { bids: orderbook?.bids, asks: orderbook?.asks, } }, [orderbook]) const handleLayoutChange = () => { setDefaultLayout(!defaultLayout) setOrderbookData((prevState) => ({ ...orderbookData, asks: prevState.asks.reverse(), })) } return ( <> {defaultLayout ? (
Orderbook
{displayCumulativeSize ? 'Cumulative ' : ''}Size ( {marketConfig.baseSymbol})
Price ({groupConfig.quoteSymbol})
{displayCumulativeSize ? 'Cumulative ' : ''}Size ( {marketConfig.baseSymbol})
{orderbookData?.bids.map( ({ price, size, cumulativeSize, sizePercent, maxSizePercent, }) => ( ) )}
{orderbookData?.asks.map( ({ price, size, cumulativeSize, sizePercent, maxSizePercent, }) => ( ) )}
Spread
{orderbookData?.spread?.toFixed(2)}
{orderbookData?.spreadPercentage?.toFixed(2)}%
) : (
Orderbook
{displayCumulativeSize ? 'Cumulative ' : ''}Size ( {marketConfig.baseSymbol})
Price ({groupConfig.quoteSymbol})
{orderbookData?.asks.map( ({ price, size, cumulativeSize, sizePercent, maxSizePercent, }) => ( ) )}
Spread
{orderbookData?.spread.toFixed(2)}
{orderbookData?.spreadPercentage.toFixed(2)}%
{orderbookData?.bids.map( ({ price, size, cumulativeSize, sizePercent, maxSizePercent, }) => ( ) )}
)}
) } const OrderbookRow = React.memo( ({ side, price, size, sizePercent, invert, hasOpenOrder, market }) => { const element = useRef(null) const setMangoStore = useMangoStore((s) => s.set) useEffect(() => { !element.current?.classList.contains('flash') && element.current?.classList.add('flash') const id = setTimeout( () => element.current?.classList.contains('flash') && element.current?.classList.remove('flash'), 250 ) return () => clearTimeout(id) }, [price, size]) const formattedSize = market?.minOrderSize && !isNaN(size) ? Number(size).toFixed(getDecimalCount(market.minOrderSize)) : size const formattedPrice = market?.tickSize && !isNaN(price) ? Number(price).toFixed(getDecimalCount(market.tickSize)) : price const handlePriceClick = () => { setMangoStore((state) => { state.tradeForm.price = price }) } const handleSizeClick = () => { setMangoStore((state) => { state.tradeForm.baseSize = size }) } if (!market) return null return (
{invert ? ( <>
{usdFormatter( formattedPrice, getDecimalCount(market.tickSize), false )}
{formattedSize}
) : ( <>
{formattedSize}
{usdFormatter( formattedPrice, getDecimalCount(market.tickSize), false )}
)}
) }, (prevProps, nextProps) => isEqual(prevProps, nextProps, ['price', 'size', 'sizePercent']) ) const MarkPriceComponent = React.memo<{ markPrice: number }>( ({ markPrice }) => { const previousMarkPrice: number = usePrevious(markPrice) return (
previousMarkPrice ? `text-th-green` : markPrice < previousMarkPrice ? `text-th-red` : `text-th-fgd-1` }`} > {markPrice > previousMarkPrice && ( )} {markPrice < previousMarkPrice && ( )} {markPrice || '----'}
) }, (prevProps, nextProps) => isEqual(prevProps, nextProps, ['markPrice']) )