mango-ui-v3/components/Orderbook.tsx

711 lines
24 KiB
TypeScript
Raw Normal View History

import React, { useRef, useEffect, useState } from 'react'
import styled from '@emotion/styled'
import Big from 'big.js'
import useInterval from '../hooks/useInterval'
import usePrevious from '../hooks/usePrevious'
2021-09-02 22:32:27 -07:00
import { isEqual, getDecimalCount, usdFormatter } from '../utils/'
2021-04-26 05:53:27 -07:00
import {
ArrowUpIcon,
ArrowDownIcon,
SwitchHorizontalIcon,
} from '@heroicons/react/solid'
import { CumulativeSizeIcon, StepSizeIcon } from './icons'
2021-04-02 11:26:21 -07:00
import useMarkPrice from '../hooks/useMarkPrice'
2021-04-05 13:48:24 -07:00
import { ElementTitle } from './styles'
2021-04-13 16:41:04 -07:00
import useMangoStore from '../stores/useMangoStore'
2021-04-26 05:53:27 -07:00
import Tooltip from './Tooltip'
2021-09-04 21:42:37 -07:00
import GroupSize from './GroupSize'
2021-09-02 22:32:27 -07:00
import { useOpenOrders } from '../hooks/useOpenOrders'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
2021-09-24 05:24:17 -07:00
import {
FlipCard,
FlipCardBack,
FlipCardFront,
FlipCardInner,
StyledFloatingElement,
} from './FlipCard'
const Line = styled.div<any>`
text-align: ${(props) => (props.invert ? 'left' : 'right')};
height: 100%;
2021-04-26 05:53:27 -07:00
filter: opacity(40%);
${(props) => props['data-width'] && `width: ${props['data-width']};`}
`
2021-04-07 08:44:22 -07:00
const groupBy = (ordersArray, market, grouping: number, isBids: boolean) => {
if (!ordersArray || !market || !grouping || grouping == market?.tickSize) {
return ordersArray || []
2021-09-04 20:36:10 -07:00
}
const groupFloors = {}
for (let i = 0; i < ordersArray.length; i++) {
if (typeof ordersArray[i] == 'undefined') {
2021-09-04 20:36:10 -07:00
break
}
2021-09-06 02:36:30 -07:00
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)
2021-09-04 20:36:10 -07:00
if (typeof groupFloors[floor] == 'undefined') {
groupFloors[floor] = ordersArray[i][1]
2021-09-04 20:36:10 -07:00
} else {
groupFloors[floor] = ordersArray[i][1] + groupFloors[floor]
2021-09-04 20:36:10 -07:00
}
}
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
}
2021-09-06 02:36:30 -07:00
return isBids ? b[0] - a[0] : a[0] - b[0]
})
return sortedGroups
2021-09-04 20:36:10 -07:00
}
const getCumulativeOrderbookSide = (
orders,
totalSize,
maxSize,
depth,
backwards = false
) => {
2021-09-04 21:42:37 -07:00
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
})
}
2021-04-26 05:53:27 -07:00
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)
2021-09-03 13:25:09 -07:00
const market = useMangoStore((s) => s.selectedMarket.current)
const markPrice = useMarkPrice()
2021-09-02 22:32:27 -07:00
const openOrders = useOpenOrders()
2021-09-02 22:45:58 -07:00
const openOrderPrices = openOrders?.length
2021-09-02 22:32:27 -07:00
? openOrders.map(({ order }) => order.price)
: []
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const currentOrderbookData = useRef(null)
const lastOrderbookData = useRef(null)
const previousDepth = usePrevious(depth)
const [orderbookData, setOrderbookData] = useState(null)
2021-04-26 05:53:27 -07:00
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)
2021-09-04 21:42:37 -07:00
useEffect(() => {
if (market && market.tickSize !== tickSize) {
setTickSize(market.tickSize)
2021-09-04 21:42:37 -07:00
setGrouping(market.tickSize)
}
2021-09-06 06:34:05 -07:00
}, [market])
useInterval(() => {
if (
!currentOrderbookData.current ||
JSON.stringify(currentOrderbookData.current) !==
JSON.stringify(lastOrderbookData.current) ||
previousDepth !== depth ||
previousGrouping !== grouping
) {
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
2021-09-06 06:44:32 -07:00
? getCumulativeOrderbookSide(bids, totalSize, maxSize, depth, false)
: getCumulativeOrderbookSide(bids, totalSize, maxSize, depth / 2, false)
2021-04-26 05:53:27 -07:00
const asksToDisplay = defaultLayout
2021-09-06 06:44:32 -07:00
? 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
2021-04-26 05:53:27 -07:00
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
}
2021-09-06 06:44:32 -07:00
2021-04-26 05:53:27 -07:00
setOrderbookData({
bids: bidsToDisplay,
2021-09-22 02:26:59 -07:00
asks: isMobile ? asksToDisplay.reverse() : asksToDisplay,
spread,
spreadPercentage,
2021-04-26 05:53:27 -07:00
})
2021-06-18 11:07:07 -07:00
} else {
setOrderbookData(null)
2021-04-26 05:53:27 -07:00
}
}
}, 250)
useEffect(() => {
lastOrderbookData.current = {
bids: orderbook?.bids,
asks: orderbook?.asks,
}
}, [orderbook])
2021-04-26 05:53:27 -07:00
const handleLayoutChange = () => {
setDefaultLayout(!defaultLayout)
setOrderbookData((prevState) => ({
...orderbookData,
asks: prevState.asks.reverse(),
}))
2021-04-26 05:53:27 -07:00
}
2021-09-04 21:42:37 -07:00
const onGroupSizeChange = (groupSize) => {
setGrouping(groupSize)
}
return !isMobile ? (
<FlipCard>
<FlipCardInner flip={defaultLayout}>
{defaultLayout ? (
<FlipCardFront>
<StyledFloatingElement className="h-full">
<div className="flex items-center justify-between pb-2.5">
<div className="flex relative">
<Tooltip
content={
displayCumulativeSize
? 'Display Step Size'
: 'Display Cumulative Size'
}
className="text-xs py-1"
>
<button
onClick={() => {
setDisplayCumulativeSize(!displayCumulativeSize)
}}
className="flex items-center justify-center rounded-full bg-th-bkg-3 w-8 h-8 hover:text-th-primary focus:outline-none"
>
{displayCumulativeSize ? (
<StepSizeIcon className="w-5 h-5" />
) : (
<CumulativeSizeIcon className="w-5 h-5" />
)}
</button>
</Tooltip>
2021-04-26 05:53:27 -07:00
</div>
<ElementTitle noMarignBottom>Orderbook</ElementTitle>
<div className="flex relative">
<Tooltip content={'Switch Layout'} className="text-xs py-1">
<button
onClick={handleLayoutChange}
className="flex items-center justify-center rounded-full bg-th-bkg-3 w-8 h-8 hover:text-th-primary focus:outline-none"
>
<SwitchHorizontalIcon className="w-5 h-5" />
</button>
</Tooltip>
2021-09-05 06:44:30 -07:00
</div>
</div>
<div className="flex justify-end items-center mb-4">
<MarkPriceComponent markPrice={markPrice} />
<GroupSize
tickSize={market?.tickSize}
onChange={onGroupSizeChange}
value={grouping}
className="relative flex flex-col w-1/3 items-end"
/>
</div>
<div
className={`text-th-fgd-4 flex justify-between mb-2 text-xs`}
>
<div className={`text-left`}>
{displayCumulativeSize ? 'Cumulative ' : ''}Size (
{marketConfig.baseSymbol})
</div>
<div className={`text-center`}>
Price ({groupConfig.quoteSymbol})
</div>
<div className={`text-right`}>
{displayCumulativeSize ? 'Cumulative ' : ''}Size (
{marketConfig.baseSymbol})
</div>
</div>
<div className="flex">
<div className="w-1/2">
{orderbookData?.bids.map(
({
price,
size,
cumulativeSize,
sizePercent,
maxSizePercent,
}) => (
<OrderbookRow
market={market}
hasOpenOrder={hasOpenOrderForPriceGroup(
openOrderPrices,
price,
grouping
)}
key={price + ''}
price={price}
size={displayCumulativeSize ? cumulativeSize : size}
side="buy"
sizePercent={
displayCumulativeSize ? maxSizePercent : sizePercent
}
grouping={grouping}
/>
)
)}
2021-04-26 05:53:27 -07:00
</div>
<div className="w-1/2">
{orderbookData?.asks.map(
({
price,
size,
cumulativeSize,
sizePercent,
maxSizePercent,
}) => (
<OrderbookRow
market={market}
hasOpenOrder={hasOpenOrderForPriceGroup(
openOrderPrices,
price,
grouping
)}
invert
key={price + ''}
price={price}
size={displayCumulativeSize ? cumulativeSize : size}
side="sell"
sizePercent={
displayCumulativeSize ? maxSizePercent : sizePercent
}
grouping={grouping}
/>
)
)}
2021-09-05 06:44:30 -07:00
</div>
</div>
<div className="flex justify-between bg-th-bkg-1 p-2 mt-4 rounded-md text-xs">
<div className="text-th-fgd-3">Spread</div>
<div className="text-th-fgd-1">
{orderbookData?.spread?.toFixed(2)}
</div>
<div className="text-th-fgd-1">
{orderbookData?.spreadPercentage?.toFixed(2)}%
</div>
</div>
</StyledFloatingElement>
</FlipCardFront>
) : (
<FlipCardBack>
<StyledFloatingElement className="h-full">
<div className="flex items-center justify-between pb-2.5">
<div className="flex relative">
<Tooltip
content={
displayCumulativeSize
? 'Display Step Size'
: 'Display Cumulative Size'
}
className="text-xs py-1"
>
<button
onClick={() => {
setDisplayCumulativeSize(!displayCumulativeSize)
}}
className="flex items-center justify-center rounded-full bg-th-bkg-3 w-8 h-8 hover:text-th-primary focus:outline-none"
>
{displayCumulativeSize ? (
<StepSizeIcon className="w-5 h-5" />
) : (
<CumulativeSizeIcon className="w-5 h-5" />
)}
</button>
</Tooltip>
</div>
<ElementTitle noMarignBottom>Orderbook</ElementTitle>
<div className="flex relative">
<Tooltip content={'Switch Layout'} className="text-xs py-1">
<button
onClick={handleLayoutChange}
className="flex items-center justify-center rounded-full bg-th-bkg-3 w-8 h-8 hover:text-th-primary focus:outline-none"
>
<SwitchHorizontalIcon className="w-5 h-5" />
</button>
</Tooltip>
</div>
</div>
2021-09-20 02:56:50 -07:00
<div className="flex flex-row items-center justify-end pb-2">
<MarkPriceComponent markPrice={markPrice} />
<GroupSize
tickSize={market?.tickSize}
onChange={onGroupSizeChange}
value={grouping}
2021-09-20 02:56:50 -07:00
className="relative flex flex-col w-1/3 items-end"
/>
</div>
<div className={`text-th-fgd-4 flex justify-between mb-2`}>
<div className={`text-left text-xs`}>
{displayCumulativeSize ? 'Cumulative ' : ''}Size (
{marketConfig.baseSymbol})
</div>
<div className={`text-right text-xs`}>
Price ({groupConfig.quoteSymbol})
</div>
</div>
{orderbookData?.asks.map(
({
price,
size,
cumulativeSize,
sizePercent,
maxSizePercent,
}) => (
<OrderbookRow
market={market}
hasOpenOrder={hasOpenOrderForPriceGroup(
openOrderPrices,
price,
grouping
)}
key={price + ''}
price={price}
size={displayCumulativeSize ? cumulativeSize : size}
side="sell"
sizePercent={
displayCumulativeSize ? maxSizePercent : sizePercent
}
grouping={grouping}
/>
)
)}
<div className="flex justify-between bg-th-bkg-1 p-2 my-2 rounded-md text-xs">
<div className="text-th-fgd-3">Spread</div>
<div className="text-th-fgd-1">
{orderbookData?.spread.toFixed(2)}
</div>
<div className="text-th-fgd-1">
{orderbookData?.spreadPercentage.toFixed(2)}%
</div>
</div>
{orderbookData?.bids.map(
({
price,
size,
cumulativeSize,
sizePercent,
maxSizePercent,
}) => (
<OrderbookRow
market={market}
hasOpenOrder={hasOpenOrderForPriceGroup(
openOrderPrices,
price,
grouping
)}
key={price + ''}
price={price}
size={displayCumulativeSize ? cumulativeSize : size}
side="buy"
sizePercent={
displayCumulativeSize ? maxSizePercent : sizePercent
}
grouping={grouping}
/>
)
)}
</StyledFloatingElement>
</FlipCardBack>
)}
</FlipCardInner>
</FlipCard>
) : (
<div>
<div className="flex items-center justify-between pb-2.5">
<div className="flex relative">
<Tooltip
content={
displayCumulativeSize
? 'Display Step Size'
: 'Display Cumulative Size'
}
className="text-xs py-1"
>
<button
onClick={() => {
setDisplayCumulativeSize(!displayCumulativeSize)
}}
className="flex items-center justify-center rounded-full bg-th-bkg-3 w-8 h-8 hover:text-th-primary focus:outline-none"
>
{displayCumulativeSize ? (
<StepSizeIcon className="w-5 h-5" />
) : (
<CumulativeSizeIcon className="w-5 h-5" />
)}
</button>
</Tooltip>
</div>
<GroupSize
tickSize={market?.tickSize}
onChange={onGroupSizeChange}
value={grouping}
className="relative flex flex-col w-1/3 items-end mb-1"
/>
</div>
<div className={`text-th-fgd-4 flex justify-between`}>
<div className={`text-left text-xs`}>
{displayCumulativeSize ? 'Cumulative ' : ''}Size
</div>
<div className={`text-right text-xs`}>Price</div>
</div>
{orderbookData?.asks.map(
({ price, size, cumulativeSize, sizePercent, maxSizePercent }) => (
<OrderbookRow
market={market}
hasOpenOrder={hasOpenOrderForPriceGroup(
openOrderPrices,
price,
grouping
)}
key={price + ''}
price={price}
size={displayCumulativeSize ? cumulativeSize : size}
side="sell"
sizePercent={displayCumulativeSize ? maxSizePercent : sizePercent}
grouping={grouping}
/>
)
)}
<div className="flex justify-between bg-th-bkg-1 p-2 my-2 rounded-md text-xs">
<div className="hidden sm:block text-th-fgd-3">Spread</div>
<div className="text-th-fgd-1">{orderbookData?.spread.toFixed(2)}</div>
<div className="text-th-fgd-1">
{orderbookData?.spreadPercentage.toFixed(2)}%
</div>
</div>
{orderbookData?.bids.map(
({ price, size, cumulativeSize, sizePercent, maxSizePercent }) => (
<OrderbookRow
market={market}
hasOpenOrder={hasOpenOrderForPriceGroup(
openOrderPrices,
price,
grouping
)}
key={price + ''}
price={price}
size={displayCumulativeSize ? cumulativeSize : size}
side="buy"
sizePercent={displayCumulativeSize ? maxSizePercent : sizePercent}
grouping={grouping}
/>
)
)}
</div>
2021-04-02 11:26:21 -07:00
)
}
2021-04-02 11:26:21 -07:00
const OrderbookRow = React.memo<any>(
({
side,
price,
size,
sizePercent,
invert,
hasOpenOrder,
market,
grouping,
}) => {
2021-04-02 11:26:21 -07:00
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
})
}
2021-09-02 22:32:27 -07:00
if (!market) return null
return (
2021-04-16 04:50:56 -07:00
<div className={`flex text-sm leading-7 justify-between`} ref={element}>
{invert ? (
<>
2021-04-26 05:53:27 -07:00
<div className={`text-left relative flex-1`}>
<Line
invert
data-width={sizePercent + '%'}
side={side}
2021-04-26 05:53:27 -07:00
className={`absolute inset-y-0 left-0 ${
side === 'buy' ? `bg-th-green` : `bg-th-red`
}`}
/>
2021-04-26 05:53:27 -07:00
<div
onClick={handlePriceClick}
className={`z-10 filter brightness-110 text-xs md:text-sm leading-5 md:leading-7 text-th-fgd-1 md:pl-2 ${
2021-09-02 22:32:27 -07:00
side === 'buy' ? `text-th-green` : `text-th-red`
}`}
2021-04-26 05:53:27 -07:00
>
{usdFormatter(formattedPrice, getDecimalCount(grouping), false)}
2021-04-26 05:53:27 -07:00
</div>
</div>
2021-09-02 22:32:27 -07:00
<div
2021-09-20 02:56:50 -07:00
className={`absolute right-4 z-10 ${
2021-09-02 22:32:27 -07:00
hasOpenOrder ? 'text-th-primary' : 'text-th-fgd-1'
}`}
onClick={handleSizeClick}
>
2021-04-15 14:36:55 -07:00
{formattedSize}
</div>
</>
) : (
<>
2021-04-15 14:36:55 -07:00
<div
2021-09-20 02:56:50 -07:00
className={`absolute md:left-4 z-10 text-xs md:text-sm leading-5 md:leading-7 ${
2021-09-02 22:32:27 -07:00
hasOpenOrder ? 'text-th-primary' : 'text-th-fgd-1'
}`}
onClick={handleSizeClick}
2021-04-15 14:36:55 -07:00
>
{formattedSize}
</div>
<div className={`text-right relative flex-1`}>
<Line
className={`absolute inset-y-0 right-0 ${
side === 'buy' ? `bg-th-green` : `bg-th-red`
}`}
data-width={sizePercent + '%'}
2021-04-12 06:32:01 -07:00
side={side}
/>
<div
className={`z-10 filter brightness-110 flex-1 text-xs md:text-sm leading-5 md:leading-7 md:pr-2 ${
2021-09-02 22:32:27 -07:00
side === 'buy' ? `text-th-green` : `text-th-red`
}`}
onClick={handlePriceClick}
>
{usdFormatter(formattedPrice, getDecimalCount(grouping), false)}
2021-04-05 12:03:20 -07:00
</div>
2021-04-02 11:26:21 -07:00
</div>
</>
)}
</div>
)
},
(prevProps, nextProps) =>
isEqual(prevProps, nextProps, ['price', 'size', 'sizePercent'])
)
2021-04-02 11:26:21 -07:00
const MarkPriceComponent = React.memo<{ markPrice: number }>(
({ markPrice }) => {
2021-04-02 11:26:21 -07:00
const previousMarkPrice: number = usePrevious(markPrice)
return (
2021-04-02 11:26:21 -07:00
<div
className={`flex justify-center items-center font-bold md:text-lg md:w-1/3 ${
2021-04-12 06:32:01 -07:00
markPrice > previousMarkPrice
? `text-th-green`
2021-04-12 06:32:01 -07:00
: markPrice < previousMarkPrice
? `text-th-red`
: `text-th-fgd-1`
}`}
2021-04-02 11:26:21 -07:00
>
{markPrice > previousMarkPrice && (
2021-04-26 05:53:27 -07:00
<ArrowUpIcon className={`h-5 w-5 mr-1 text-th-green`} />
2021-04-02 11:26:21 -07:00
)}
{markPrice < previousMarkPrice && (
2021-04-26 05:53:27 -07:00
<ArrowDownIcon className={`h-5 w-5 mr-1 text-th-red`} />
2021-04-02 11:26:21 -07:00
)}
{markPrice || '----'}
2021-04-02 11:26:21 -07:00
</div>
)
},
(prevProps, nextProps) => isEqual(prevProps, nextProps, ['markPrice'])
)