Merge pull request #193 from blockworks-foundation/depth-chart
orderbook depth chart
This commit is contained in:
commit
226d610bbf
|
@ -43,7 +43,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
const { asPath } = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (width < breakpoints.xl) {
|
||||
if (width < breakpoints['2xl']) {
|
||||
setIsCollapsed(true)
|
||||
}
|
||||
}, [width])
|
||||
|
@ -84,7 +84,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
|
||||
<div className="fixed z-20 hidden h-screen md:block">
|
||||
<button
|
||||
className="absolute right-0 top-1/2 z-20 hidden h-8 w-3 -translate-y-1/2 rounded-none rounded-l bg-th-bkg-3 hover:bg-th-bkg-4 focus:outline-none focus-visible:bg-th-bkg-4 xl:block"
|
||||
className="absolute right-0 top-1/2 z-20 hidden h-8 w-3 -translate-y-1/2 rounded-none rounded-l bg-th-bkg-3 hover:bg-th-bkg-4 focus:outline-none focus-visible:bg-th-bkg-4 2xl:block"
|
||||
onClick={handleToggleSidebar}
|
||||
>
|
||||
<ChevronRightIcon
|
||||
|
|
|
@ -45,7 +45,7 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
|
|||
<div
|
||||
className={`transition-all duration-${sideBarAnimationDuration} ${
|
||||
collapsed ? 'w-[64px]' : 'w-44 lg:w-48 xl:w-52'
|
||||
} border-r border-th-bkg-3 bg-th-bkg-1`}
|
||||
} box-border border-r border-th-bkg-3 bg-th-bkg-1`}
|
||||
>
|
||||
<div className="flex min-h-screen flex-col justify-between">
|
||||
<div className="my-2">
|
||||
|
|
|
@ -167,7 +167,7 @@ const AccountHeroStats = ({
|
|||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-6 border-b border-th-bkg-3">
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 lg:col-span-2 lg:border-t-0 xl:col-span-1">
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 lg:col-span-2 lg:border-t-0 2xl:col-span-1">
|
||||
<div id="account-step-four">
|
||||
<div className="flex justify-between">
|
||||
<Tooltip
|
||||
|
@ -214,9 +214,7 @@ const AccountHeroStats = ({
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<p className="tooltip-underline text-sm font-normal text-th-fgd-3 xl:text-base">
|
||||
{t('health')}
|
||||
</p>
|
||||
<p className="tooltip-underline">{t('health')}</p>
|
||||
</Tooltip>
|
||||
{mangoAccountAddress ? (
|
||||
<Tooltip
|
||||
|
@ -255,7 +253,7 @@ const AccountHeroStats = ({
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 flex border-t border-th-bkg-3 py-3 pl-6 md:col-span-3 md:border-l lg:col-span-2 lg:border-t-0 xl:col-span-1">
|
||||
<div className="col-span-6 flex border-t border-th-bkg-3 py-3 pl-6 md:col-span-3 md:border-l lg:col-span-2 lg:border-t-0 2xl:col-span-1">
|
||||
<div id="account-step-five">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-free-collateral')}
|
||||
|
@ -263,9 +261,7 @@ const AccountHeroStats = ({
|
|||
placement="top-start"
|
||||
delay={100}
|
||||
>
|
||||
<p className="tooltip-underline text-sm text-th-fgd-3 xl:text-base">
|
||||
{t('free-collateral')}
|
||||
</p>
|
||||
<p className="tooltip-underline">{t('free-collateral')}</p>
|
||||
</Tooltip>
|
||||
<p className="mt-1 mb-0.5 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
|
||||
<FormatNumericValue
|
||||
|
@ -305,7 +301,7 @@ const AccountHeroStats = ({
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 flex border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 lg:col-span-2 lg:border-l lg:border-t-0 xl:col-span-1">
|
||||
<div className="col-span-6 flex border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 lg:col-span-2 lg:border-l lg:border-t-0 2xl:col-span-1">
|
||||
<div
|
||||
id="account-step-seven"
|
||||
className="flex w-full flex-col items-start"
|
||||
|
@ -316,9 +312,7 @@ const AccountHeroStats = ({
|
|||
placement="top-start"
|
||||
delay={100}
|
||||
>
|
||||
<p className="tooltip-underline inline text-sm text-th-fgd-3 xl:text-base">
|
||||
{t('pnl')}
|
||||
</p>
|
||||
<p className="tooltip-underline">{t('pnl')}</p>
|
||||
</Tooltip>
|
||||
{mangoAccountAddress ? (
|
||||
<div className="flex items-center space-x-3">
|
||||
|
@ -364,12 +358,10 @@ const AccountHeroStats = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 md:border-l lg:col-span-2 lg:border-l-0 xl:col-span-1 xl:border-l xl:border-t-0">
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 md:border-l lg:col-span-2 lg:border-l-0 2xl:col-span-1 2xl:border-l 2xl:border-t-0">
|
||||
<div id="account-step-six">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-sm text-th-fgd-3 xl:text-base">
|
||||
{t('account:lifetime-volume')}
|
||||
</p>
|
||||
<p>{t('account:lifetime-volume')}</p>
|
||||
{mangoAccountAddress ? (
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
|
@ -413,7 +405,7 @@ const AccountHeroStats = ({
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 text-left md:col-span-3 lg:col-span-2 lg:border-l xl:col-span-1 xl:border-t-0">
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 text-left md:col-span-3 lg:col-span-2 lg:border-l 2xl:col-span-1 2xl:border-t-0">
|
||||
<div id="account-step-eight">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Tooltip
|
||||
|
@ -422,7 +414,7 @@ const AccountHeroStats = ({
|
|||
placement="top-start"
|
||||
delay={100}
|
||||
>
|
||||
<p className="tooltip-underline text-sm text-th-fgd-3 xl:text-base">
|
||||
<p className="tooltip-underline">
|
||||
{t('total-interest-earned')}
|
||||
</p>
|
||||
</Tooltip>
|
||||
|
@ -457,7 +449,7 @@ const AccountHeroStats = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 text-left md:col-span-3 md:border-l lg:col-span-2 xl:col-span-1 xl:border-t-0">
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 text-left md:col-span-3 md:border-l lg:col-span-2 2xl:col-span-1 2xl:border-t-0">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-total-funding')}
|
||||
|
@ -465,7 +457,7 @@ const AccountHeroStats = ({
|
|||
placement="top-start"
|
||||
delay={100}
|
||||
>
|
||||
<p className="tooltip-underline text-sm text-th-fgd-3 xl:text-base">
|
||||
<p className="tooltip-underline">
|
||||
{t('account:total-funding-earned')}
|
||||
</p>
|
||||
</Tooltip>
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import { ChangeEvent, useEffect, useRef, useState } from 'react'
|
||||
|
||||
const Slider = ({
|
||||
amount,
|
||||
max,
|
||||
min,
|
||||
onChange,
|
||||
step,
|
||||
}: {
|
||||
amount: number
|
||||
max?: string
|
||||
min?: string
|
||||
onChange: (x: string) => void
|
||||
step: number
|
||||
}) => {
|
||||
const [value, setValue] = useState(0)
|
||||
const inputEl = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (inputEl.current) {
|
||||
const target = inputEl.current
|
||||
const min = parseFloat(target.min)
|
||||
const max = parseFloat(target.max)
|
||||
|
||||
target.style.backgroundSize =
|
||||
max - min === 0
|
||||
? '0% 100%'
|
||||
: ((value - min) * 100) / (max - min) + '% 100%'
|
||||
}
|
||||
}, [value])
|
||||
|
||||
useEffect(() => {
|
||||
if (amount) {
|
||||
setValue(amount)
|
||||
} else {
|
||||
setValue(0)
|
||||
}
|
||||
}, [amount])
|
||||
|
||||
const handleSliderChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const target = e.target
|
||||
const min = parseFloat(target.min)
|
||||
const max = parseFloat(target.max)
|
||||
const val = parseFloat(target.value)
|
||||
|
||||
target.style.backgroundSize = ((val - min) * 100) / (max - min) + '% 100%'
|
||||
|
||||
onChange(e.target.value)
|
||||
setValue(parseFloat(e.target.value))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor="default-range" className="block text-sm"></label>
|
||||
<input
|
||||
ref={inputEl}
|
||||
id="default-range"
|
||||
type="range"
|
||||
min={min || '0'}
|
||||
max={max || '100'}
|
||||
step={step}
|
||||
className="w-full focus:outline-none"
|
||||
onChange={handleSliderChange}
|
||||
value={value}
|
||||
></input>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Slider
|
|
@ -6,6 +6,7 @@ interface SwitchProps {
|
|||
onChange: (x: boolean) => void
|
||||
children?: ReactNode
|
||||
disabled?: boolean
|
||||
small?: boolean
|
||||
}
|
||||
|
||||
const Switch: FunctionComponent<SwitchProps> = ({
|
||||
|
@ -14,6 +15,7 @@ const Switch: FunctionComponent<SwitchProps> = ({
|
|||
children,
|
||||
onChange,
|
||||
disabled,
|
||||
small,
|
||||
}) => {
|
||||
const handleClick = () => {
|
||||
onChange(!checked)
|
||||
|
@ -26,7 +28,9 @@ const Switch: FunctionComponent<SwitchProps> = ({
|
|||
type="button"
|
||||
className={`${
|
||||
checked ? 'bg-th-success' : 'bg-th-bkg-4'
|
||||
} relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full
|
||||
} relative inline-flex ${
|
||||
small ? 'h-4 w-8' : 'h-5 w-10'
|
||||
} flex-shrink-0 cursor-pointer rounded-full
|
||||
border-2 border-transparent transition-colors duration-200 ease-in-out focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-th-fgd-4 ${
|
||||
disabled ? 'opacity-60' : ''
|
||||
}`}
|
||||
|
@ -39,8 +43,14 @@ const Switch: FunctionComponent<SwitchProps> = ({
|
|||
<span
|
||||
aria-hidden="true"
|
||||
className={`${
|
||||
checked ? 'translate-x-5' : 'translate-x-0'
|
||||
} pointer-events-none inline-block h-4 w-4 rounded-full
|
||||
checked
|
||||
? small
|
||||
? 'translate-x-4'
|
||||
: 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} pointer-events-none inline-block ${
|
||||
small ? 'h-3 w-3' : 'h-4 w-4'
|
||||
} rounded-full
|
||||
bg-white shadow ring-0 transition duration-200 ease-in-out`}
|
||||
></span>
|
||||
</button>
|
||||
|
|
|
@ -562,6 +562,7 @@ const SwapForm = () => {
|
|||
className="text-th-fgd-3"
|
||||
checked={useMargin}
|
||||
onChange={handleSetMargin}
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
|
@ -0,0 +1,516 @@
|
|||
import Slider from '@components/forms/Slider'
|
||||
import useMarkPrice from 'hooks/useMarkPrice'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import {
|
||||
XAxis,
|
||||
YAxis,
|
||||
ResponsiveContainer,
|
||||
AreaChart,
|
||||
Area,
|
||||
ReferenceLine,
|
||||
Label,
|
||||
LabelProps,
|
||||
} from 'recharts'
|
||||
import { CategoricalChartFunc } from 'recharts/types/chart/generateCategoricalChart'
|
||||
import { COLORS } from 'styles/colors'
|
||||
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
|
||||
import { CartesianViewBox } from 'recharts/types/util/types'
|
||||
import { cumOrderbookSide } from 'types'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
|
||||
type LabelPosition =
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'center'
|
||||
| 'bottom'
|
||||
| 'insideLeft'
|
||||
| 'insideRight'
|
||||
| 'insideTop'
|
||||
| 'insideBottom'
|
||||
| 'insideTopLeft'
|
||||
| 'insideTopRight'
|
||||
| 'insideBottomLeft'
|
||||
| 'insideBottomRight'
|
||||
| 'top'
|
||||
|
||||
const Y_TICK_COUNT = 10
|
||||
|
||||
interface CustomLabel extends LabelProps {
|
||||
viewBox?: CartesianViewBox
|
||||
}
|
||||
|
||||
const MarkPriceLabel = ({ value, viewBox }: CustomLabel) => {
|
||||
if (typeof value === 'string' && viewBox?.x && viewBox?.y) {
|
||||
const { x, y } = viewBox
|
||||
const valueLength = value.length
|
||||
const valueWidth = valueLength * 6
|
||||
return (
|
||||
<g>
|
||||
<foreignObject x={x - valueWidth} y={y - 10} width="100%" height={20}>
|
||||
<div className="w-max rounded bg-th-bkg-3 p-1 font-mono text-[9px] leading-none">
|
||||
{value}
|
||||
</div>
|
||||
</foreignObject>
|
||||
</g>
|
||||
)
|
||||
} else return null
|
||||
}
|
||||
|
||||
type RawOrderbook = number[][]
|
||||
type DepthOrderbookSide = {
|
||||
price: number
|
||||
size: number
|
||||
cumulativeSize: number
|
||||
}
|
||||
|
||||
const DepthChart = () => {
|
||||
const { theme } = useTheme()
|
||||
const { serumOrPerpMarket } = useSelectedMarket()
|
||||
const [mouseData, setMouseData] = useState<cumOrderbookSide | null>(null)
|
||||
const markPrice = useMarkPrice()
|
||||
const orderbook = mangoStore((s) => s.selectedMarket.orderbook)
|
||||
const [priceRangePercent, setPriceRangePercentPercent] = useState('10')
|
||||
const { width } = useViewport()
|
||||
const increaseHeight = width ? width > breakpoints['3xl'] : false
|
||||
|
||||
const formatOrderbookData = (orderbook: RawOrderbook, markPrice: number) => {
|
||||
const maxPrice = markPrice * 4
|
||||
const minPrice = markPrice / 4
|
||||
const formattedBook = []
|
||||
|
||||
let cumulativeSize = 0
|
||||
|
||||
for (let i = 0; i < orderbook.length; i++) {
|
||||
const [price, size] = orderbook[i]
|
||||
|
||||
cumulativeSize += size
|
||||
|
||||
const object = {
|
||||
price: price,
|
||||
size: size,
|
||||
cumulativeSize: cumulativeSize,
|
||||
}
|
||||
|
||||
if (price >= minPrice && price <= maxPrice) {
|
||||
formattedBook.push(object)
|
||||
}
|
||||
}
|
||||
return formattedBook
|
||||
}
|
||||
|
||||
// format chart data for the bids and asks series
|
||||
const mergeCumulativeData = (
|
||||
bids: DepthOrderbookSide[],
|
||||
asks: DepthOrderbookSide[]
|
||||
) => {
|
||||
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 || !serumOrPerpMarket || !markPrice) return []
|
||||
const formattedBids = formatOrderbookData(orderbook.bids, markPrice)
|
||||
const formattedAsks = formatOrderbookData(orderbook.asks, markPrice)
|
||||
return mergeCumulativeData(formattedBids, formattedAsks)
|
||||
}, [markPrice, orderbook, serumOrPerpMarket])
|
||||
|
||||
// find the max value for the x-axis
|
||||
const findXDomainMax = (
|
||||
data: DepthOrderbookSide[],
|
||||
yMin: number,
|
||||
yMax: number
|
||||
) => {
|
||||
let closestItemForYMin = 0
|
||||
let minDifferenceForYMin = Infinity
|
||||
|
||||
let closestItemForYMax = 0
|
||||
let minDifferenceForYMax = Infinity
|
||||
|
||||
for (const item of data) {
|
||||
const differenceForYMin = Math.abs(item.price - yMin)
|
||||
const differenceForYMax = Math.abs(item.price - yMax)
|
||||
|
||||
if (differenceForYMin < minDifferenceForYMin) {
|
||||
minDifferenceForYMin = differenceForYMin
|
||||
closestItemForYMin = item.cumulativeSize
|
||||
}
|
||||
|
||||
if (differenceForYMax < minDifferenceForYMax) {
|
||||
minDifferenceForYMax = differenceForYMax
|
||||
closestItemForYMax = item.cumulativeSize
|
||||
}
|
||||
}
|
||||
|
||||
return Math.max(closestItemForYMin, closestItemForYMax)
|
||||
}
|
||||
|
||||
// calc axis domains
|
||||
const [xMax, yMin, yMax] = useMemo(() => {
|
||||
let xMax = 100
|
||||
let yMin = 0
|
||||
let yMax = 100
|
||||
|
||||
if (markPrice) {
|
||||
yMin = markPrice / (1 + parseFloat(priceRangePercent) / 100)
|
||||
yMax = markPrice * (1 + parseFloat(priceRangePercent) / 100)
|
||||
}
|
||||
if (chartData.length) {
|
||||
xMax = findXDomainMax(chartData, yMin, yMax)
|
||||
}
|
||||
|
||||
return [xMax, yMin, yMax]
|
||||
}, [chartData, markPrice, priceRangePercent])
|
||||
|
||||
// get nearest data on the opposing side to the mouse
|
||||
const opposingMouseReference = useMemo(() => {
|
||||
if (!markPrice || !mouseData) return null
|
||||
const mousePrice = mouseData.price
|
||||
const difference = Math.abs(mousePrice - markPrice) / markPrice
|
||||
if (mousePrice >= markPrice) {
|
||||
const price = markPrice / (1 + difference)
|
||||
let closestItemBelow = null
|
||||
let minDifference = Infinity
|
||||
for (const item of chartData) {
|
||||
const difference = Math.abs(item.price - price)
|
||||
|
||||
if (difference < minDifference) {
|
||||
minDifference = difference
|
||||
closestItemBelow = item
|
||||
}
|
||||
}
|
||||
return closestItemBelow
|
||||
} else {
|
||||
const price = markPrice * (1 + difference)
|
||||
let closestItemAbove = null
|
||||
let minDifference = Infinity
|
||||
for (const item of chartData) {
|
||||
const difference = Math.abs(item.price - price)
|
||||
|
||||
if (difference < minDifference) {
|
||||
minDifference = difference
|
||||
closestItemAbove = item
|
||||
}
|
||||
}
|
||||
return closestItemAbove
|
||||
}
|
||||
}, [markPrice, mouseData])
|
||||
|
||||
const priceFormatter = useCallback(
|
||||
(price: number) => {
|
||||
if (!serumOrPerpMarket) return price.toFixed()
|
||||
const tickDecimals = getDecimalCount(serumOrPerpMarket.tickSize)
|
||||
if (tickDecimals >= 7) {
|
||||
return price.toExponential(3)
|
||||
} else return price.toFixed(tickDecimals)
|
||||
},
|
||||
[serumOrPerpMarket]
|
||||
)
|
||||
|
||||
const xTickFormatter = useCallback(
|
||||
(size: number) => {
|
||||
if (!serumOrPerpMarket) return size.toFixed()
|
||||
const minOrderDecimals = getDecimalCount(serumOrPerpMarket.minOrderSize)
|
||||
return size.toFixed(minOrderDecimals)
|
||||
},
|
||||
[serumOrPerpMarket]
|
||||
)
|
||||
|
||||
const isWithinRangeOfTick = useCallback(
|
||||
(value: number, baseValue: number) => {
|
||||
const difference = Math.abs(value - baseValue)
|
||||
const range = (yMax - yMin) / Y_TICK_COUNT
|
||||
|
||||
return difference <= range
|
||||
},
|
||||
[yMin, yMax]
|
||||
)
|
||||
|
||||
const yTickFormatter = useCallback(
|
||||
(tick: number) => {
|
||||
if ((markPrice && isWithinRangeOfTick(markPrice, tick)) || mouseData) {
|
||||
return ''
|
||||
}
|
||||
return priceFormatter(tick)
|
||||
},
|
||||
[markPrice, mouseData]
|
||||
)
|
||||
|
||||
const getChartReferenceColor = (price: number | undefined) => {
|
||||
if (!price || !markPrice) return 'var(--fgd-2)'
|
||||
return price > markPrice ? 'var(--down)' : 'var(--up)'
|
||||
}
|
||||
|
||||
const getPercentFromMarkPrice = (price: number | undefined) => {
|
||||
if (!price || !markPrice) return
|
||||
const percentDif = ((price - markPrice) / markPrice) * 100
|
||||
return `${percentDif.toFixed(2)}%`
|
||||
}
|
||||
|
||||
const getSizeFromMouseData = useCallback(
|
||||
(size: number | undefined) => {
|
||||
if (!size || !serumOrPerpMarket) return
|
||||
return floorToDecimal(
|
||||
size,
|
||||
getDecimalCount(serumOrPerpMarket.tickSize)
|
||||
).toString()
|
||||
},
|
||||
[serumOrPerpMarket]
|
||||
)
|
||||
|
||||
const getSizeLabelPosition = useCallback(
|
||||
(size: number | undefined, price: number | undefined) => {
|
||||
if (!xMax || !size || !price || !markPrice) return `insideRight`
|
||||
const yPosition = price > markPrice ? 'Top' : 'Bottom'
|
||||
const midPoint = xMax / 2
|
||||
const xPosition = size > midPoint ? 'Left' : 'Right'
|
||||
return `inside${yPosition}${xPosition}` as LabelPosition
|
||||
},
|
||||
[xMax, markPrice]
|
||||
)
|
||||
|
||||
const getPercentLabelPosition = useCallback(
|
||||
(price: number | undefined) => {
|
||||
if (!markPrice || !price || !yMax || !yMin) return 'bottom'
|
||||
const upperMidPoint = (markPrice + yMax) / 2
|
||||
const lowerMidPoint = (markPrice + yMin) / 2
|
||||
return price > markPrice
|
||||
? price > upperMidPoint
|
||||
? 'bottom'
|
||||
: 'top'
|
||||
: price > lowerMidPoint
|
||||
? 'bottom'
|
||||
: 'top'
|
||||
},
|
||||
[markPrice, yMax, yMin]
|
||||
)
|
||||
|
||||
const handleMouseMove: CategoricalChartFunc = (coords) => {
|
||||
if (coords?.activePayload) {
|
||||
setMouseData(coords.activePayload[0].payload)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setMouseData(null)
|
||||
}
|
||||
|
||||
return chartData.length ? (
|
||||
<>
|
||||
<div className="flex h-10 items-center border-b border-th-bkg-3 py-1 px-2">
|
||||
<div className="flex w-full items-center">
|
||||
<span className="w-16 font-mono text-xs text-th-fgd-3">
|
||||
{priceRangePercent}%
|
||||
</span>
|
||||
<Slider
|
||||
amount={parseFloat(priceRangePercent)}
|
||||
max="100"
|
||||
min="0.5"
|
||||
onChange={(p) => setPriceRangePercentPercent(p)}
|
||||
step={0.5}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={increaseHeight ? 'h-[570px]' : 'h-[482px]'}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
layout="vertical"
|
||||
margin={{
|
||||
top: 8,
|
||||
left: -8,
|
||||
}}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<XAxis
|
||||
axisLine={false}
|
||||
reversed={true}
|
||||
domain={[() => 0, () => xMax]}
|
||||
type="number"
|
||||
tick={false}
|
||||
tickLine={false}
|
||||
tickFormatter={(tick) => xTickFormatter(tick)}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey="price"
|
||||
reversed={true}
|
||||
domain={[() => yMin, () => yMax]}
|
||||
axisLine={false}
|
||||
tick={{
|
||||
fill: 'var(--fgd-4)',
|
||||
fontSize: 8,
|
||||
}}
|
||||
tickCount={Y_TICK_COUNT}
|
||||
tickLine={false}
|
||||
tickFormatter={(tick) => yTickFormatter(tick)}
|
||||
/>
|
||||
<Area
|
||||
type="stepBefore"
|
||||
dataKey="bids"
|
||||
stroke={COLORS.UP[theme]}
|
||||
fill="url(#bidsGradient)"
|
||||
isAnimationActive={false}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<Area
|
||||
type="stepBefore"
|
||||
dataKey="asks"
|
||||
stroke={COLORS.DOWN[theme]}
|
||||
fill="url(#asksGradient)"
|
||||
isAnimationActive={false}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<ReferenceLine
|
||||
y={mouseData?.price}
|
||||
stroke={getChartReferenceColor(mouseData?.price)}
|
||||
strokeDasharray="3, 3"
|
||||
>
|
||||
<Label
|
||||
value={mouseData ? priceFormatter(mouseData.price) : ''}
|
||||
fontSize={9}
|
||||
fill={getChartReferenceColor(mouseData?.price)}
|
||||
position="left"
|
||||
offset={5}
|
||||
/>
|
||||
<Label
|
||||
value={getPercentFromMarkPrice(mouseData?.price)}
|
||||
fontSize={9}
|
||||
fill={getChartReferenceColor(opposingMouseReference?.price)}
|
||||
position={getPercentLabelPosition(mouseData?.price)}
|
||||
offset={6}
|
||||
/>
|
||||
</ReferenceLine>
|
||||
<ReferenceLine
|
||||
y={opposingMouseReference?.price}
|
||||
stroke={getChartReferenceColor(opposingMouseReference?.price)}
|
||||
strokeDasharray="3, 3"
|
||||
>
|
||||
<Label
|
||||
value={
|
||||
opposingMouseReference
|
||||
? priceFormatter(opposingMouseReference.price)
|
||||
: ''
|
||||
}
|
||||
fontSize={9}
|
||||
fill={getChartReferenceColor(opposingMouseReference?.price)}
|
||||
position="left"
|
||||
offset={5}
|
||||
/>
|
||||
<Label
|
||||
value={getPercentFromMarkPrice(opposingMouseReference?.price)}
|
||||
fontSize={9}
|
||||
fill={getChartReferenceColor(mouseData?.price)}
|
||||
position={getPercentLabelPosition(
|
||||
opposingMouseReference?.price
|
||||
)}
|
||||
offset={6}
|
||||
/>
|
||||
</ReferenceLine>
|
||||
<ReferenceLine
|
||||
stroke={getChartReferenceColor(mouseData?.price)}
|
||||
strokeDasharray="3, 3"
|
||||
segment={
|
||||
mouseData && mouseData?.price >= markPrice
|
||||
? [
|
||||
{ x: mouseData?.cumulativeSize, y: markPrice },
|
||||
{ x: mouseData?.cumulativeSize, y: yMax },
|
||||
]
|
||||
: [
|
||||
{ x: mouseData?.cumulativeSize, y: yMin },
|
||||
{ x: mouseData?.cumulativeSize, y: markPrice },
|
||||
]
|
||||
}
|
||||
>
|
||||
<Label
|
||||
value={getSizeFromMouseData(mouseData?.cumulativeSize)}
|
||||
fontSize={9}
|
||||
fill={getChartReferenceColor(mouseData?.price)}
|
||||
position={getSizeLabelPosition(
|
||||
mouseData?.cumulativeSize,
|
||||
mouseData?.price
|
||||
)}
|
||||
offset={6}
|
||||
/>
|
||||
</ReferenceLine>
|
||||
<ReferenceLine
|
||||
stroke={getChartReferenceColor(opposingMouseReference?.price)}
|
||||
strokeDasharray="3, 3"
|
||||
segment={
|
||||
opposingMouseReference &&
|
||||
opposingMouseReference?.price >= markPrice
|
||||
? [
|
||||
{
|
||||
x: opposingMouseReference?.cumulativeSize,
|
||||
y: markPrice,
|
||||
},
|
||||
{ x: opposingMouseReference?.cumulativeSize, y: yMax },
|
||||
]
|
||||
: [
|
||||
{ x: opposingMouseReference?.cumulativeSize, y: yMin },
|
||||
{
|
||||
x: opposingMouseReference?.cumulativeSize,
|
||||
y: markPrice,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Label
|
||||
value={getSizeFromMouseData(
|
||||
opposingMouseReference?.cumulativeSize
|
||||
)}
|
||||
fontSize={9}
|
||||
fill={getChartReferenceColor(opposingMouseReference?.price)}
|
||||
position={getSizeLabelPosition(
|
||||
opposingMouseReference?.cumulativeSize,
|
||||
opposingMouseReference?.price
|
||||
)}
|
||||
offset={6}
|
||||
/>
|
||||
</ReferenceLine>
|
||||
{markPrice ? (
|
||||
<ReferenceLine y={markPrice} stroke="var(--bkg-4)">
|
||||
<Label
|
||||
value={priceFormatter(markPrice)}
|
||||
content={<MarkPriceLabel />}
|
||||
/>
|
||||
</ReferenceLine>
|
||||
) : null}
|
||||
<defs>
|
||||
<linearGradient id="bidsGradient" x1="0" y1="0" x2="1" 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="0" y1="0" x2="1" 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>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default DepthChart
|
|
@ -22,7 +22,7 @@ import { SOUND_SETTINGS_KEY } from 'utils/constants'
|
|||
import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
|
||||
import { Howl } from 'howler'
|
||||
import { isMangoError } from 'types'
|
||||
import { decodeBook, decodeBookL2 } from './Orderbook'
|
||||
import { decodeBook, decodeBookL2 } from 'utils/orderbook'
|
||||
import InlineNotification from '@components/shared/InlineNotification'
|
||||
|
||||
interface MarketCloseModalProps {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { TABS } from './OrderbookAndTrades'
|
|||
import RecentTrades from './RecentTrades'
|
||||
import TradingChartContainer from './TradingChartContainer'
|
||||
import FavoriteMarketsBar from './FavoriteMarketsBar'
|
||||
import DepthChart from './DepthChart'
|
||||
|
||||
const MobileTradeAdvancedPage = () => {
|
||||
const [activeTab, setActiveTab] = useState('trade:book')
|
||||
|
@ -32,17 +33,25 @@ const MobileTradeAdvancedPage = () => {
|
|||
<AdvancedTradeForm />
|
||||
</div>
|
||||
<div className="col-span-1 border-l border-th-bkg-3">
|
||||
<div className="border-b border-th-bkg-3">
|
||||
<div className="hide-scroll overflow-x-auto border-b border-th-bkg-3">
|
||||
<TabButtons
|
||||
activeValue={activeTab}
|
||||
onChange={(tab: string) => setActiveTab(tab)}
|
||||
values={TABS}
|
||||
fillWidth
|
||||
showBorders
|
||||
/>
|
||||
</div>
|
||||
<div className={activeTab === 'trade:book' ? 'visible' : 'hidden'}>
|
||||
<Orderbook />
|
||||
</div>
|
||||
<div
|
||||
className={`h-full ${
|
||||
activeTab === 'trade:depth' ? 'visible' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<DepthChart />
|
||||
</div>
|
||||
<div className={activeTab === 'trade:trades' ? 'visible' : 'hidden'}>
|
||||
<RecentTrades />
|
||||
</div>
|
||||
|
|
|
@ -1,36 +1,39 @@
|
|||
import { AccountInfo, PublicKey } from '@solana/web3.js'
|
||||
import Big from 'big.js'
|
||||
import { PublicKey } from '@solana/web3.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 { Market } from '@project-serum/serum'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
import {
|
||||
floorToDecimal,
|
||||
formatNumericValue,
|
||||
getDecimalCount,
|
||||
} from 'utils/numbers'
|
||||
import { ANIMATION_SETTINGS_KEY } from 'utils/constants'
|
||||
import {
|
||||
ANIMATION_SETTINGS_KEY,
|
||||
DEPTH_CHART_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'
|
||||
import { breakpoints } from '../../utils/theme'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import {
|
||||
BookSide,
|
||||
BookSideType,
|
||||
MangoClient,
|
||||
PerpMarket,
|
||||
Serum3Market,
|
||||
} from '@blockworks-foundation/mango-v4'
|
||||
import { BookSide, Serum3Market } from '@blockworks-foundation/mango-v4'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
|
||||
import { ArrowPathIcon } from '@heroicons/react/20/solid'
|
||||
import { sleep } from 'utils'
|
||||
import { OrderbookFeed } from '@blockworks-foundation/mango-feeds'
|
||||
import Switch from '@components/forms/Switch'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
import {
|
||||
decodeBook,
|
||||
decodeBookL2,
|
||||
getCumulativeOrderbookSide,
|
||||
getMarket,
|
||||
groupBy,
|
||||
updatePerpMarketOnGroup,
|
||||
} from 'utils/orderbook'
|
||||
import { OrderbookData, OrderbookL2 } from 'types'
|
||||
import { isEqual } from 'lodash'
|
||||
|
||||
const sizeCompacter = Intl.NumberFormat('en', {
|
||||
maximumFractionDigits: 6,
|
||||
|
@ -39,202 +42,31 @@ const sizeCompacter = Intl.NumberFormat('en', {
|
|||
|
||||
const SHOW_EXPONENTIAL_THRESHOLD = 0.00001
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
export const decodeBookL2 = (book: SpotOrderBook | BookSide): number[][] => {
|
||||
const depth = 300
|
||||
if (book instanceof SpotOrderBook) {
|
||||
return book.getL2(depth).map(([price, size]) => [price, size])
|
||||
} else if (book instanceof BookSide) {
|
||||
return book.getL2Ui(depth)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function decodeBook(
|
||||
client: MangoClient,
|
||||
market: Market | PerpMarket,
|
||||
accInfo: AccountInfo<Buffer>,
|
||||
side: 'bids' | 'asks'
|
||||
): 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
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
book.perpMarket.perpMarketIndex
|
||||
)
|
||||
if (perpMarket) {
|
||||
perpMarket[`_${side}`] = book
|
||||
// mangoStore.getState().actions.fetchOpenOrders()
|
||||
}
|
||||
}
|
||||
|
||||
type OrderbookData = {
|
||||
bids: cumOrderbookSide[]
|
||||
asks: cumOrderbookSide[]
|
||||
spread: number
|
||||
spreadPercentage: number
|
||||
}
|
||||
|
||||
const Orderbook = () => {
|
||||
const { t } = useTranslation(['common', 'trade'])
|
||||
const {
|
||||
serumOrPerpMarket: market,
|
||||
baseSymbol,
|
||||
quoteSymbol,
|
||||
} = useSelectedMarket()
|
||||
const { serumOrPerpMarket: market } = useSelectedMarket()
|
||||
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)
|
||||
const [grouping, setGrouping] = useState(0.01)
|
||||
const [useOrderbookFeed, setUseOrderbookFeed] = useState(false)
|
||||
// const [useOrderbookFeed, setUseOrderbookFeed] = useState(
|
||||
// localStorage.getItem(USE_ORDERBOOK_FEED_KEY) !== null
|
||||
// ? localStorage.getItem(USE_ORDERBOOK_FEED_KEY) === 'true'
|
||||
// : true
|
||||
// )
|
||||
|
||||
const currentOrderbookData = useRef<OrderbookL2>()
|
||||
const orderbookElRef = useRef<HTMLDivElement>(null)
|
||||
const [showDepthChart, setShowDepthChart] = useLocalStorageState<boolean>(
|
||||
DEPTH_CHART_KEY,
|
||||
false
|
||||
)
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.md : false
|
||||
const isMobile = width ? width < breakpoints.lg : false
|
||||
const [orderbookData, setOrderbookData] = useState<OrderbookData | null>(null)
|
||||
const currentOrderbookData = useRef<OrderbookL2>()
|
||||
|
||||
const depth = useMemo(() => {
|
||||
return isMobile ? 9 : 40
|
||||
}, [isMobile])
|
||||
return width > breakpoints['3xl'] ? 12 : 10
|
||||
}, [width])
|
||||
|
||||
const depthArray: number[] = useMemo(() => {
|
||||
return Array(depth).fill(0)
|
||||
|
@ -249,19 +81,19 @@ const Orderbook = () => {
|
|||
}
|
||||
}, [market, tickSize])
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
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])
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
|
@ -357,32 +189,15 @@ const Orderbook = () => {
|
|||
spread,
|
||||
spreadPercentage,
|
||||
})
|
||||
if (!isScrolled) {
|
||||
verticallyCenterOrderbook()
|
||||
}
|
||||
} else {
|
||||
setOrderbookData(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
[grouping, market, isScrolled, verticallyCenterOrderbook]
|
||||
[depth, grouping, market]
|
||||
)
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
const set = mangoStore.getState().set
|
||||
|
@ -609,82 +424,25 @@ const Orderbook = () => {
|
|||
})
|
||||
}, [bidAccountAddress])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', verticallyCenterOrderbook)
|
||||
}, [verticallyCenterOrderbook])
|
||||
|
||||
const resetOrderbook = useCallback(async () => {
|
||||
setShowBids(true)
|
||||
setShowAsks(true)
|
||||
await sleep(300)
|
||||
verticallyCenterOrderbook()
|
||||
}, [verticallyCenterOrderbook])
|
||||
|
||||
const onGroupSizeChange = useCallback((groupSize: number) => {
|
||||
setGrouping(groupSize)
|
||||
}, [])
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
setIsScrolled(true)
|
||||
}, [])
|
||||
|
||||
const toggleSides = (side: string) => {
|
||||
if (side === 'bids') {
|
||||
setShowBids(true)
|
||||
setShowAsks(false)
|
||||
} else {
|
||||
setShowBids(false)
|
||||
setShowAsks(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between border-b border-th-bkg-3 px-4 py-2">
|
||||
<div
|
||||
id="trade-step-three"
|
||||
className="hidden items-center space-x-1.5 md:flex"
|
||||
>
|
||||
<Tooltip
|
||||
className={`${!showAsks ? 'hidden' : ''}`}
|
||||
content={t('trade:show-bids')}
|
||||
placement="bottom"
|
||||
<div>
|
||||
<div className="flex h-10 items-center justify-between border-b border-th-bkg-3 px-4">
|
||||
{!isMobile ? (
|
||||
<Switch
|
||||
checked={showDepthChart}
|
||||
onChange={() => setShowDepthChart(!showDepthChart)}
|
||||
small
|
||||
>
|
||||
<button
|
||||
className={`rounded ${
|
||||
showAsks ? 'bg-th-bkg-3' : 'bg-th-bkg-2'
|
||||
} 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')}
|
||||
>
|
||||
<OrderbookIcon className="h-4 w-4" side="buy" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
className={`${!showBids ? 'hidden' : ''}`}
|
||||
content={t('trade:show-asks')}
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
className={`rounded ${
|
||||
showBids ? 'bg-th-bkg-3' : 'bg-th-bkg-2'
|
||||
} 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')}
|
||||
>
|
||||
<OrderbookIcon className="h-4 w-4" side="sell" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content={'Reset and center orderbook'} placement="bottom">
|
||||
<button
|
||||
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"
|
||||
onClick={resetOrderbook}
|
||||
>
|
||||
<ArrowPathIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<span className="text-xxs">{t('trade:depth')}</span>
|
||||
</Switch>
|
||||
) : null}
|
||||
{market ? (
|
||||
<>
|
||||
<p className="text-xs md:hidden">{t('trade:grouping')}:</p>
|
||||
<p className="text-xs lg:hidden">{t('trade:grouping')}:</p>
|
||||
<div id="trade-step-four">
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
|
@ -702,95 +460,80 @@ const Orderbook = () => {
|
|||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 px-4 pt-2 pb-1 text-xxs text-th-fgd-4">
|
||||
<div className="col-span-1 text-right">
|
||||
{t('trade:size')} ({baseSymbol})
|
||||
</div>
|
||||
<div className="col-span-1 text-right">
|
||||
{t('price')} ({quoteSymbol})
|
||||
</div>
|
||||
<div className="grid grid-cols-2 px-2 py-0.5 text-xxs text-th-fgd-4">
|
||||
<div className="col-span-1">{t('price')}</div>
|
||||
<div className="col-span-1 text-right">{t('trade:size')}</div>
|
||||
</div>
|
||||
<div
|
||||
className="hide-scroll relative h-full overflow-y-scroll"
|
||||
ref={orderbookElRef}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{showAsks
|
||||
? depthArray.map((_x, idx) => {
|
||||
let index = idx
|
||||
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)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="h-[24px]" key={idx}>
|
||||
{!!orderbookData?.asks[index] && market ? (
|
||||
<MemoizedOrderbookRow
|
||||
minOrderSize={market.minOrderSize}
|
||||
tickSize={market.tickSize}
|
||||
hasOpenOrder={orderbookData?.asks[index].isUsersOrder}
|
||||
key={orderbookData?.asks[index].price}
|
||||
price={orderbookData?.asks[index].price}
|
||||
size={orderbookData?.asks[index].size}
|
||||
side="sell"
|
||||
sizePercent={orderbookData?.asks[index].sizePercent}
|
||||
cumulativeSizePercent={
|
||||
orderbookData?.asks[index].cumulativeSizePercent
|
||||
}
|
||||
grouping={grouping}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
{showBids && showAsks ? (
|
||||
<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"
|
||||
>
|
||||
<div className="col-span-1 flex justify-between">
|
||||
<div className="text-xxs">{t('trade:spread')}</div>
|
||||
<div className="font-mono">
|
||||
{orderbookData?.spreadPercentage.toFixed(2)}%
|
||||
</div>
|
||||
<div className="relative h-full">
|
||||
{depthArray.map((_x, idx) => {
|
||||
let index = idx
|
||||
if (orderbookData?.asks) {
|
||||
const lengthDiff = depthArray.length - orderbookData.asks.length
|
||||
if (lengthDiff > 0) {
|
||||
index = index < lengthDiff ? -1 : Math.abs(lengthDiff - index)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="h-[20px]" key={idx}>
|
||||
{!!orderbookData?.asks[index] && market ? (
|
||||
<MemoizedOrderbookRow
|
||||
minOrderSize={market.minOrderSize}
|
||||
tickSize={market.tickSize}
|
||||
hasOpenOrder={orderbookData?.asks[index].isUsersOrder}
|
||||
key={orderbookData?.asks[index].price}
|
||||
price={orderbookData?.asks[index].price}
|
||||
size={orderbookData?.asks[index].size}
|
||||
side="sell"
|
||||
sizePercent={orderbookData?.asks[index].sizePercent}
|
||||
cumulativeSizePercent={
|
||||
orderbookData?.asks[index].cumulativeSizePercent
|
||||
}
|
||||
grouping={grouping}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<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}
|
||||
)
|
||||
})}
|
||||
<div
|
||||
className="my-1 grid grid-cols-2 border-y border-th-bkg-3 py-1 px-4 text-xs text-th-fgd-4"
|
||||
id="trade-step-nine"
|
||||
>
|
||||
<div className="col-span-1 flex justify-between">
|
||||
<div className="text-xxs">{t('trade:spread')}</div>
|
||||
<div className="font-mono">
|
||||
{orderbookData?.spreadPercentage.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{showBids
|
||||
? depthArray.map((_x, index) => (
|
||||
<div className="h-[24px]" key={index}>
|
||||
{!!orderbookData?.bids[index] && market ? (
|
||||
<MemoizedOrderbookRow
|
||||
minOrderSize={market.minOrderSize}
|
||||
tickSize={market.tickSize}
|
||||
hasOpenOrder={orderbookData?.bids[index].isUsersOrder}
|
||||
price={orderbookData?.bids[index].price}
|
||||
size={orderbookData?.bids[index].size}
|
||||
side="buy"
|
||||
sizePercent={orderbookData?.bids[index].sizePercent}
|
||||
cumulativeSizePercent={
|
||||
orderbookData?.bids[index].cumulativeSizePercent
|
||||
}
|
||||
grouping={grouping}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
{depthArray.map((_x, index) => (
|
||||
<div className="h-[20px]" key={index}>
|
||||
{!!orderbookData?.bids[index] && market ? (
|
||||
<MemoizedOrderbookRow
|
||||
minOrderSize={market.minOrderSize}
|
||||
tickSize={market.tickSize}
|
||||
hasOpenOrder={orderbookData?.bids[index].isUsersOrder}
|
||||
price={orderbookData?.bids[index].price}
|
||||
size={orderbookData?.bids[index].size}
|
||||
side="buy"
|
||||
sizePercent={orderbookData?.bids[index].sizePercent}
|
||||
cumulativeSizePercent={
|
||||
orderbookData?.bids[index].cumulativeSizePercent
|
||||
}
|
||||
grouping={grouping}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -893,13 +636,23 @@ const OrderbookRow = ({
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`relative flex h-[24px] cursor-pointer justify-between border-b border-b-th-bkg-1 text-sm`}
|
||||
className={`relative flex h-[20px] cursor-pointer justify-between border-b border-b-th-bkg-1 text-sm`}
|
||||
ref={element}
|
||||
>
|
||||
<>
|
||||
<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"
|
||||
className={`z-10 flex h-full w-full items-center pl-2 hover:underline`}
|
||||
onClick={handlePriceClick}
|
||||
>
|
||||
<span className="w-full font-mono text-xs">
|
||||
{price < SHOW_EXPONENTIAL_THRESHOLD
|
||||
? formattedPrice.toExponential()
|
||||
: formattedPrice.toFixed(groupingDecimalCount)}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex h-full w-full items-center justify-start pr-2 hover:underline"
|
||||
onClick={handleSizeClick}
|
||||
>
|
||||
<div
|
||||
|
@ -913,26 +666,16 @@ const OrderbookRow = ({
|
|||
: formattedSize.toFixed(minOrderSizeDecimals)}
|
||||
</div>
|
||||
</div>
|
||||
<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)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Line
|
||||
className={`absolute left-0 opacity-40 brightness-125 ${
|
||||
className={`absolute left-0 opacity-30 brightness-125 ${
|
||||
side === 'buy' ? `bg-th-up-muted` : `bg-th-down-muted`
|
||||
}`}
|
||||
data-width={Math.max(sizePercent, 0.5) + '%'}
|
||||
/>
|
||||
<Line
|
||||
className={`absolute left-0 opacity-70 ${
|
||||
className={`absolute left-0 opacity-40 ${
|
||||
side === 'buy' ? `bg-th-up` : `bg-th-down`
|
||||
}`}
|
||||
data-width={
|
||||
|
|
|
@ -1,31 +1,67 @@
|
|||
import TabButtons from '@components/shared/TabButtons'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Orderbook from './Orderbook'
|
||||
import RecentTrades from './RecentTrades'
|
||||
import DepthChart from './DepthChart'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
import { DEPTH_CHART_KEY } from 'utils/constants'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
|
||||
export const TABS: [string, number][] = [
|
||||
['trade:book', 0],
|
||||
['trade:depth', 0],
|
||||
['trade:trades', 0],
|
||||
]
|
||||
|
||||
const OrderbookAndTrades = () => {
|
||||
const [activeTab, setActiveTab] = useState('trade:book')
|
||||
const [showDepthChart] = useLocalStorageState<boolean>(DEPTH_CHART_KEY, false)
|
||||
const { width } = useViewport()
|
||||
const hideDepthTab = width ? width > breakpoints.lg : false
|
||||
|
||||
const tabsToShow = useMemo(() => {
|
||||
if (hideDepthTab) {
|
||||
return TABS.filter((t) => !t[0].includes('depth'))
|
||||
}
|
||||
return TABS
|
||||
}, [hideDepthTab])
|
||||
|
||||
useEffect(() => {
|
||||
if (hideDepthTab && activeTab === 'trade:depth') {
|
||||
setActiveTab('trade:book')
|
||||
}
|
||||
}, [activeTab, hideDepthTab])
|
||||
|
||||
return (
|
||||
<div className="hide-scroll h-full">
|
||||
<div className="border-b border-th-bkg-3">
|
||||
<div className="hide-scroll overflow-x-auto border-b border-th-bkg-3">
|
||||
<TabButtons
|
||||
activeValue={activeTab}
|
||||
onChange={(tab: string) => setActiveTab(tab)}
|
||||
values={TABS}
|
||||
fillWidth
|
||||
values={tabsToShow}
|
||||
fillWidth={!showDepthChart || !hideDepthTab}
|
||||
showBorders
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`flex ${activeTab === 'trade:book' ? 'visible' : 'hidden'}`}
|
||||
>
|
||||
{showDepthChart ? (
|
||||
<div className="hidden w-1/2 border-r border-th-bkg-3 lg:block">
|
||||
<DepthChart />
|
||||
</div>
|
||||
) : null}
|
||||
<div className={showDepthChart ? 'w-full lg:w-1/2' : 'w-full'}>
|
||||
<Orderbook />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`h-full ${
|
||||
activeTab === 'trade:book' ? 'visible' : 'hidden'
|
||||
activeTab === 'trade:depth' ? 'visible' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<Orderbook />
|
||||
<DepthChart />
|
||||
</div>
|
||||
<div
|
||||
className={`h-full ${
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { useMemo } from 'react'
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import ReactGridLayout, { Responsive, WidthProvider } from 'react-grid-layout'
|
||||
import ReactGridLayout, {
|
||||
Layout,
|
||||
Layouts,
|
||||
Responsive,
|
||||
WidthProvider,
|
||||
} from 'react-grid-layout'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
// import { IS_ONBOARDED_KEY } from 'utils/constants'
|
||||
// import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
|
@ -15,7 +20,11 @@ import OrderbookAndTrades from './OrderbookAndTrades'
|
|||
// import TradeOnboardingTour from '@components/tours/TradeOnboardingTour'
|
||||
import FavoriteMarketsBar from './FavoriteMarketsBar'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
import { TRADE_LAYOUT_KEY } from 'utils/constants'
|
||||
import {
|
||||
DEPTH_CHART_KEY,
|
||||
SIDEBAR_COLLAPSE_KEY,
|
||||
TRADE_LAYOUT_KEY,
|
||||
} from 'utils/constants'
|
||||
import TradeHotKeys from './TradeHotKeys'
|
||||
|
||||
export type TradeLayout =
|
||||
|
@ -28,18 +37,6 @@ const TradingChartContainer = dynamic(() => import('./TradingChartContainer'), {
|
|||
ssr: false,
|
||||
})
|
||||
|
||||
const ResponsiveGridLayout = WidthProvider(Responsive)
|
||||
|
||||
const sidebarWidth = 63
|
||||
const totalCols = 24
|
||||
const gridBreakpoints = {
|
||||
md: breakpoints.md - sidebarWidth,
|
||||
lg: breakpoints.lg - sidebarWidth,
|
||||
xl: breakpoints.xl - sidebarWidth,
|
||||
xxl: breakpoints['2xl'] - sidebarWidth,
|
||||
xxxl: breakpoints['3xl'] - sidebarWidth,
|
||||
}
|
||||
|
||||
const getHeight = (
|
||||
pageHeight: number,
|
||||
minHeight: number,
|
||||
|
@ -48,6 +45,8 @@ const getHeight = (
|
|||
return Math.max(minHeight, pageHeight - remainingRowHeight)
|
||||
}
|
||||
|
||||
const ResponsiveGridLayout = WidthProvider(Responsive)
|
||||
|
||||
const TradeAdvancedPage = () => {
|
||||
const { height, width } = useViewport()
|
||||
const { uiLocked } = mangoStore((s) => s.settings)
|
||||
|
@ -59,150 +58,222 @@ const TradeAdvancedPage = () => {
|
|||
TRADE_LAYOUT_KEY,
|
||||
'chartLeft'
|
||||
)
|
||||
const [isCollapsed] = useLocalStorageState(SIDEBAR_COLLAPSE_KEY, false)
|
||||
const [showDepthChart] = useLocalStorageState<boolean>(DEPTH_CHART_KEY, false)
|
||||
|
||||
const totalCols = 24
|
||||
const gridBreakpoints = useMemo(() => {
|
||||
const sidebarWidth = isCollapsed ? 64 : 207
|
||||
return {
|
||||
md: breakpoints.md - sidebarWidth,
|
||||
lg: breakpoints.lg - sidebarWidth,
|
||||
xl: breakpoints.xl - sidebarWidth,
|
||||
xxl: breakpoints['2xl'] - sidebarWidth,
|
||||
xxxl: breakpoints['3xl'] - sidebarWidth,
|
||||
}
|
||||
}, [isCollapsed])
|
||||
|
||||
const defaultLayouts: ReactGridLayout.Layouts = useMemo(() => {
|
||||
const topnavbarHeight = 67
|
||||
const topnavbarHeight = 64
|
||||
const innerHeight = Math.max(height - topnavbarHeight, 1000)
|
||||
const marketHeaderHeight = 48
|
||||
|
||||
const balancesXPos = {
|
||||
chartLeft: { xxxl: 0, xxl: 0, xl: 0, lg: 0 },
|
||||
chartMiddleOBRight: { xxxl: 4, xxl: 5, xl: 5, lg: 6 },
|
||||
chartMiddleOBLeft: { xxxl: 4, xxl: 4, xl: 4, lg: 5 },
|
||||
chartRight: { xxxl: 9, xxl: 9, xl: 9, lg: 11 },
|
||||
chartMiddleOBLeft: { xxxl: 0, xxl: 0, xl: 0, lg: 0 },
|
||||
chartRight: { xxxl: 4, xxl: 5, xl: 5, lg: 5 },
|
||||
}
|
||||
|
||||
const chartXPos = {
|
||||
chartLeft: { xxxl: 0, xxl: 0, xl: 0, lg: 0 },
|
||||
chartMiddleOBRight: { xxxl: 4, xxl: 5, xl: 5, lg: 6 },
|
||||
chartMiddleOBLeft: { xxxl: 4, xxl: 4, xl: 4, lg: 5 },
|
||||
chartRight: { xxxl: 9, xxl: 9, xl: 9, lg: 11 },
|
||||
chartMiddleOBRight: { xxxl: 4, xxl: 5, xl: 5, lg: 5 },
|
||||
chartMiddleOBLeft: {
|
||||
xxxl: showDepthChart ? 7 : 4,
|
||||
xxl: showDepthChart ? 7 : 4,
|
||||
xl: showDepthChart ? 7 : 4,
|
||||
lg: showDepthChart ? 8 : 5,
|
||||
},
|
||||
chartRight: {
|
||||
xxxl: showDepthChart ? 12 : 9,
|
||||
xxl: showDepthChart ? 12 : 9,
|
||||
xl: showDepthChart ? 12 : 9,
|
||||
lg: showDepthChart ? 14 : 11,
|
||||
},
|
||||
}
|
||||
|
||||
const bookXPos = {
|
||||
chartLeft: { xxxl: 16, xxl: 15, xl: 15, lg: 13 },
|
||||
chartMiddleOBRight: { xxxl: 20, xxl: 20, xl: 20, lg: 19 },
|
||||
chartLeft: {
|
||||
xxxl: showDepthChart ? 13 : 16,
|
||||
xxl: showDepthChart ? 12 : 15,
|
||||
xl: showDepthChart ? 12 : 15,
|
||||
lg: showDepthChart ? 11 : 14,
|
||||
},
|
||||
chartMiddleOBRight: {
|
||||
xxxl: showDepthChart ? 17 : 20,
|
||||
xxl: showDepthChart ? 17 : 20,
|
||||
xl: showDepthChart ? 17 : 20,
|
||||
lg: showDepthChart ? 16 : 19,
|
||||
},
|
||||
chartMiddleOBLeft: { xxxl: 0, xxl: 0, xl: 0, lg: 0 },
|
||||
chartRight: { xxxl: 4, xxl: 5, xl: 5, lg: 6 },
|
||||
chartRight: { xxxl: 4, xxl: 5, xl: 5, lg: 5 },
|
||||
}
|
||||
|
||||
const formXPos = {
|
||||
chartLeft: { xxxl: 20, xxl: 19, xl: 19, lg: 18 },
|
||||
chartLeft: { xxxl: 20, xxl: 19, xl: 19, lg: 19 },
|
||||
chartMiddleOBRight: { xxxl: 0, xxl: 0, xl: 0, lg: 0 },
|
||||
chartMiddleOBLeft: { xxxl: 20, xxl: 19, xl: 19, lg: 18 },
|
||||
chartMiddleOBLeft: { xxxl: 20, xxl: 19, xl: 19, lg: 19 },
|
||||
chartRight: { xxxl: 0, xxl: 0, xl: 0, lg: 0 },
|
||||
}
|
||||
|
||||
return {
|
||||
xxxl: [
|
||||
{ i: 'market-header', x: 0, y: 0, w: 24, h: marketHeaderHeight },
|
||||
{ i: 'tv-chart', x: chartXPos[tradeLayout].xxxl, y: 1, w: 16, h: 640 },
|
||||
{
|
||||
i: 'balances',
|
||||
x: balancesXPos[tradeLayout].xxxl,
|
||||
y: 2,
|
||||
w: 16,
|
||||
h: getHeight(innerHeight, 0, 640),
|
||||
i: 'tv-chart',
|
||||
x: chartXPos[tradeLayout].xxxl,
|
||||
y: 1,
|
||||
w: showDepthChart ? 13 : 16,
|
||||
h: 640,
|
||||
},
|
||||
{
|
||||
i: 'orderbook',
|
||||
x: bookXPos[tradeLayout].xxxl,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: getHeight(innerHeight, 0, 0),
|
||||
y: 1,
|
||||
w: showDepthChart ? 7 : 4,
|
||||
h: 640,
|
||||
},
|
||||
{
|
||||
i: 'trade-form',
|
||||
x: formXPos[tradeLayout].xxxl,
|
||||
y: 0,
|
||||
y: 1,
|
||||
w: 4,
|
||||
h: getHeight(innerHeight, 0, 0),
|
||||
},
|
||||
{
|
||||
i: 'balances',
|
||||
x: balancesXPos[tradeLayout].xxxl,
|
||||
y: 2,
|
||||
w: 20,
|
||||
h: getHeight(innerHeight, 0, 640),
|
||||
},
|
||||
],
|
||||
xxl: [
|
||||
{ i: 'market-header', x: 0, y: 0, w: 24, h: marketHeaderHeight },
|
||||
{ i: 'tv-chart', x: chartXPos[tradeLayout].xxl, y: 1, w: 15, h: 488 },
|
||||
{
|
||||
i: 'balances',
|
||||
x: balancesXPos[tradeLayout].xxl,
|
||||
y: 2,
|
||||
w: 15,
|
||||
h: getHeight(innerHeight, 0, 488),
|
||||
i: 'tv-chart',
|
||||
x: chartXPos[tradeLayout].xxl,
|
||||
y: 1,
|
||||
w: showDepthChart ? 12 : 15,
|
||||
h: 552,
|
||||
},
|
||||
{
|
||||
i: 'orderbook',
|
||||
x: bookXPos[tradeLayout].xxl,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: getHeight(innerHeight, 0, 0),
|
||||
y: 1,
|
||||
w: showDepthChart ? 7 : 4,
|
||||
h: 552,
|
||||
},
|
||||
{
|
||||
i: 'trade-form',
|
||||
x: formXPos[tradeLayout].xxl,
|
||||
y: 0,
|
||||
y: 1,
|
||||
w: 5,
|
||||
h: getHeight(innerHeight, 0, 0),
|
||||
},
|
||||
{
|
||||
i: 'balances',
|
||||
x: balancesXPos[tradeLayout].xxl,
|
||||
y: 2,
|
||||
w: 19,
|
||||
h: getHeight(innerHeight, 0, 552),
|
||||
},
|
||||
],
|
||||
xl: [
|
||||
{ i: 'market-header', x: 0, y: 0, w: 24, h: marketHeaderHeight },
|
||||
{ i: 'tv-chart', x: chartXPos[tradeLayout].xl, y: 1, w: 15, h: 488 },
|
||||
{
|
||||
i: 'balances',
|
||||
x: balancesXPos[tradeLayout].xl,
|
||||
y: 2,
|
||||
w: 15,
|
||||
h: getHeight(innerHeight, 0, 488),
|
||||
i: 'tv-chart',
|
||||
x: chartXPos[tradeLayout].xl,
|
||||
y: 1,
|
||||
w: showDepthChart ? 12 : 15,
|
||||
h: 552,
|
||||
},
|
||||
{
|
||||
i: 'orderbook',
|
||||
x: bookXPos[tradeLayout].xl,
|
||||
y: 0,
|
||||
w: 4,
|
||||
h: getHeight(innerHeight, 0, 0),
|
||||
y: 1,
|
||||
w: showDepthChart ? 7 : 4,
|
||||
h: 552,
|
||||
},
|
||||
{
|
||||
i: 'trade-form',
|
||||
x: formXPos[tradeLayout].xl,
|
||||
y: 0,
|
||||
y: 1,
|
||||
w: 5,
|
||||
h: getHeight(innerHeight, 0, 0),
|
||||
},
|
||||
{
|
||||
i: 'balances',
|
||||
x: balancesXPos[tradeLayout].xl,
|
||||
y: 2,
|
||||
w: 19,
|
||||
h: getHeight(innerHeight, 0, 552),
|
||||
},
|
||||
],
|
||||
lg: [
|
||||
{ i: 'market-header', x: 0, y: 0, w: 24, h: marketHeaderHeight },
|
||||
{ i: 'tv-chart', x: chartXPos[tradeLayout].lg, y: 1, w: 13, h: 456 },
|
||||
{
|
||||
i: 'balances',
|
||||
x: balancesXPos[tradeLayout].lg,
|
||||
y: 2,
|
||||
w: 13,
|
||||
h: getHeight(innerHeight, 0, 456),
|
||||
i: 'tv-chart',
|
||||
x: chartXPos[tradeLayout].lg,
|
||||
y: 1,
|
||||
w: showDepthChart ? 11 : 14,
|
||||
h: 552,
|
||||
},
|
||||
{
|
||||
i: 'orderbook',
|
||||
x: bookXPos[tradeLayout].lg,
|
||||
y: 0,
|
||||
w: 5,
|
||||
h: getHeight(innerHeight, 0, 0),
|
||||
y: 1,
|
||||
w: showDepthChart ? 8 : 5,
|
||||
h: 552,
|
||||
},
|
||||
{
|
||||
i: 'trade-form',
|
||||
x: formXPos[tradeLayout].lg,
|
||||
y: 0,
|
||||
w: 6,
|
||||
y: 1,
|
||||
w: 5,
|
||||
h: getHeight(innerHeight, 0, 0),
|
||||
},
|
||||
{
|
||||
i: 'balances',
|
||||
x: balancesXPos[tradeLayout].lg,
|
||||
y: 2,
|
||||
w: 19,
|
||||
h: getHeight(innerHeight, 0, 552),
|
||||
},
|
||||
],
|
||||
md: [
|
||||
{ i: 'market-header', x: 0, y: 0, w: 24, h: marketHeaderHeight },
|
||||
{ i: 'tv-chart', x: 0, y: 1, w: 17, h: 464 },
|
||||
{ i: 'orderbook', x: 18, y: 2, w: 7, h: 552 },
|
||||
{ i: 'trade-form', x: 18, y: 1, w: 7, h: 572 },
|
||||
{ i: 'balances', x: 0, y: 2, w: 17, h: 428 + marketHeaderHeight },
|
||||
{ i: 'orderbook', x: 18, y: 2, w: 7, h: 428 + marketHeaderHeight },
|
||||
{ i: 'trade-form', x: 18, y: 1, w: 7, h: 492 + marketHeaderHeight },
|
||||
],
|
||||
}
|
||||
}, [height])
|
||||
}, [height, showDepthChart, tradeLayout])
|
||||
|
||||
const [layouts, setLayouts] = useState<Layouts>(defaultLayouts)
|
||||
const [breakpoint, setBreakpoint] = useState('')
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: Layout[] | undefined, layouts: Layouts) => {
|
||||
setLayouts(layouts)
|
||||
},
|
||||
[setLayouts]
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
handleLayoutChange(undefined, defaultLayouts)
|
||||
}, [breakpoint, showDepthChart, tradeLayout])
|
||||
|
||||
return showMobileView ? (
|
||||
<MobileTradeAdvancedPage />
|
||||
|
@ -210,10 +281,9 @@ const TradeAdvancedPage = () => {
|
|||
<TradeHotKeys>
|
||||
<FavoriteMarketsBar />
|
||||
<ResponsiveGridLayout
|
||||
onBreakpointChange={(bp) => console.log('bp: ', bp)}
|
||||
// layouts={savedLayouts ? savedLayouts : defaultLayouts}
|
||||
layouts={defaultLayouts}
|
||||
layouts={layouts}
|
||||
breakpoints={gridBreakpoints}
|
||||
onBreakpointChange={(bp) => setBreakpoint(bp)}
|
||||
cols={{
|
||||
xxxl: totalCols,
|
||||
xxl: totalCols,
|
||||
|
@ -228,6 +298,8 @@ const TradeAdvancedPage = () => {
|
|||
containerPadding={[0, 0]}
|
||||
margin={[0, 0]}
|
||||
useCSSTransforms
|
||||
onLayoutChange={handleLayoutChange}
|
||||
measureBeforeMount
|
||||
>
|
||||
<div key="market-header" className="z-10">
|
||||
<AdvancedMarketHeader />
|
||||
|
@ -240,22 +312,35 @@ const TradeAdvancedPage = () => {
|
|||
<TradingChartContainer />
|
||||
</div>
|
||||
</div>
|
||||
<div key="balances">
|
||||
<div
|
||||
className={`${
|
||||
tradeLayout === 'chartLeft' ? 'lg:border-r lg:border-th-bkg-3' : ''
|
||||
}`}
|
||||
key="balances"
|
||||
>
|
||||
<TradeInfoTabs />
|
||||
</div>
|
||||
<div
|
||||
className={`border-y border-l border-th-bkg-3 lg:border-b-0 ${
|
||||
tradeLayout === 'chartMiddleOBRight' ? 'lg:border-r' : ''
|
||||
} ${tradeLayout !== 'chartMiddleOBLeft' ? 'lg:border-l-0' : ''}`}
|
||||
tradeLayout === 'chartMiddleOBRight'
|
||||
? 'lg:border-r lg:border-l-0'
|
||||
: ''
|
||||
} ${
|
||||
tradeLayout === 'chartRight' ? 'lg:border-r lg:border-l-0' : ''
|
||||
} ${tradeLayout === 'chartLeft' ? 'lg:border-l-0' : ''}`}
|
||||
key="trade-form"
|
||||
>
|
||||
<AdvancedTradeForm />
|
||||
</div>
|
||||
<div
|
||||
key="orderbook"
|
||||
className={`overflow-hidden border-l border-th-bkg-3 lg:border-t lg:border-r ${
|
||||
tradeLayout === 'chartMiddleOBRight' ? 'lg:border-r-0' : ''
|
||||
} ${tradeLayout === 'chartMiddleOBLeft' ? 'lg:border-l-0' : ''}`}
|
||||
className={`overflow-hidden border-l border-th-bkg-3 lg:border-y ${
|
||||
tradeLayout === 'chartRight' ? 'lg:border-l-0 lg:border-r' : ''
|
||||
} ${
|
||||
tradeLayout === 'chartMiddleOBLeft'
|
||||
? 'lg:border-l-0 lg:border-r'
|
||||
: ''
|
||||
} ${tradeLayout === 'chartLeft' ? 'lg:border-r' : ''}`}
|
||||
>
|
||||
<OrderbookAndTrades />
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
"daily-volume": "24h Volume",
|
||||
"export": "Export {{dataType}}",
|
||||
"funding-chart": "Funding Chart",
|
||||
"init-health": "Init Health",
|
||||
"maint-health": "Maint Health",
|
||||
"health-contributions": "Health Contributions",
|
||||
"init-health-contribution": "Init Health Contribution",
|
||||
"init-health-contributions": "Init Health Contributions",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"connect-unsettled": "Connect to view your unsettled funds",
|
||||
"copy-and-share": "Copy Image to Clipboard",
|
||||
"current-price": "Current Price",
|
||||
"depth": "Depth",
|
||||
"edit-order": "Edit Order",
|
||||
"est-liq-price": "Est. Liq. Price",
|
||||
"avg-entry-price": "Avg. Entry Price",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"connect-unsettled": "Connect to view your unsettled funds",
|
||||
"copy-and-share": "Copy Image to Clipboard",
|
||||
"current-price": "Current Price",
|
||||
"depth": "Depth",
|
||||
"edit-order": "Edit Order",
|
||||
"est-liq-price": "Est. Liq. Price",
|
||||
"avg-entry-price": "Avg. Entry Price",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"connect-unsettled": "Connect to view your unsettled funds",
|
||||
"copy-and-share": "Copy Image to Clipboard",
|
||||
"current-price": "Current Price",
|
||||
"depth": "Depth",
|
||||
"edit-order": "Edit Order",
|
||||
"est-liq-price": "Est. Liq. Price",
|
||||
"avg-entry-price": "Avg. Entry Price",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"connect-unsettled": "Connect to view your unsettled funds",
|
||||
"copy-and-share": "Copy Image to Clipboard",
|
||||
"current-price": "Current Price",
|
||||
"depth": "Depth",
|
||||
"edit-order": "Edit Order",
|
||||
"est-liq-price": "Est. Liq. Price",
|
||||
"avg-entry-price": "Avg. Entry Price",
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"connect-unsettled": "連接以查看未結清餘額",
|
||||
"copy-and-share": "將圖片複製到剪貼版",
|
||||
"current-price": "目前價格",
|
||||
"depth": "Depth",
|
||||
"edit-order": "編輯訂單",
|
||||
"est-liq-price": "預計清算價格",
|
||||
"est-slippage": "預計下滑",
|
||||
|
|
|
@ -443,9 +443,6 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
},
|
||||
screens: {
|
||||
xl: '1600px',
|
||||
},
|
||||
},
|
||||
},
|
||||
// variants: {
|
||||
|
|
|
@ -414,6 +414,23 @@ export type TickerData = {
|
|||
ticker_id: string
|
||||
}
|
||||
|
||||
export type cumOrderbookSide = {
|
||||
price: number
|
||||
size: number
|
||||
cumulativeSize: number
|
||||
sizePercent: number
|
||||
maxSizePercent: number
|
||||
cumulativeSizePercent: number
|
||||
isUsersOrder: boolean
|
||||
}
|
||||
|
||||
export type OrderbookData = {
|
||||
bids: cumOrderbookSide[]
|
||||
asks: cumOrderbookSide[]
|
||||
spread: number
|
||||
spreadPercentage: number
|
||||
}
|
||||
|
||||
export interface HealthContribution {
|
||||
asset: string
|
||||
contribution: number
|
||||
|
|
|
@ -57,6 +57,8 @@ export const ACCEPT_TERMS_KEY = 'termsOfUseAccepted-0.1'
|
|||
|
||||
export const TRADE_LAYOUT_KEY = 'tradeLayoutKey-0.1'
|
||||
|
||||
export const DEPTH_CHART_KEY = 'showDepthChart-0.1'
|
||||
|
||||
export const STATS_TAB_KEY = 'activeStatsTab-0.1'
|
||||
|
||||
export const USE_ORDERBOOK_FEED_KEY = 'useOrderbookFeed-0.1'
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
import {
|
||||
BookSide,
|
||||
BookSideType,
|
||||
MangoClient,
|
||||
PerpMarket,
|
||||
} from '@blockworks-foundation/mango-v4'
|
||||
import { Market, Orderbook as SpotOrderBook } from '@project-serum/serum'
|
||||
import { AccountInfo } from '@solana/web3.js'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import Big from 'big.js'
|
||||
import { cumOrderbookSide } from 'types'
|
||||
import { getDecimalCount } from './numbers'
|
||||
|
||||
export 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)
|
||||
}
|
||||
|
||||
export const decodeBookL2 = (book: SpotOrderBook | BookSide): number[][] => {
|
||||
const depth = 300
|
||||
if (book instanceof SpotOrderBook) {
|
||||
return book.getL2(depth).map(([price, size]) => [price, size])
|
||||
} else if (book instanceof BookSide) {
|
||||
return book.getL2Ui(depth)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function decodeBook(
|
||||
client: MangoClient,
|
||||
market: Market | PerpMarket,
|
||||
accInfo: AccountInfo<Buffer>,
|
||||
side: 'bids' | 'asks'
|
||||
): 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
|
||||
}
|
||||
}
|
||||
|
||||
export const updatePerpMarketOnGroup = (
|
||||
book: BookSide,
|
||||
side: 'bids' | 'asks'
|
||||
) => {
|
||||
const group = mangoStore.getState().group
|
||||
const perpMarket = group?.getPerpMarketByMarketIndex(
|
||||
book.perpMarket.perpMarketIndex
|
||||
)
|
||||
if (perpMarket) {
|
||||
perpMarket[`_${side}`] = book
|
||||
// mangoStore.getState().actions.fetchOpenOrders()
|
||||
}
|
||||
}
|
||||
|
||||
export 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
|
||||
})
|
||||
}
|
||||
|
||||
export 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
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
|
@ -15,5 +15,5 @@ export const breakpoints = {
|
|||
// => @media (min-width: 1536px) { ... }
|
||||
|
||||
'3xl': 1792,
|
||||
// => @media (min-width: 1536px) { ... }
|
||||
// => @media (min-width: 1792px) { ... }
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue