'use client' import { useState, useEffect } from 'react' import { useLocale } from 'next-intl' import { useAccount, useSignMessage } from 'wagmi' import { getPerformance, getTrades, getSignalAccuracy } from '@/lib/api' import type { BotPerformance, BotTrade } from '@/types' import type { SignalAccuracy } from '@/lib/api' import { getCachedViewEnvelope, getOrCreateViewEnvelope } from '@/lib/signedRequest' 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[]) { 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 AnalyticsPageClient() { const locale = useLocale() const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json const { address, isConnected } = useAccount() const { signMessageAsync } = useSignMessage() const [perf, setPerf] = useState(null) const [trades, setTrades] = useState([]) const [accuracy, setAccuracy] = useState(null) const [period, setPeriod] = useState('30d') useEffect(() => { let cancelled = false if (!address || !isConnected) { setPerf(null) setTrades([]) setAccuracy(null) return } ;(async () => { try { const perfEnv = getCachedViewEnvelope('view_performance', address) ?? await getOrCreateViewEnvelope({ action: 'view_performance', wallet: address, signMessageAsync }) const tradesEnv = getCachedViewEnvelope('view_trades', address) ?? await getOrCreateViewEnvelope({ action: 'view_trades', wallet: address, signMessageAsync }) const [p, t, a] = await Promise.all([ getPerformance(address, perfEnv).catch(() => null), getTrades(address, tradesEnv, 100, 1).catch(() => []), getSignalAccuracy().catch(() => null), ]) if (!cancelled) { setPerf(p) setTrades(t) setAccuracy(a) } } catch { if (!cancelled) { setPerf(null) setTrades([]) } } })() return () => { cancelled = true } }, [address, isConnected, signMessageAsync]) const filteredTrades = trades.filter((trade) => inPeriod(trade.closed_at, period)) 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: isZh ? '最大回撤' : 'Max drawdown', v: filteredTrades.length ? maxDrawdown.toFixed(1) + '%' : '—', sub: isZh ? '从高点到低点的最大跌幅' : 'Worst peak-to-trough', down: true }, { k: isZh ? '总交易数' : 'Total trades', v: String(filteredTrades.length || '—'), sub: isZh ? '已平仓机器人交易' : 'Closed bot executions' }, { k: isZh ? '平均持仓时间' : 'Avg hold time', v: filteredTrades.length ? fmtHold(Math.round(avgHold)) : '—', sub: isZh ? '按单笔计算' : 'Per trade' }, { k: isZh ? '平均单笔盈亏' : 'Avg trade P&L', v: fmtMoney(avgTrade), sub: isZh ? '每笔交易均值' : 'Mean per trade', up: avgTrade > 0 }, { k: isZh ? '最佳单笔' : 'Best trade', v: fmtMoney(bestTrade), sub: isZh ? '单笔最大盈利' : 'Largest single win', up: true }, { k: isZh ? '最差单笔' : 'Worst trade', v: fmtMoney(worstTrade), sub: isZh ? '单笔最大亏损' : 'Largest single loss', down: true }, ] return (

{isZh ? '分析面板' : 'Analytics'}

{isZh ? `查看 ${period} 窗口内的信号与交易表现。` : `Deep dive into ${period} of signals and trades.`}

{(['7d', '30d', '90d', 'All'] as const).map(p => ( ))}
{isZh ? '表现' : 'Performance'} · {period}
{filteredTrades.length ? fmtMoney(summaryPnl) : '—'}
= 50 ? 'up' : 'down'}`}>{winRate.toFixed(1)}% {isZh ? '胜率' : 'win rate'} {isZh ? `${filteredTrades.length} 笔交易` : `${filteredTrades.length} trades`}
{metrics.map(m => (
{m.k}
{m.v}
{m.sub}
))}
{accuracy && (
{isZh ? `AI 信号准确率 · ${accuracy.total_directional_signals} 条方向性信号` : `AI Signal Accuracy · ${accuracy.total_directional_signals} directional signals`}
{isZh ? '整体' : 'Overall'}
{(['m5','m15','m1h'] as const).map(w => { const d = accuracy.overall[w] const pct = d.accuracy_pct const color = pct >= 55 ? 'var(--up)' : pct >= 45 ? 'var(--ink-2)' : 'var(--down)' return (
{w.replace('m','').replace('1h','1h')} {pct.toFixed(0)}%
) })}
{isZh ? `${accuracy.overall.m5.checked} 条已测` : `${accuracy.overall.m5.checked} measured`}
{Object.entries(accuracy.by_signal).map(([sig, data]) => { const label = sig === 'buy' ? (isZh ? '🟢 做多' : '🟢 Buy') : sig === 'short' ? (isZh ? '🔴 做空' : '🔴 Short') : (isZh ? '🟡 卖出' : '🟡 Sell') return (
{label} ({data.count})
{(['m5','m15','m1h'] as const).map(w => { const d = data[w] if (d.checked < 2) return (
{w}
) const pct = d.accuracy_pct const color = pct >= 55 ? 'var(--up)' : pct >= 45 ? 'var(--ink-2)' : 'var(--down)' return (
{w} {pct.toFixed(0)}%
) })}
{isZh ? `${data.m5.checked} 条已测` : `${data.m5.checked} measured`}
) })}
)} {!filteredTrades.length && (

{!isConnected || !address ? (isZh ? '连接钱包以加载你的分析数据。' : 'Connect your wallet to load your analytics.') : trades.length ? (isZh ? `${period} 时间窗内还没有已平仓交易。` : `No trades closed in the ${period} window yet.`) : (isZh ? '还没有交易数据。机器人开始执行后,这里会自动出现统计。' : 'No trade data yet. The bot will populate analytics once it starts executing.')}

)}
) }