Merge pull request #18 from blockworks-foundation/perp-stats
Add funding rate and open interest graphs
This commit is contained in:
commit
4fe6edf64b
|
@ -1,57 +1,129 @@
|
|||
import { PerpMarket } from '@blockworks-foundation/mango-client'
|
||||
import { useState } from 'react'
|
||||
import useMangoStats from '../../hooks/useMangoStats'
|
||||
import useMangoGroupConfig from '../../hooks/useMangoGroupConfig'
|
||||
import useMangoStore from '../../stores/useMangoStore'
|
||||
import Chart from '../Chart'
|
||||
import BN from 'bn.js'
|
||||
|
||||
const icons = {
|
||||
BTC: '/assets/icons/btc.svg',
|
||||
ETH: '/assets/icons/eth.svg',
|
||||
SOL: '/assets/icons/sol.svg',
|
||||
SRM: '/assets/icons/srm.svg',
|
||||
USDT: '/assets/icons/usdt.svg',
|
||||
USDC: '/assets/icons/usdc.svg',
|
||||
WUSDT: '/assets/icons/usdt.svg',
|
||||
'BTC-PERP': '/assets/icons/btc.svg',
|
||||
'ETH-PERP': '/assets/icons/eth.svg',
|
||||
'SOL-PERP': '/assets/icons/sol.svg',
|
||||
'SRM-PERP': '/assets/icons/srm.svg',
|
||||
'USDT-PERP': '/assets/icons/usdt.svg',
|
||||
'MNGO-PERP': '/assets/icons/mngo.svg',
|
||||
}
|
||||
|
||||
export default function StatsPerps() {
|
||||
const [selectedAsset, setSelectedAsset] = useState<string>('BTC')
|
||||
const { latestStats, stats } = useMangoStats()
|
||||
function calculateFundingRate(
|
||||
oldestLongFunding,
|
||||
oldestShortFunding,
|
||||
latestLongFunding,
|
||||
latestShortFunding,
|
||||
perpMarket,
|
||||
oraclePrice
|
||||
) {
|
||||
if (!perpMarket || !oraclePrice) return 0.0
|
||||
|
||||
const selectedStatsData = stats.filter((stat) => stat.name === selectedAsset)
|
||||
// Averaging long and short funding excludes socialized loss
|
||||
const startFunding =
|
||||
(parseFloat(oldestLongFunding) + parseFloat(oldestShortFunding)) / 2
|
||||
const endFunding =
|
||||
(parseFloat(latestLongFunding) + parseFloat(latestShortFunding)) / 2
|
||||
const fundingDifference = endFunding - startFunding
|
||||
|
||||
const fundingInQuoteDecimals =
|
||||
fundingDifference / Math.pow(10, perpMarket.quoteDecimals)
|
||||
|
||||
// TODO - use avgPrice and discard oraclePrice once stats are better
|
||||
// const avgPrice = (latestStat.baseOraclePrice + oldestStat.baseOraclePrice) / 2
|
||||
const basePriceInBaseLots =
|
||||
oraclePrice * perpMarket.baseLotsToNumber(new BN(1))
|
||||
return (fundingInQuoteDecimals / basePriceInBaseLots) * 100
|
||||
}
|
||||
|
||||
export default function StatsPerps({ perpStats }) {
|
||||
const [selectedAsset, setSelectedAsset] = useState<string>('BTC-PERP')
|
||||
const marketConfigs = useMangoGroupConfig().perpMarkets
|
||||
const selectedMarketConfig = marketConfigs.find(
|
||||
(m) => m.name === selectedAsset
|
||||
)
|
||||
const markets = Object.values(
|
||||
useMangoStore.getState().selectedMangoGroup.markets
|
||||
).filter((m) => m instanceof PerpMarket) as PerpMarket[]
|
||||
const selectedMarket = markets.find((m) =>
|
||||
m.publicKey.equals(selectedMarketConfig.publicKey)
|
||||
)
|
||||
const selectedStatsData = perpStats.filter(
|
||||
(stat) => stat.name === selectedAsset
|
||||
)
|
||||
|
||||
const perpsData = selectedStatsData.map((x) => {
|
||||
return {
|
||||
fundingRate: calculateFundingRate(
|
||||
x.oldestLongFunding,
|
||||
x.oldestShortFunding,
|
||||
x.latestLongFunding,
|
||||
x.latestShortFunding,
|
||||
selectedMarket,
|
||||
x.baseOraclePrice
|
||||
),
|
||||
openInterest: selectedMarket.baseLotsToNumber(x.openInterest) / 2,
|
||||
time: x.hourly,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col-reverse items-center sm:flex-row sm:justify-between sm:h-12 mb-4 w-full">
|
||||
<AssetHeader asset={selectedAsset} />
|
||||
<div className="flex pb-4 sm:pb-0">
|
||||
{latestStats.map((stat) => (
|
||||
{marketConfigs.map((market) => (
|
||||
<div
|
||||
className={`px-2 py-1 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-3
|
||||
${
|
||||
selectedAsset === stat.name
|
||||
selectedAsset === market.name
|
||||
? `ring-1 ring-inset ring-th-primary text-th-primary`
|
||||
: `text-th-fgd-1 opacity-50 hover:opacity-100`
|
||||
}
|
||||
`}
|
||||
onClick={() => setSelectedAsset(stat.name)}
|
||||
key={stat.name as string}
|
||||
onClick={() => setSelectedAsset(market.name)}
|
||||
key={market.name as string}
|
||||
>
|
||||
{stat.name}
|
||||
{market.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-flow-col grid-cols-1 grid-rows-1 gap-4 pb-8">
|
||||
<div className="grid grid-flow-col grid-cols-1 grid-rows-2 md:grid-cols-2 md:grid-rows-1 gap-4 pb-8">
|
||||
<div
|
||||
className="border border-th-bkg-3 relative p-4 rounded-md"
|
||||
style={{ height: '300px' }}
|
||||
>
|
||||
<Chart
|
||||
title="Funding Rate"
|
||||
title="Avg. Hourly Funding Rate"
|
||||
xAxis="time"
|
||||
yAxis="fundingRate"
|
||||
data={selectedStatsData}
|
||||
labelFormat={(x) => `${(x * 100).toFixed(5)}%`}
|
||||
type="bar"
|
||||
data={perpsData}
|
||||
labelFormat={(x) => `${x.toFixed(4)}%`}
|
||||
type="area"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="border border-th-bkg-3 relative p-4 rounded-md"
|
||||
style={{ height: '300px' }}
|
||||
>
|
||||
<Chart
|
||||
title="Open Interest"
|
||||
xAxis="time"
|
||||
yAxis="openInterest"
|
||||
data={perpsData}
|
||||
labelFormat={(x) =>
|
||||
x &&
|
||||
x.toLocaleString(undefined, {
|
||||
maximumFractionDigits: selectedMarketConfig.baseDecimals,
|
||||
}) + selectedMarketConfig.baseSymbol
|
||||
}
|
||||
type="area"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -61,7 +133,7 @@ export default function StatsPerps() {
|
|||
|
||||
const AssetHeader = ({ asset }) => {
|
||||
switch (asset) {
|
||||
case 'BTC':
|
||||
case 'BTC-PERP':
|
||||
return (
|
||||
<div className="flex items-center text-xl text-th-fgd-1">
|
||||
<img
|
||||
|
@ -71,10 +143,10 @@ const AssetHeader = ({ asset }) => {
|
|||
height="24"
|
||||
className="mr-2.5"
|
||||
/>
|
||||
Bitcoin
|
||||
Bitcoin Perpetual Futures
|
||||
</div>
|
||||
)
|
||||
case 'ETH':
|
||||
case 'ETH-PERP':
|
||||
return (
|
||||
<div className="flex items-center text-xl text-th-fgd-1">
|
||||
<img
|
||||
|
@ -84,10 +156,10 @@ const AssetHeader = ({ asset }) => {
|
|||
height="24"
|
||||
className="mr-2.5"
|
||||
/>
|
||||
Ethereum
|
||||
Ethereum Perpetual Futures
|
||||
</div>
|
||||
)
|
||||
case 'SOL':
|
||||
case 'SOL-PERP':
|
||||
return (
|
||||
<div className="flex items-center text-xl text-th-fgd-1">
|
||||
<img
|
||||
|
@ -97,7 +169,7 @@ const AssetHeader = ({ asset }) => {
|
|||
height="24"
|
||||
className="mr-2.5"
|
||||
/>
|
||||
Solana
|
||||
Solana Perpetual Futures
|
||||
</div>
|
||||
)
|
||||
case 'SRM':
|
||||
|
@ -110,20 +182,7 @@ const AssetHeader = ({ asset }) => {
|
|||
height="24"
|
||||
className="mr-2.5"
|
||||
/>
|
||||
Serum
|
||||
</div>
|
||||
)
|
||||
case 'USDC':
|
||||
return (
|
||||
<div className="flex items-center text-xl text-th-fgd-1">
|
||||
<img
|
||||
src={icons[asset]}
|
||||
alt={icons[asset]}
|
||||
width="24"
|
||||
height="24"
|
||||
className="mr-2.5"
|
||||
/>
|
||||
USD Coin
|
||||
Serum Perpetual Futures
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
|
@ -136,7 +195,7 @@ const AssetHeader = ({ asset }) => {
|
|||
height="24"
|
||||
className="mr-2.5"
|
||||
/>
|
||||
Bitcoin
|
||||
Bitcoin Perpetual Futures
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -17,6 +17,18 @@ const useMangoStats = () => {
|
|||
utilization: '0',
|
||||
},
|
||||
])
|
||||
const [perpStats, setPerpStats] = useState([
|
||||
{
|
||||
name: '',
|
||||
hourly: '',
|
||||
oldestLongFunding: 0,
|
||||
oldestShortFunding: 0,
|
||||
latestLongFunding: 0,
|
||||
latestShortFunding: 0,
|
||||
openInterest: 0,
|
||||
baseOraclePrice: 0,
|
||||
},
|
||||
])
|
||||
const [latestStats, setLatestStats] = useState<any[]>([])
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const mangoGroupName = useMangoStore((s) => s.selectedMangoGroup.name)
|
||||
|
@ -34,6 +46,17 @@ const useMangoStats = () => {
|
|||
fetchHistoricalStats()
|
||||
}, [mangoGroupName])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchHistoricalPerpStats = async () => {
|
||||
const response = await fetch(
|
||||
`https://mango-stats-v3.herokuapp.com/perp?mangoGroup=${mangoGroupName}`
|
||||
)
|
||||
const stats = await response.json()
|
||||
setPerpStats(stats)
|
||||
}
|
||||
fetchHistoricalPerpStats()
|
||||
}, [mangoGroupName])
|
||||
|
||||
useEffect(() => {
|
||||
const getLatestStats = async () => {
|
||||
if (mangoGroup) {
|
||||
|
@ -75,7 +98,7 @@ const useMangoStats = () => {
|
|||
getLatestStats()
|
||||
}, [mangoGroup])
|
||||
|
||||
return { latestStats, stats }
|
||||
return { latestStats, stats, perpStats }
|
||||
}
|
||||
|
||||
export default useMangoStats
|
||||
|
|
|
@ -9,13 +9,13 @@ import useMangoStats from '../hooks/useMangoStats'
|
|||
const TABS = [
|
||||
'Totals',
|
||||
'Assets',
|
||||
// 'Perps',
|
||||
'Perps',
|
||||
// 'Markets',
|
||||
// 'Liquidations',
|
||||
]
|
||||
|
||||
export default function StatsPage() {
|
||||
const { latestStats, stats } = useMangoStats()
|
||||
const { latestStats, stats, perpStats } = useMangoStats()
|
||||
const [activeTab, setActiveTab] = useState(TABS[0])
|
||||
|
||||
const handleTabChange = (tabName) => {
|
||||
|
@ -53,6 +53,7 @@ export default function StatsPage() {
|
|||
activeTab={activeTab}
|
||||
latestStats={latestStats}
|
||||
stats={stats}
|
||||
perpStats={perpStats}
|
||||
/>
|
||||
</div>
|
||||
</PageBodyContainer>
|
||||
|
@ -60,14 +61,14 @@ export default function StatsPage() {
|
|||
)
|
||||
}
|
||||
|
||||
const TabContent = ({ activeTab, latestStats, stats }) => {
|
||||
const TabContent = ({ activeTab, latestStats, stats, perpStats }) => {
|
||||
switch (activeTab) {
|
||||
case 'Totals':
|
||||
return <StatsTotals latestStats={latestStats} stats={stats} />
|
||||
case 'Assets':
|
||||
return <StatsAssets latestStats={latestStats} stats={stats} />
|
||||
case 'Perps':
|
||||
return <StatsPerps />
|
||||
return <StatsPerps perpStats={perpStats} />
|
||||
case 'Markets':
|
||||
return <div>Markets</div>
|
||||
case 'Liquidations':
|
||||
|
|
Loading…
Reference in New Issue