237 lines
7.6 KiB
TypeScript
237 lines
7.6 KiB
TypeScript
import React, { useCallback, useMemo, useState } from 'react'
|
||
import useMangoStore from '../stores/useMangoStore'
|
||
import usePrevious from '../hooks/usePrevious'
|
||
import useInterval from '../hooks/useInterval'
|
||
import ChartApi from '../utils/chartDataConnector'
|
||
import UiLock from './UiLock'
|
||
import ManualRefresh from './ManualRefresh'
|
||
import useOraclePrice from '../hooks/useOraclePrice'
|
||
import DayHighLow from './DayHighLow'
|
||
import { useEffect } from 'react'
|
||
import { formatUsdValue } from '../utils'
|
||
|
||
function calculateFundingRate(perpStats, perpMarket) {
|
||
const oldestStat = perpStats[perpStats.length - 1]
|
||
const latestStat = perpStats[0]
|
||
|
||
if (!latestStat || !perpMarket) return null
|
||
|
||
const fundingDifference = latestStat.longFunding - oldestStat.longFunding
|
||
const fundingInQuoteDecimals =
|
||
fundingDifference / Math.pow(10, perpMarket.quoteDecimals)
|
||
const basePriceInBaseLots =
|
||
latestStat.baseOraclePrice * perpMarket.baseLotsToNumber(1)
|
||
|
||
return (fundingInQuoteDecimals / basePriceInBaseLots) * 100
|
||
}
|
||
|
||
function parseOpenInterest(perpStats, perpMarket) {
|
||
if (!perpStats?.length || !perpMarket) return 0
|
||
|
||
return perpMarket.baseLotsToNumber(perpStats[0].openInterest / 2)
|
||
}
|
||
|
||
const MarketHeader = () => {
|
||
const oraclePrice = useOraclePrice()
|
||
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
|
||
const selectedMarket = useMangoStore((s) => s.selectedMarket.current)
|
||
const mangoGroupName = useMangoStore((s) => s.selectedMangoGroup.name)
|
||
const baseSymbol = marketConfig.baseSymbol
|
||
const selectedMarketName = marketConfig.name
|
||
const previousMarketName: string = usePrevious(selectedMarketName)
|
||
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
|
||
const connected = useMangoStore((s) => s.wallet.connected)
|
||
|
||
const [ohlcv, setOhlcv] = useState(null)
|
||
const [loading, setLoading] = useState(false)
|
||
const [perpStats, setPerpStats] = useState([])
|
||
const [spotStats, setSpotStats] = useState(null)
|
||
// const change = ohlcv ? ((ohlcv.c[0] - ohlcv.o[0]) / ohlcv.o[0]) * 100 : '--'
|
||
const volume = ohlcv ? ohlcv.v[0] : '--'
|
||
|
||
const fetchSpotStats = useCallback(async () => {
|
||
const urlParams = new URLSearchParams({ mangoGroup: mangoGroupName })
|
||
urlParams.append('market', selectedMarketName)
|
||
const spotStats = await fetch(
|
||
'https://mango-stats-v3.herokuapp.com/spot/change/24?' + urlParams
|
||
)
|
||
|
||
const parsedSpotStats = await spotStats.json()
|
||
setSpotStats(parsedSpotStats)
|
||
}, [selectedMarketName, mangoGroupName])
|
||
|
||
const fetchPerpStats = useCallback(async () => {
|
||
const perpStats = await fetch(
|
||
`https://mango-stats-v3.herokuapp.com/perp/funding_rate?market=${selectedMarketName}`
|
||
)
|
||
const parsedPerpStats = await perpStats.json()
|
||
setPerpStats(parsedPerpStats)
|
||
}, [selectedMarketName])
|
||
|
||
useEffect(() => {
|
||
if (!selectedMarketName.includes('PERP')) {
|
||
fetchSpotStats()
|
||
}
|
||
}, [fetchSpotStats, selectedMarketName])
|
||
|
||
useEffect(() => {
|
||
if (selectedMarketName.includes('PERP')) {
|
||
fetchPerpStats()
|
||
}
|
||
}, [fetchPerpStats, selectedMarketName])
|
||
|
||
const fetchOhlcv = useCallback(async () => {
|
||
if (!selectedMarketName) return
|
||
|
||
// calculate from and to date (0:00UTC to 23:59:59UTC)
|
||
const date = new Date()
|
||
const utcFrom = new Date(
|
||
Date.UTC(
|
||
date.getFullYear(),
|
||
date.getUTCMonth(),
|
||
date.getUTCDate(),
|
||
0,
|
||
0,
|
||
0
|
||
)
|
||
)
|
||
const utcTo = new Date(
|
||
Date.UTC(
|
||
date.getFullYear(),
|
||
date.getUTCMonth(),
|
||
date.getUTCDate(),
|
||
23,
|
||
59,
|
||
59
|
||
)
|
||
)
|
||
|
||
const from = utcFrom.getTime() / 1000
|
||
const to = utcTo.getTime() / 1000
|
||
|
||
const ohlcv = await ChartApi.getOhlcv(selectedMarketName, '1D', from, to)
|
||
if (ohlcv) {
|
||
setOhlcv(ohlcv)
|
||
setLoading(false)
|
||
}
|
||
}, [selectedMarketName])
|
||
|
||
useInterval(async () => {
|
||
fetchOhlcv()
|
||
}, 5000)
|
||
|
||
useMemo(() => {
|
||
if (previousMarketName !== selectedMarketName) {
|
||
setLoading(true)
|
||
}
|
||
}, [selectedMarketName])
|
||
|
||
return (
|
||
<div
|
||
className={`flex items-end sm:items-center justify-between pt-4 px-6 md:pb-1 md:pt-8 md:px-6`}
|
||
>
|
||
<div className="flex flex-col sm:flex-row sm:items-center">
|
||
<div className="pb-3 sm:pb-0 pr-8">
|
||
<div className="flex items-center">
|
||
<img
|
||
alt=""
|
||
width="24"
|
||
height="24"
|
||
src={`/assets/icons/${baseSymbol.toLowerCase()}.svg`}
|
||
className={`mr-2.5`}
|
||
/>
|
||
|
||
<div className="font-semibold pr-0.5 text-xl">{baseSymbol}</div>
|
||
<span className="text-th-fgd-4 text-xl">
|
||
{selectedMarketName.includes('PERP') ? '–' : '/'}
|
||
</span>
|
||
<div className="font-semibold pl-0.5 text-xl">
|
||
{selectedMarketName.split(/\/|-/)[1]}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center">
|
||
<div className="pr-6">
|
||
<div className="text-th-fgd-3 tiny-text pb-0.5">Oracle price</div>
|
||
<div className="font-semibold text-th-fgd-1 text-xs">
|
||
{oraclePrice ? formatUsdValue(oraclePrice) : '--'}
|
||
</div>
|
||
</div>
|
||
<div className="pr-4">
|
||
<div className="text-th-fgd-3 tiny-text pb-0.5">24h Change</div>
|
||
{spotStats?.change ? (
|
||
<div
|
||
className={`font-semibold text-xs ${
|
||
spotStats.change > 0
|
||
? `text-th-green`
|
||
: spotStats.change < 0
|
||
? `text-th-red`
|
||
: `text-th-fgd-1`
|
||
}`}
|
||
>
|
||
{spotStats.change.toFixed(2) + '%'}
|
||
</div>
|
||
) : (
|
||
<MarketDataLoader />
|
||
)}
|
||
</div>
|
||
<div className="pr-6">
|
||
<div className="text-th-fgd-3 tiny-text pb-0.5">24h Vol</div>
|
||
<div className="font-semibold text-th-fgd-1 text-xs">
|
||
{ohlcv && !loading && volume ? (
|
||
volume !== '--' ? (
|
||
<>
|
||
{volume.toFixed(2)}
|
||
<span className="ml-1 text-th-fgd-3 tiny-text pb-0.5">
|
||
{baseSymbol}
|
||
</span>
|
||
</>
|
||
) : (
|
||
volume
|
||
)
|
||
) : (
|
||
<MarketDataLoader />
|
||
)}
|
||
</div>
|
||
</div>
|
||
<DayHighLow
|
||
high={spotStats?.high}
|
||
low={spotStats?.low}
|
||
latest={spotStats?.latest}
|
||
/>
|
||
{selectedMarketName.includes('PERP') ? (
|
||
<>
|
||
<div className="pr-6">
|
||
<div className="text-th-fgd-3 tiny-text pb-0.5">
|
||
Avg Funding Rate (1h)
|
||
</div>
|
||
<div className="font-semibold text-th-fgd-1 text-xs">
|
||
{calculateFundingRate(perpStats, selectedMarket)?.toFixed(6)}%
|
||
</div>
|
||
</div>
|
||
<div className="pr-6">
|
||
<div className="text-th-fgd-3 tiny-text pb-0.5">
|
||
Open Interest
|
||
</div>
|
||
<div className="font-semibold text-th-fgd-1 text-xs">
|
||
{parseOpenInterest(perpStats, selectedMarket)} {baseSymbol}
|
||
</div>
|
||
</div>
|
||
</>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center">
|
||
<UiLock />
|
||
{connected && mangoAccount ? <ManualRefresh className="pl-2" /> : null}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default MarketHeader
|
||
|
||
const MarketDataLoader = () => (
|
||
<div className="animate-pulse bg-th-bkg-3 h-3.5 mt-0.5 w-10 rounded-sm" />
|
||
)
|