'use client' import { useState, useEffect } from 'react' import { getPerformance, getTrades } from '@/lib/api' import type { BotPerformance, BotTrade } from '@/types' type Period = '7d' | '30d' | '90d' | 'All' function fmtMoney(n: number) { if (n == null || isNaN(n)) return '—' const abs = Math.abs(n) const s = abs.toLocaleString('en-US', { maximumFractionDigits: 0 }) if (n < 0) return '-$' + s if (n > 0) return '+$' + s return '$' + s } function fmtHold(s: number) { if (s < 60) return s + 's' const m = Math.floor(s / 60) if (m < 60) return m + 'm' return Math.floor(m / 60) + 'h ' + (m % 60) + 'm' } function inPeriod(iso: string, period: Period) { if (period === 'All') return true const days = Number.parseInt(period, 10) if (Number.isNaN(days)) return true const time = new Date(iso).getTime() return time >= Date.now() - days * 24 * 60 * 60 * 1000 } function calcDrawdownPct(trades: BotTrade[]) { // Skip externally-closed trades (pnl_usd null) — including them as `+ null` // turns equity into NaN and the whole drawdown chart goes blank. const ordered = [...trades] .filter((t) => t.pnl_usd !== null && t.pnl_usd !== undefined) .sort((a, b) => new Date(a.closed_at).getTime() - new Date(b.closed_at).getTime()) let equity = 0 let peak = 0 let maxDrawdownPct = 0 for (const trade of ordered) { equity += trade.pnl_usd as number peak = Math.max(peak, equity) if (peak > 0) { maxDrawdownPct = Math.max(maxDrawdownPct, ((peak - equity) / peak) * 100) } } return maxDrawdownPct } export default function AnalyticsPage() { const [perf, setPerf] = useState(null) const [trades, setTrades] = useState([]) const [period, setPeriod] = useState('30d') useEffect(() => { Promise.all([ getPerformance().catch(() => null), getTrades(100, 1).catch(() => []), ]).then(([p, t]) => { setPerf(p) setTrades(t) }) }, []) const filteredTrades = trades.filter((trade) => inPeriod(trade.closed_at, period)) // Trades closed externally on Hyperliquid have pnl_usd === null. Including // them silently in reduce/Math.max/Math.min produces NaN ("$NaN P&L") and // also pollutes win_rate. Aggregate only on the priced subset. const pricedTrades = filteredTrades.filter( (t) => t.pnl_usd !== null && t.pnl_usd !== undefined, ) const pnls = pricedTrades.map((t) => t.pnl_usd as number) const totalPnl = pnls.reduce((s, p) => s + p, 0) const wins = pnls.filter((p) => p > 0).length const winRate = pricedTrades.length ? (wins / pricedTrades.length) * 100 : 0 const bestTrade = pnls.length ? Math.max(...pnls) : 0 const worstTrade = pnls.length ? Math.min(...pnls) : 0 const avgTrade = pnls.length ? totalPnl / pnls.length : 0 const avgHold = filteredTrades.length ? filteredTrades.reduce((sum, trade) => sum + trade.hold_seconds, 0) / filteredTrades.length : 0 const maxDrawdown = period === '30d' && perf ? perf.max_drawdown_pct : calcDrawdownPct(filteredTrades) const summaryPnl = period === '30d' && perf && trades.length >= filteredTrades.length ? perf.net_pnl_usd : totalPnl const metrics = [ { k: 'Max drawdown', v: filteredTrades.length ? maxDrawdown.toFixed(1) + '%' : '—', sub: 'Worst peak-to-trough', down: true }, { k: 'Total trades', v: String(filteredTrades.length || '—'), sub: 'Closed bot executions' }, { k: 'Avg hold time', v: filteredTrades.length ? fmtHold(Math.round(avgHold)) : '—', sub: 'Per trade' }, { k: 'Avg trade P&L', v: fmtMoney(avgTrade), sub: 'Mean per trade', up: avgTrade > 0 }, { k: 'Best trade', v: fmtMoney(bestTrade), sub: 'Largest single win', up: true }, { k: 'Worst trade', v: fmtMoney(worstTrade), sub: 'Largest single loss', down: true }, ] return (

Analytics

Deep dive into {period} of signals and trades.

{(['7d', '30d', '90d', 'All'] as const).map(p => ( ))}
{/* Summary card */}
Performance · {period}
{filteredTrades.length ? fmtMoney(summaryPnl) : '—'}
= 50 ? 'up' : 'down'}`}>{winRate.toFixed(1)}% win rate {filteredTrades.length} trades
{/* Metric grid */}
{metrics.map(m => (
{m.k}
{m.v}
{m.sub}
))}
{/* No data message */} {!filteredTrades.length && (

{trades.length ? `No trades closed in the ${period} window yet.` : 'No trade data yet. The bot will populate analytics once it starts executing.'}

)}
) }