feat: revamp dashboard, trades, and add landing/legal pages
- Major UI updates across dashboard, analytics, posts, trades, settings - New landing page, robots/sitemap, contact/privacy/terms pages - Updated globals.css with extensive styling and new landing.css - Refactor signedRequest, realtime data hook, and dashboard store Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ 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)
|
||||
@@ -20,30 +22,65 @@ function fmtHold(s: number) {
|
||||
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].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
|
||||
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<BotPerformance | null>(null)
|
||||
const [trades, setTrades] = useState<BotTrade[]>([])
|
||||
const [period, setPeriod] = useState('30d')
|
||||
const [period, setPeriod] = useState<Period>('30d')
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
getPerformance().catch(() => null),
|
||||
getTrades(200, 1).catch(() => []),
|
||||
getTrades(100, 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 filteredTrades = trades.filter((trade) => inPeriod(trade.closed_at, period))
|
||||
const totalPnl = filteredTrades.reduce((sum, trade) => sum + trade.pnl_usd, 0)
|
||||
const wins = filteredTrades.filter((trade) => trade.pnl_usd > 0).length
|
||||
const winRate = filteredTrades.length ? (wins / filteredTrades.length) * 100 : 0
|
||||
const bestTrade = filteredTrades.length ? Math.max(...filteredTrades.map(t => t.pnl_usd)) : 0
|
||||
const worstTrade = filteredTrades.length ? Math.min(...filteredTrades.map(t => t.pnl_usd)) : 0
|
||||
const avgTrade = filteredTrades.length ? filteredTrades.reduce((s, t) => s + t.pnl_usd, 0) / filteredTrades.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: 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: '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 },
|
||||
@@ -57,7 +94,7 @@ export default function AnalyticsPage() {
|
||||
<p className="page-sub">Deep dive into {period} of signals and trades.</p>
|
||||
</div>
|
||||
<div className="nav-tabs">
|
||||
{['7d', '30d', '90d', 'All'].map(p => (
|
||||
{(['7d', '30d', '90d', 'All'] as const).map(p => (
|
||||
<button key={p} className={`nav-tab ${period === p ? 'active' : ''}`} onClick={() => setPeriod(p)}>{p}</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -69,11 +106,11 @@ export default function AnalyticsPage() {
|
||||
<div>
|
||||
<div className="tiny">Performance · {period}</div>
|
||||
<div className="hero-value mono" style={{ marginTop: 6, fontSize: 40 }}>
|
||||
{perf ? fmtMoney(perf.net_pnl_usd) : '—'}
|
||||
{filteredTrades.length ? fmtMoney(summaryPnl) : '—'}
|
||||
</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>
|
||||
<span className={`chip ${winRate >= 50 ? 'up' : 'down'}`}>{winRate.toFixed(1)}% win rate</span>
|
||||
<span className="chip">{filteredTrades.length} trades</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,9 +128,13 @@ export default function AnalyticsPage() {
|
||||
</div>
|
||||
|
||||
{/* No data message */}
|
||||
{!perf && !trades.length && (
|
||||
{!filteredTrades.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>
|
||||
<p style={{ fontSize: 14 }}>
|
||||
{trades.length
|
||||
? `No trades closed in the ${period} window yet.`
|
||||
: 'No trade data yet. The bot will populate analytics once it starts executing.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user