This commit is contained in:
k
2026-04-21 19:32:53 +08:00
parent 1747fc489f
commit 83e5892ddf
25 changed files with 2582 additions and 916 deletions
+94 -7
View File
@@ -1,14 +1,101 @@
import { getTranslations } from 'next-intl/server'
'use client'
export default async function AnalyticsPage() {
const t = await getTranslations('analytics')
import { useState, useEffect } from 'react'
import { getPerformance, getTrades } from '@/lib/api'
import type { BotPerformance, BotTrade } from '@/types'
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'
}
export default function AnalyticsPage() {
const [perf, setPerf] = useState<BotPerformance | null>(null)
const [trades, setTrades] = useState<BotTrade[]>([])
const [period, setPeriod] = useState('30d')
useEffect(() => {
Promise.all([
getPerformance().catch(() => null),
getTrades(200, 1).catch(() => []),
]).then(([p, t]) => {
setPerf(p)
setTrades(t)
})
}, [])
const winRate = perf ? perf.win_rate * 100 : 0
const bestTrade = trades.length ? Math.max(...trades.map(t => t.pnl_usd)) : 0
const worstTrade = trades.length ? Math.min(...trades.map(t => t.pnl_usd)) : 0
const avgTrade = trades.length ? trades.reduce((s, t) => s + t.pnl_usd, 0) / trades.length : 0
const metrics = [
{ k: 'Max drawdown', v: perf ? perf.max_drawdown_pct.toFixed(1) + '%' : '—', sub: 'Worst peak-to-trough', down: true },
{ k: 'Total trades', v: String(perf?.total_trades ?? '—'), sub: 'Bot executions' },
{ k: 'Avg hold time', v: perf ? fmtHold(perf.avg_hold_seconds) : '—', 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 (
<div className="max-w-[1400px] mx-auto px-6 py-6">
<div className="bg-[#0a0a0a] border border-[#141414] rounded-[10px] p-10 text-center">
<h1 className="text-[22px] font-medium text-white mb-3">{t('title')}</h1>
<p className="text-[14px] text-[#555555]">{t('comingSoon')}</p>
<div className="page">
<div className="page-head">
<div>
<h1 className="page-title">Analytics</h1>
<p className="page-sub">Deep dive into {period} of signals and trades.</p>
</div>
<div className="nav-tabs">
{['7d', '30d', '90d', 'All'].map(p => (
<button key={p} className={`nav-tab ${period === p ? 'active' : ''}`} onClick={() => setPeriod(p)}>{p}</button>
))}
</div>
</div>
{/* Summary card */}
<div className="card" style={{ padding: 28, marginBottom: 20 }}>
<div className="row between" style={{ alignItems: 'flex-start', marginBottom: 20 }}>
<div>
<div className="tiny">Performance · {period}</div>
<div className="hero-value mono" style={{ marginTop: 6, fontSize: 40 }}>
{perf ? fmtMoney(perf.net_pnl_usd) : '—'}
</div>
<div className="row gap-s" style={{ marginTop: 8 }}>
<span className={`chip ${(perf?.win_rate ?? 0) >= 0.5 ? 'up' : 'down'}`}>{winRate.toFixed(1)}% win rate</span>
<span className="chip">{perf?.total_trades ?? 0} trades</span>
</div>
</div>
</div>
</div>
{/* Metric grid */}
<div className="metric-grid" style={{ marginBottom: 20 }}>
{metrics.map(m => (
<div key={m.k} className="card" style={{ padding: 20 }}>
<div className="tiny" style={{ marginBottom: 8 }}>{m.k}</div>
<div className={`mono ${m.up ? 'delta up' : m.down ? 'delta down' : ''}`} style={{ fontSize: 24, fontWeight: 500, letterSpacing: '-0.01em' }}>{m.v}</div>
<div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 4 }}>{m.sub}</div>
</div>
))}
</div>
{/* No data message */}
{!perf && !trades.length && (
<div className="card" style={{ padding: 60, textAlign: 'center', color: 'var(--ink-3)' }}>
<p style={{ fontSize: 14 }}>No trade data yet. The bot will populate analytics once it starts executing.</p>
</div>
)}
</div>
)
}