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

298 lines
10 KiB
TypeScript
Raw Normal View History

2022-09-20 13:05:50 -07:00
import useInterval from '@components/shared/useInterval'
import mangoStore from '@store/mangoStore'
2023-01-17 21:06:00 -08:00
import { useEffect, useMemo, useState } from 'react'
import { formatNumericValue, getDecimalCount } from 'utils/numbers'
2022-09-20 13:05:50 -07:00
import { useTranslation } from 'next-i18next'
2022-11-20 12:20:27 -08:00
import useSelectedMarket from 'hooks/useSelectedMarket'
2022-11-24 17:57:19 -08:00
import { Howl } from 'howler'
import useLocalStorageState from 'hooks/useLocalStorageState'
2023-02-26 15:44:38 -08:00
import {
MANGO_DATA_API_URL,
SOUND_SETTINGS_KEY,
TRADE_VOLUME_ALERT_KEY,
} from 'utils/constants'
import { IconButton } from '@components/shared/Button'
2023-01-17 21:06:00 -08:00
import { BellAlertIcon, BellSlashIcon } from '@heroicons/react/20/solid'
2022-11-24 17:57:19 -08:00
import Tooltip from '@components/shared/Tooltip'
2022-11-27 15:07:45 -08:00
import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
2023-01-17 21:06:00 -08:00
import TradeVolumeAlertModal, {
DEFAULT_VOLUME_ALERT_SETTINGS,
} from '@components/modals/TradeVolumeAlertModal'
2023-01-18 21:18:06 -08:00
import dayjs from 'dayjs'
import ErrorBoundary from '@components/ErrorBoundary'
2023-02-26 15:44:38 -08:00
import { useQuery } from '@tanstack/react-query'
import { PerpMarket } from '@blockworks-foundation/mango-v4'
2023-04-13 04:25:58 -07:00
import { EmptyObject, isPerpFillEvent, PerpTradeHistory } from 'types'
2023-02-26 15:44:38 -08:00
import { Market } from '@project-serum/serum'
2022-09-20 13:05:50 -07:00
2023-01-17 21:06:00 -08:00
const volumeAlertSound = new Howl({
2022-12-07 21:05:36 -08:00
src: ['/sounds/trade-buy.mp3'],
2023-01-17 21:06:00 -08:00
volume: 0.8,
2022-12-07 21:05:36 -08:00
})
type Test = { buys: number; sells: number }
2023-02-26 15:44:38 -08:00
const formatPrice = (
market: Market | PerpMarket | undefined,
price: number | string
) => {
return market?.tickSize
? formatNumericValue(price, getDecimalCount(market.tickSize))
: 0
}
const formatSize = (
market: Market | PerpMarket | undefined,
size: number | string
) => {
return market?.minOrderSize
? formatNumericValue(size, getDecimalCount(market.minOrderSize))
: 0
}
const fetchMarketTradeHistory = async (marketAddress: string) => {
const response = await fetch(
`${MANGO_DATA_API_URL}/stats/perp-market-history?perp-market=${marketAddress}`
)
return response.json()
}
2022-09-20 13:05:50 -07:00
const RecentTrades = () => {
2022-11-20 20:52:03 -08:00
const { t } = useTranslation(['common', 'trade'])
const fills = mangoStore((s) => s.selectedMarket.fills)
2023-01-17 21:06:00 -08:00
const [latestFillId, setLatestFillId] = useState('')
const [soundSettings] = useLocalStorageState(
2022-11-24 17:57:19 -08:00
SOUND_SETTINGS_KEY,
INITIAL_SOUND_SETTINGS
)
2023-01-17 21:06:00 -08:00
const [alertSettings] = useLocalStorageState(
TRADE_VOLUME_ALERT_KEY,
DEFAULT_VOLUME_ALERT_SETTINGS
)
const [showVolumeAlertModal, setShowVolumeAlertModal] = useState(false)
2022-11-28 18:01:31 -08:00
2023-01-14 21:01:30 -08:00
const {
selectedMarket,
serumOrPerpMarket: market,
baseSymbol,
2023-01-17 21:06:00 -08:00
quoteBank,
2023-01-14 21:01:30 -08:00
quoteSymbol,
2023-02-26 15:44:38 -08:00
selectedMarketAddress,
2023-01-14 21:01:30 -08:00
} = useSelectedMarket()
2022-09-20 13:05:50 -07:00
const perpMarketQuery = useQuery<PerpTradeHistory[] | EmptyObject>(
2023-02-26 15:44:38 -08:00
['market-trade-history', selectedMarketAddress],
() => fetchMarketTradeHistory(selectedMarketAddress!),
{
cacheTime: 1000 * 60 * 15,
staleTime: 0,
enabled: !!selectedMarketAddress && market instanceof PerpMarket,
2023-02-26 15:44:38 -08:00
refetchOnWindowFocus: true,
refetchInterval: 1000 * 10,
}
)
useEffect(() => {
const actions = mangoStore.getState().actions
if (selectedMarket) {
actions.loadMarketFills()
}
}, [selectedMarket])
2023-01-17 21:06:00 -08:00
useEffect(() => {
if (!fills.length) return
2023-02-13 20:06:37 -08:00
const latesetFill = fills[0]
2023-01-17 21:06:00 -08:00
if (!latestFillId) {
const fillId = isPerpFillEvent(latesetFill)
? latesetFill.takerClientOrderId
: latesetFill.orderId
2023-02-13 20:06:37 -08:00
setLatestFillId(fillId.toString())
2023-01-17 21:06:00 -08:00
}
}, [fills])
useInterval(() => {
2023-02-13 20:06:37 -08:00
const latesetFill = fills[0]
if (!soundSettings['recent-trades'] || !quoteBank || !latesetFill) return
const fillId = isPerpFillEvent(latesetFill)
? latesetFill.takerClientOrderId
: latesetFill.orderId
2023-02-13 20:06:37 -08:00
setLatestFillId(fillId.toString())
const fillsLimitIndex = fills.findIndex((f) => {
const id = isPerpFillEvent(f) ? f.takerClientOrderId : f.orderId
2023-02-13 20:06:37 -08:00
return id.toString() === fillId.toString()
})
2023-01-17 21:06:00 -08:00
const newFillsVolumeValue = fills
.slice(0, fillsLimitIndex)
.reduce((a, c) => {
const size = isPerpFillEvent(c) ? c.quantity : c.size
return a + size * c.price
}, 0)
2023-01-17 21:06:00 -08:00
if (newFillsVolumeValue * quoteBank.uiPrice > Number(alertSettings.value)) {
volumeAlertSound.play()
}
}, alertSettings.seconds * 1000)
2023-01-05 01:30:52 -08:00
const [buyRatio, sellRatio] = useMemo(() => {
if (!fills.length) return [0, 0]
2023-01-10 17:00:26 -08:00
const vol = fills.reduce(
(acc: Test, fill) => {
let side
let size
if (isPerpFillEvent(fill)) {
side = fill.takerSide === 0 ? 'buy' : 'sell'
size = fill.quantity
} else {
side = fill.side
size = fill.size
}
if (side === 'buy') {
acc.buys = acc.buys + size
2023-01-10 17:00:26 -08:00
} else {
acc.sells = acc.sells + size
2023-01-10 17:00:26 -08:00
}
return acc
2023-01-10 17:00:26 -08:00
},
{ buys: 0, sells: 0 }
)
const totalVol = vol.buys + vol.sells
return [vol.buys / totalVol, vol.sells / totalVol]
2023-01-05 01:30:52 -08:00
}, [fills])
2022-09-20 13:05:50 -07:00
return (
<ErrorBoundary>
2023-02-28 09:13:30 -08:00
<div className="hide-scroll h-full overflow-y-scroll">
<div className="flex items-center justify-between border-b border-th-bkg-3 py-1 pr-2 pl-0">
<Tooltip
className="hidden md:block"
content={t('trade:tooltip-volume-alert')}
2023-03-10 10:01:47 -08:00
delay={100}
>
2023-01-17 21:06:00 -08:00
<IconButton
onClick={() => setShowVolumeAlertModal(true)}
size="small"
hideBg
>
{soundSettings['recent-trades'] ? (
<BellAlertIcon className="h-4 w-4 text-th-fgd-3" />
) : (
<BellSlashIcon className="h-4 w-4 text-th-fgd-3" />
)}
</IconButton>
</Tooltip>
2023-01-20 03:38:56 -08:00
<span className="text-xxs text-th-fgd-4 xl:text-xs">
2023-01-17 21:06:00 -08:00
{t('trade:buys')}:{' '}
2023-01-20 03:38:56 -08:00
<span className="text-th-up">{(buyRatio * 100).toFixed(1)}%</span>
2023-01-17 21:06:00 -08:00
<span className="px-2">|</span>
{t('trade:sells')}:{' '}
2023-01-20 03:38:56 -08:00
<span className="text-th-down">
2023-01-17 21:06:00 -08:00
{(sellRatio * 100).toFixed(1)}%
</span>
2023-01-05 01:30:52 -08:00
</span>
2023-01-17 21:06:00 -08:00
</div>
2023-02-28 09:13:30 -08:00
<div className="pl-0 pr-2">
2023-01-17 21:06:00 -08:00
<table className="min-w-full">
<thead>
<tr className="text-right text-xxs text-th-fgd-4">
<th className="py-2 font-normal">{`${t(
'price'
)} (${quoteSymbol})`}</th>
<th className="py-2 font-normal">
{t('trade:size')} ({baseSymbol})
</th>
<th className="py-2 font-normal">{t('time')}</th>
</tr>
</thead>
<tbody>
2023-02-26 15:44:38 -08:00
{selectedMarket instanceof PerpMarket
? perpMarketQuery?.data &&
Array.isArray(perpMarketQuery?.data) &&
2023-02-26 15:44:38 -08:00
perpMarketQuery?.data.map((t) => {
return (
<tr className="font-mono text-xs" key={`${t.seq_num}`}>
<td
2023-02-28 09:13:30 -08:00
className={`pb-1.5 text-right tracking-tight ${
2023-02-26 15:44:38 -08:00
['buy', 'bid'].includes(t.taker_side)
? 'text-th-up'
: 'text-th-down'
}`}
>
{formatPrice(market, t.price)}
</td>
2023-02-28 09:13:30 -08:00
<td className="pb-1.5 text-right tracking-normal text-th-fgd-3">
2023-02-26 15:44:38 -08:00
{formatSize(market, t.quantity)}
</td>
2023-02-28 09:13:30 -08:00
<td className="pb-1.5 text-right tracking-tight text-th-fgd-4">
2023-02-26 15:44:38 -08:00
{t.block_datetime ? (
<Tooltip
placement="right"
content={new Date(
t.block_datetime
).toLocaleDateString()}
>
{new Date(t.block_datetime).toLocaleTimeString()}
</Tooltip>
) : (
'-'
)}
</td>
</tr>
)
})
: !!fills.length &&
fills.map((trade, i: number) => {
let side
let size
let time
if (isPerpFillEvent(trade)) {
side = trade.takerSide === 0 ? 'bid' : 'ask'
size = trade.quantity
time = trade.timestamp.toString()
} else {
side = trade.side
size = trade.size
time = ''
}
const formattedPrice = formatPrice(market, trade.price)
const formattedSize = formatSize(market, size)
return (
<tr className="font-mono text-xs" key={i}>
<td
2023-02-28 09:13:30 -08:00
className={`pb-1.5 text-right tracking-tight ${
2023-02-26 15:44:38 -08:00
['buy', 'bid'].includes(side)
? 'text-th-up'
: 'text-th-down'
}`}
>
{formattedPrice}
</td>
2023-02-28 09:13:30 -08:00
<td className="pb-1.5 text-right tracking-normal text-th-fgd-3">
{formattedSize}
</td>
<td className="pb-1.5 text-right tracking-tight text-th-fgd-4">
2023-02-26 15:44:38 -08:00
{time
? dayjs(Number(time) * 1000).format('hh:mma')
: '-'}
</td>
</tr>
)
})}
2023-01-17 21:06:00 -08:00
</tbody>
</table>
</div>
2022-11-24 17:57:19 -08:00
</div>
2023-01-17 21:06:00 -08:00
{showVolumeAlertModal ? (
<TradeVolumeAlertModal
isOpen={showVolumeAlertModal}
onClose={() => setShowVolumeAlertModal(false)}
/>
) : null}
</ErrorBoundary>
2022-09-20 13:05:50 -07:00
)
}
export default RecentTrades