'use client'; import { type ReactNode, useMemo } from 'react'; import { Badge, Box, Card, Heading, SimpleGrid, Skeleton, SkeletonText, Stat, Table, Text, } from '@chakra-ui/react'; import { Chart, useChart } from '@chakra-ui/charts'; import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, Legend, ReferenceLine, ResponsiveContainer, Tooltip, YAxis, } from 'recharts'; import { format } from 'date-fns'; import { FiBarChart2 } from 'react-icons/fi'; import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart'; import { TimeSeriesXAxis } from '#shared/Charts/TimeSeriesXAxis'; import { EmptyState } from '#ui'; import { FIXED_SYMBOLS, type ChartWindow } from './derivativesDashboardConfig'; import { type DerivativesChartRow, type PriceChartRow, type SymbolMetrics, buildDerivativesDashboardViewModel, toFiniteNumber, } from './derivativesViewModel'; type SymbolChartTheme = { primary: string; primaryNegative: string; secondary: string; }; const SYMBOL_THEMES: Record = { BTCUSDT: { primary: 'orange.solid', primaryNegative: 'red.solid', secondary: 'orange.solid', }, ETHUSDT: { primary: 'teal.solid', primaryNegative: 'pink.solid', secondary: 'teal.solid', }, }; const compactFormatter = new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 2, }); const compactSignedFormatter = new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 2, signDisplay: 'always', }); const formatCompact = (value: number | null | undefined) => { const parsed = toFiniteNumber(value); if (parsed == null) return 'n/a'; return compactFormatter.format(parsed); }; const formatSignedCompact = (value: number | null | undefined) => { const parsed = toFiniteNumber(value); if (parsed == null) return 'n/a'; return compactSignedFormatter.format(parsed); }; const formatPercent = (value: number | null | undefined) => { const parsed = toFiniteNumber(value); if (parsed == null) return 'n/a'; return `${parsed >= 0 ? '+' : ''}${parsed.toFixed(2)}%`; }; const formatFunding = (value: number | null | undefined) => { const parsed = toFiniteNumber(value); if (parsed == null) return 'n/a'; const basisPoints = parsed * 10_000; return `${basisPoints >= 0 ? '+' : ''}${basisPoints.toFixed(2)} bps`; }; const formatAxisCompact = (value: number) => compactFormatter.format(Math.abs(value)); const formatPrice = (value: number | null | undefined) => { const parsed = toFiniteNumber(value); if (parsed == null) return 'n/a'; const digits = parsed >= 1000 ? 2 : parsed >= 1 ? 3 : 6; return parsed.toLocaleString('en-US', { maximumFractionDigits: digits, minimumFractionDigits: 0, }); }; const getChartDomain = ( values: Array, options?: { includeZero?: boolean; minPaddingPct?: number }, ): [number, number] | undefined => { const finite = values.filter( (value): value is number => typeof value === 'number' && Number.isFinite(value), ); if (!finite.length) return undefined; let min = Math.min(...finite); let max = Math.max(...finite); if (options?.includeZero) { min = Math.min(min, 0); max = Math.max(max, 0); } const span = max - min; const paddingPct = options?.minPaddingPct ?? 0.06; const basePadding = span > 0 ? span * paddingPct : Math.max(Math.abs(max || min || 1) * paddingPct, 1e-6); return [min - basePadding, max + basePadding]; }; const formatFullTime = (value: string | null | undefined) => { if (!value) return 'n/a'; const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return value; return format(parsed, 'dd.MM.yyyy HH:mm'); }; const getValueColor = (value: number | null | undefined) => { const parsed = toFiniteNumber(value); if (parsed == null || parsed === 0) return 'gray.200'; return parsed > 0 ? 'teal.300' : 'red.300'; }; const getFundingColor = (value: number | null | undefined) => { const parsed = toFiniteNumber(value); if (parsed == null || parsed === 0) return 'gray.200'; return parsed > 0 ? 'orange.300' : 'teal.300'; }; const getSymbolLabel = (symbol: string) => symbol === 'BTCUSDT' ? 'BTC' : symbol === 'ETHUSDT' ? 'ETH' : symbol; const DashboardSkeleton = () => ( <> {FIXED_SYMBOLS.map((symbol) => ( {Array.from({ length: 4 }).map((_, index) => ( ))} ))} ); const SymbolMetricsCard = ({ title, metrics, }: { title: string; metrics: SymbolMetrics; }) => ( {title} Updated {formatFullTime(metrics.lastTs)} Open Interest {formatCompact(metrics.currentOpenInterest)} OI Change {formatPercent(metrics.oiChangePct)} {formatSignedCompact(metrics.oiChange)} Funding {formatFunding(metrics.currentFundingRate)} Delta {formatFunding(metrics.fundingChange)} Liquidations {formatCompact(metrics.sumLiqTotal)} Long {formatCompact(metrics.sumLiqLong)} / Short{' '} {formatCompact(metrics.sumLiqShort)} ); const ChartCard = ({ title, description, children, }: { title: string; description: string; children: ReactNode; }) => ( {title} {description} {children} ); const SymbolPriceCard = ({ symbol, chartRows, window, }: { symbol: string; chartRows: PriceChartRow[]; window: ChartWindow; }) => { const theme = SYMBOL_THEMES[symbol]; const symbolLabel = getSymbolLabel(symbol); const priceChartConfig = useMemo( () => ({ data: chartRows, series: [{ name: 'price', color: theme.primary }], }), [chartRows, theme.primary], ); const priceChart = useChart(priceChartConfig as never); const latestPrice = chartRows[chartRows.length - 1]?.price ?? null; const priceDomain = useMemo( () => getChartDomain(chartRows.map((row) => row.price)), [chartRows], ); return ( } /> ); }; const SymbolOpenInterestCard = ({ symbol, chartRows, window, }: { symbol: string; chartRows: DerivativesChartRow[]; window: ChartWindow; }) => { const theme = SYMBOL_THEMES[symbol]; const symbolLabel = getSymbolLabel(symbol); const oiChartConfig = useMemo( () => ({ data: chartRows, series: [{ name: 'openInterest', color: theme.primary }], }), [chartRows, theme.primary], ); const oiChart = useChart(oiChartConfig as never); const oiDomain = useMemo( () => getChartDomain(chartRows.map((row) => row.openInterest)), [chartRows], ); return ( } /> ); }; const SymbolFundingCard = ({ symbol, chartRows, window, }: { symbol: string; chartRows: DerivativesChartRow[]; window: ChartWindow; }) => { const theme = SYMBOL_THEMES[symbol]; const symbolLabel = getSymbolLabel(symbol); const fundingChartConfig = useMemo( () => ({ data: chartRows, series: [{ name: 'funding', color: theme.primary }], }), [chartRows, theme.primary], ); const fundingChart = useChart(fundingChartConfig as never); return ( `${value} bps`} /> } /> {fundingChart.data.map((entry, idx) => ( = 0 ? fundingChart.color(theme.primary) : fundingChart.color(theme.primaryNegative) } /> ))} ); }; const SymbolLiquidationCard = ({ symbol, chartRows, window, }: { symbol: string; chartRows: DerivativesChartRow[]; window: ChartWindow; }) => { const theme = SYMBOL_THEMES[symbol]; const symbolLabel = getSymbolLabel(symbol); const liquidationChartConfig = useMemo( () => ({ data: chartRows, series: [ { name: 'longLiquidations', color: theme.primaryNegative }, { name: 'shortLiquidations', color: theme.secondary }, ], }), [chartRows, theme.primaryNegative, theme.secondary], ); const liquidationChart = useChart(liquidationChartConfig as never); return ( } /> ); }; type DashboardViewModel = ReturnType; export const DerivativesDashboardView = ({ dashboard, chartWindow, }: { dashboard: DashboardViewModel; chartWindow: ChartWindow; }) => { const { chartDataBySymbol, metricsBySymbol, noDetailData, noSummaryData, overviewRows, showSkeleton, } = dashboard; if (showSkeleton) return ; if (noSummaryData) { return ( ); } return ( <> {noDetailData ? ( ) : ( <> {FIXED_SYMBOLS.map((symbol) => ( ))} {FIXED_SYMBOLS.map((symbol) => ( ))} {FIXED_SYMBOLS.map((symbol) => ( ))} {FIXED_SYMBOLS.map((symbol) => ( ))} {FIXED_SYMBOLS.map((symbol) => ( ))} )} BTC / ETH Overview One row per symbol for the selected interval and window. Symbol OI OI Δ Funding Long Liq Short Liq Pressure Updated {overviewRows.map(({ symbol, metrics, bias }) => ( {getSymbolLabel(symbol)} {formatCompact(metrics.currentOpenInterest)} {formatPercent(metrics.oiChangePct)} {formatFunding(metrics.currentFundingRate)} {formatCompact(metrics.sumLiqLong)} {formatCompact(metrics.sumLiqShort)} {bias.label} {formatFullTime(metrics.lastTs)} ))} ); };