2022-09-20 13:05:50 -07:00
|
|
|
import useInterval from '@components/shared/useInterval'
|
2022-11-20 20:16:11 -08:00
|
|
|
import mangoStore from '@store/mangoStore'
|
2023-01-17 21:06:00 -08:00
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
2023-01-24 18:54:06 -08:00
|
|
|
import { formatNumericValue, getDecimalCount } from 'utils/numbers'
|
2022-09-20 13:05:50 -07:00
|
|
|
import { ChartTradeType } from 'types'
|
|
|
|
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 { IconButton } from '@components/shared/Button'
|
|
|
|
import useLocalStorageState from 'hooks/useLocalStorageState'
|
2023-01-17 21:06:00 -08:00
|
|
|
import { SOUND_SETTINGS_KEY, TRADE_VOLUME_ALERT_KEY } from 'utils/constants'
|
|
|
|
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'
|
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
|
|
|
})
|
|
|
|
|
2022-09-20 13:05:50 -07:00
|
|
|
const RecentTrades = () => {
|
2022-11-20 20:52:03 -08:00
|
|
|
const { t } = useTranslation(['common', 'trade'])
|
2022-11-20 20:16:11 -08:00
|
|
|
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,
|
|
|
|
} = useSelectedMarket()
|
2022-09-20 13:05:50 -07:00
|
|
|
|
2023-01-17 21:06:00 -08:00
|
|
|
useEffect(() => {
|
|
|
|
if (!fills.length) return
|
|
|
|
if (!latestFillId) {
|
2023-02-13 19:54:15 -08:00
|
|
|
setLatestFillId(fills[0]?.orderId?.toString())
|
2023-01-17 21:06:00 -08:00
|
|
|
}
|
|
|
|
}, [fills])
|
|
|
|
|
|
|
|
useInterval(() => {
|
|
|
|
if (!soundSettings['recent-trades'] || !quoteBank) return
|
2023-02-13 19:06:50 -08:00
|
|
|
setLatestFillId(fills[0]?.orderId?.toString())
|
2023-01-17 21:06:00 -08:00
|
|
|
const fillsLimitIndex = fills.findIndex(
|
|
|
|
(f) => f.orderId.toString() === latestFillId
|
|
|
|
)
|
|
|
|
const newFillsVolumeValue = fills
|
|
|
|
.slice(0, fillsLimitIndex)
|
|
|
|
.reduce((a, c) => a + c.size * c.price, 0)
|
|
|
|
if (newFillsVolumeValue * quoteBank.uiPrice > Number(alertSettings.value)) {
|
|
|
|
volumeAlertSound.play()
|
|
|
|
}
|
|
|
|
}, alertSettings.seconds * 1000)
|
|
|
|
|
2022-11-20 20:16:11 -08:00
|
|
|
// const fetchRecentTrades = useCallback(async () => {
|
|
|
|
// if (!market) return
|
2022-09-20 13:05:50 -07:00
|
|
|
|
2022-11-20 20:16:11 -08:00
|
|
|
// try {
|
|
|
|
// const response = await fetch(
|
|
|
|
// `https://event-history-api-candles.herokuapp.com/trades/address/${market.publicKey}`
|
|
|
|
// )
|
|
|
|
// const parsedResp = await response.json()
|
|
|
|
// const newTrades = parsedResp.data
|
|
|
|
// if (!newTrades) return null
|
2022-09-20 13:05:50 -07:00
|
|
|
|
2022-11-20 20:16:11 -08:00
|
|
|
// if (newTrades.length && trades.length === 0) {
|
|
|
|
// setTrades(newTrades)
|
|
|
|
// } else if (newTrades?.length && !isEqual(newTrades[0], trades[0])) {
|
|
|
|
// setTrades(newTrades)
|
|
|
|
// }
|
|
|
|
// } catch (e) {
|
|
|
|
// console.error('Unable to fetch recent trades', e)
|
|
|
|
// }
|
|
|
|
// }, [market, trades])
|
2022-09-20 13:05:50 -07:00
|
|
|
|
2022-11-20 20:52:03 -08:00
|
|
|
useEffect(() => {
|
|
|
|
// if (CLUSTER === 'mainnet-beta') {
|
|
|
|
// fetchRecentTrades()
|
|
|
|
// }
|
|
|
|
const actions = mangoStore.getState().actions
|
|
|
|
actions.loadMarketFills()
|
|
|
|
}, [selectedMarket])
|
2022-09-20 13:05:50 -07:00
|
|
|
|
|
|
|
useInterval(async () => {
|
2022-11-20 20:16:11 -08:00
|
|
|
// if (CLUSTER === 'mainnet-beta') {
|
|
|
|
// fetchRecentTrades()
|
|
|
|
// }
|
|
|
|
const actions = mangoStore.getState().actions
|
|
|
|
actions.loadMarketFills()
|
|
|
|
}, 5000)
|
|
|
|
|
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(
|
|
|
|
(a: { buys: number; sells: number }, c: any) => {
|
|
|
|
if (c.side === 'buy' || c.takerSide === 1) {
|
|
|
|
a.buys = a.buys + c.size
|
|
|
|
} else {
|
|
|
|
a.sells = a.sells + c.size
|
|
|
|
}
|
|
|
|
return a
|
|
|
|
},
|
|
|
|
{ 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 (
|
2023-01-17 21:06:00 -08:00
|
|
|
<>
|
|
|
|
<div className="thin-scroll h-full overflow-y-scroll">
|
|
|
|
<div className="flex items-center justify-between border-b border-th-bkg-3 py-1 px-2">
|
|
|
|
<Tooltip content={t('trade:tooltip-volume-alert')} delay={250}>
|
|
|
|
<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>
|
|
|
|
<div className="px-2">
|
|
|
|
<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>
|
|
|
|
{!!fills.length &&
|
|
|
|
fills.map((trade: ChartTradeType, i: number) => {
|
|
|
|
const side =
|
2023-01-19 01:49:57 -08:00
|
|
|
trade.side || (trade.takerSide === 0 ? 'bid' : 'ask')
|
2023-01-17 21:06:00 -08:00
|
|
|
|
2023-01-29 16:53:05 -08:00
|
|
|
const formattedPrice =
|
|
|
|
market?.tickSize && trade.price
|
|
|
|
? formatNumericValue(
|
|
|
|
trade.price,
|
|
|
|
getDecimalCount(market.tickSize)
|
|
|
|
)
|
|
|
|
: trade?.price || 0
|
|
|
|
|
2023-01-17 21:06:00 -08:00
|
|
|
const formattedSize =
|
|
|
|
market?.minOrderSize && trade.size
|
2023-01-29 16:53:05 -08:00
|
|
|
? formatNumericValue(
|
2023-01-17 21:06:00 -08:00
|
|
|
trade.size,
|
|
|
|
getDecimalCount(market.minOrderSize)
|
|
|
|
)
|
2023-01-29 16:53:05 -08:00
|
|
|
: trade?.size || 0
|
2023-01-17 21:06:00 -08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<tr className="font-mono text-xs" key={i}>
|
|
|
|
<td
|
|
|
|
className={`pb-1.5 text-right ${
|
|
|
|
['buy', 'bid'].includes(side)
|
|
|
|
? 'text-th-up'
|
|
|
|
: 'text-th-down'
|
|
|
|
}`}
|
|
|
|
>
|
2023-01-29 16:53:05 -08:00
|
|
|
{formattedPrice}
|
2023-01-17 21:06:00 -08:00
|
|
|
</td>
|
2023-01-29 16:53:05 -08:00
|
|
|
<td className="pb-1.5 text-right">{formattedSize}</td>
|
2023-01-17 21:06:00 -08:00
|
|
|
<td className="pb-1.5 text-right text-th-fgd-4">
|
|
|
|
{trade.time
|
|
|
|
? new Date(trade.time).toLocaleTimeString()
|
|
|
|
: trade.timestamp
|
2023-01-19 01:49:57 -08:00
|
|
|
? dayjs(trade.timestamp.toNumber() * 1000).format(
|
|
|
|
'hh:mma'
|
|
|
|
)
|
2023-01-17 21:06:00 -08:00
|
|
|
: '-'}
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
)
|
|
|
|
})}
|
|
|
|
</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}
|
|
|
|
</>
|
2022-09-20 13:05:50 -07:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export default RecentTrades
|