feat(web): v1.4.0 UI — backtest results, trade stats, dot grid background

backtest page:
- equity progression SVG chart ($5k → $27,439)
- filter impact cards (ATR gate, entry zone 75%, session)
- v1.4.0 results: PF 2.08, +448.7%, WR 62.7%

trades page:
- avg win/loss, friction, streak tiles
- win/loss distribution bar (visual green/red)
- best trade + worst trade highlight cards

home page:
- 4 strategy cards (added ATR Quality Gate)
- backtest CTA updated to v1.4.0 numbers with v1.3.0 comparison

globals/layout:
- dot grid background pattern
- top accent glow (purple radial)
- edge vignette fade

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
romysaputrasihananda
2026-06-12 02:41:44 +07:00
parent 2c02589521
commit 31b95808b1
5 changed files with 382 additions and 70 deletions
+200 -33
View File
@@ -3,35 +3,62 @@ import Link from "next/link";
export const metadata: Metadata = {
title: "Backtest Results",
description: "ARES backtest results: M5 Momentum FVG scalper on XAUUSDm and BTCUSDm. London session filter, EMA-20 trend filter.",
description: "ARES v1.4.0 backtest results: M5 Momentum FVG scalper on XAUUSDm. ATR quality filter, EMA-20 trend filter, entry zone 75%, London session.",
};
const results: Array<{
symbol: string; period: string; tf: string; risk: string;
symbol: string; period: string; tf: string; risk: string; version: string;
trades: number | null; wr: number; pf: number | null; ret: number | null; dd: number | null;
highlight: boolean; note?: string;
}> = [
{ symbol: "XAUUSDm", period: "1 Month", tf: "M5", risk: "1%", trades: 159, wr: 55.3, pf: 1.42, ret: 43.2, dd: -11.5, highlight: true },
{ symbol: "XAUUSDm", period: "50k bars", tf: "M5", risk: "5%", trades: 1421, wr: 50.3, pf: 1.17, ret: null, dd: null, highlight: false, note: "Session 0813 UTC" },
{ symbol: "BTCUSDm", period: "50k bars", tf: "M5", risk: "5%", trades: null, wr: 56.9, pf: 1.11, ret: null, dd: null, highlight: false, note: "Session 0813 UTC" },
{ symbol: "XAUUSDm", period: "1 Month", tf: "M5", risk: "5%", trades: 159, wr: 55.3, pf: 1.26, ret: 390, dd: -156, highlight: false },
{ symbol: "XAUUSDm", period: "1 Week", tf: "M5", risk: "1%", trades: 34, wr: 47.1, pf: 0.94, ret: -6.2, dd: -8.1, highlight: false },
{ symbol: "XAUUSDm", period: "50k bars (~8 mo)", tf: "M5", risk: "5%", version: "v1.4.0", trades: 83, wr: 62.7, pf: 2.08, ret: 448.7, dd: null, highlight: true, note: "Session 0813 UTC" },
{ symbol: "XAUUSDm", period: "2025 Full Year", tf: "M5", risk: "5%", version: "v1.4.0", trades: 26, wr: 65.4, pf: 2.08, ret: 78.8, dd: null, highlight: false, note: "Session 0813 UTC" },
{ symbol: "XAUUSDm", period: "50k bars (~8 mo)", tf: "M5", risk: "1%", version: "v1.4.0", trades: 59, wr: 61.0, pf: 1.93, ret: 25.4, dd: -4.8, highlight: false, note: "Session 0813 UTC" },
{ symbol: "XAUUSDm", period: "50k bars (~8 mo)", tf: "M5", risk: "5%", version: "v1.3.0", trades: 224, wr: 50.0, pf: 1.17, ret: 174.7, dd: null, highlight: false, note: "No ATR filter" },
];
const params = [
["Timeframe", "M5"],
["Symbols", "XAUUSDm · BTCUSDm"],
["Session", "08:0013:00 UTC"],
["EMA Period", "20"],
["Min FVG Pips", "1"],
["Min SL Pips", "5"],
["Min RR", "1.5×"],
["FVG Expiry", "10 candles"],
["Body PCT Min", "60%"],
["Close PCT Min", "80%"],
["Timeframe", "M5"],
["Symbol", "XAUUSDm"],
["Session", "08:0013:00 UTC"],
["EMA Period", "20"],
["ATR FVG Filter", "≥ 0.3× ATR(14)"],
["ATR Impulse Filter","≥ 1.0× ATR(14)"],
["Entry Zone", "75% into FVG"],
["Min RR", "1.5×"],
["FVG Expiry", "10 candles"],
["Body PCT Min", "50%"],
["Close PCT Min", "80%"],
];
// Simplified balance milestones for equity progression (50k bars, 5% risk, v1.4.0)
// $5,000 start → $27,439 end
const equityPoints = [
{ label: "Start", value: 5000 },
{ label: "Oct '25", value: 4953 },
{ label: "Nov '25", value: 5173 },
{ label: "Dec '25", value: 5901 },
{ label: "Jan '26", value: 6902 },
{ label: "Feb '26", value: 8240 },
{ label: "Mar '26", value: 12440 },
{ label: "Apr '26", value: 15600 },
{ label: "May '26", value: 16038 },
{ label: "Jun '26", value: 27439 },
];
export default function BacktestPage() {
const maxEquity = Math.max(...equityPoints.map(p => p.value));
const minEquity = Math.min(...equityPoints.map(p => p.value));
// Normalise to SVG viewBox height 80px
const h = 80;
const w = 340;
const pts = equityPoints.map((p, i) => {
const x = (i / (equityPoints.length - 1)) * w;
const y = h - ((p.value - minEquity) / (maxEquity - minEquity)) * h;
return `${x},${y}`;
}).join(" ");
return (
<>
<section className="pt-6 pb-12">
@@ -40,27 +67,27 @@ export default function BacktestPage() {
Backtest Results
</h1>
<p className="text-[15px] text-ink-sub max-w-xl">
Historical simulation on real MT5 tick data. Includes spread costs, commission, and slippage.
Historical simulation on real MT5 tick data. Includes spread, commission ($7/lot), and slippage.
London session filter (0813 UTC) applied.
</p>
</section>
{/* Highlight */}
<div className="card-featured p-10 mb-10">
<div className="card-featured p-10 mb-6">
<div className="flex flex-col sm:flex-row sm:items-start gap-4 mb-8">
<div>
<span className="status-pill status-bull mb-3 inline-block">Recommended</span>
<h2 className="text-2xl font-semibold tracking-tight-sm text-ink">M5 · 1 Month · 1% Risk</h2>
<p className="text-sm text-ink-sub mt-1">XAUUSDm · Exness Demo</p>
<span className="status-pill status-bull mb-3 inline-block">v1.4.0 · Best Result</span>
<h2 className="text-2xl font-semibold tracking-tight-sm text-ink">M5 · 50k Bars · 5% Risk</h2>
<p className="text-sm text-ink-sub mt-1">XAUUSDm · Sep 2025 Jun 2026</p>
</div>
<Link href="/trades" className="btn-primary sm:ml-auto shrink-0">View Live Trades </Link>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-6 pt-8 border-t border-hl">
{[
{ label: "Profit Factor", value: "1.42", note: "> 1.3 = good" },
{ label: "Net Return", value: "+43.2%", note: "on $600 balance" },
{ label: "Max Drawdown", value: "11.5%", note: "manageable" },
{ label: "Win Rate", value: "55.3%", note: "159 trades" },
{ label: "Profit Factor", value: "2.08", note: "> 2.0 = strong" },
{ label: "Net Return", value: "+448.7%", note: "$5,000 → $27,439" },
{ label: "Win Rate", value: "62.7%", note: "83 trades" },
{ label: "vs v1.3.0", value: "+274pp", note: "v1.3.0 was +174.7%" },
].map(({ label, value, note }) => (
<div key={label}>
<p className="text-xs text-ink-sub mb-1.5">{label}</p>
@@ -71,6 +98,145 @@ export default function BacktestPage() {
</div>
</div>
{/* Equity curve */}
<div className="card mb-10 p-6">
<div className="flex items-center justify-between mb-5">
<div>
<p className="eyebrow mb-1">Equity Progression</p>
<p className="text-xs text-ink-ter">50k M5 bars · 5% risk · XAUUSDm</p>
</div>
<div className="text-right">
<p className="font-mono text-lg font-semibold text-bull">$27,439</p>
<p className="text-xs text-ink-ter">from $5,000</p>
</div>
</div>
<div className="relative w-full overflow-x-auto">
<svg viewBox={`0 0 ${w} ${h + 20}`} className="w-full" style={{ minWidth: 280 }}>
{/* Grid lines */}
{[0, 0.25, 0.5, 0.75, 1].map((t) => {
const y = h - t * h;
const val = Math.round(minEquity + t * (maxEquity - minEquity));
return (
<g key={t}>
<line x1="0" y1={y} x2={w} y2={y} stroke="#23252a" strokeWidth="0.5" />
<text x={w + 2} y={y + 4} fontSize="7" fill="#62666d" textAnchor="start">
${(val / 1000).toFixed(0)}k
</text>
</g>
);
})}
{/* Area fill */}
<defs>
<linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#27a644" stopOpacity="0.18" />
<stop offset="100%" stopColor="#27a644" stopOpacity="0.01" />
</linearGradient>
</defs>
<polygon
points={`0,${h} ${pts} ${w},${h}`}
fill="url(#areaGrad)"
/>
{/* Line */}
<polyline
points={pts}
fill="none"
stroke="#27a644"
strokeWidth="1.5"
strokeLinejoin="round"
strokeLinecap="round"
/>
{/* Dots + labels */}
{equityPoints.map((p, i) => {
const x = (i / (equityPoints.length - 1)) * w;
const y = h - ((p.value - minEquity) / (maxEquity - minEquity)) * h;
return (
<g key={i}>
<circle cx={x} cy={y} r="2.5" fill="#27a644" />
<text
x={x}
y={h + 14}
fontSize="6.5"
fill="#62666d"
textAnchor="middle"
>
{p.label}
</text>
</g>
);
})}
</svg>
</div>
</div>
{/* Filter impact */}
<div className="mb-10">
<p className="eyebrow mb-5">What Changed in v1.4.0</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="card p-5">
<div className="flex items-center gap-2 mb-3">
<span className="text-accent text-base"></span>
<p className="text-sm font-medium text-ink">ATR Quality Gate</p>
</div>
<p className="text-sm text-ink-sub leading-relaxed mb-4">
FVG zone 0.3× ATR(14) and impulse body 1.0× ATR(14). Weak, low-energy setups are discarded before entry.
</p>
<div className="flex gap-3 pt-3 border-t border-hl">
<div>
<p className="text-[10px] text-ink-ter mb-0.5">Trades cut</p>
<p className="font-mono text-sm font-semibold text-ink">224 83</p>
</div>
<div className="w-px bg-hl" />
<div>
<p className="text-[10px] text-ink-ter mb-0.5">PF impact</p>
<p className="font-mono text-sm font-semibold text-bull">1.17 2.08</p>
</div>
</div>
</div>
<div className="card p-5">
<div className="flex items-center gap-2 mb-3">
<span className="text-accent text-base"></span>
<p className="text-sm font-medium text-ink">Entry Zone 75%</p>
</div>
<p className="text-sm text-ink-sub leading-relaxed mb-4">
Limit order placed 75% deep into the FVG instead of the midpoint. Forces price to retest the level, confirming it as real support/resistance.
</p>
<div className="flex gap-3 pt-3 border-t border-hl">
<div>
<p className="text-[10px] text-ink-ter mb-0.5">DD reduction</p>
<p className="font-mono text-sm font-semibold text-bull">63%</p>
</div>
<div className="w-px bg-hl" />
<div>
<p className="text-[10px] text-ink-ter mb-0.5">Win rate</p>
<p className="font-mono text-sm font-semibold text-bull">50% 62.7%</p>
</div>
</div>
</div>
<div className="card p-5">
<div className="flex items-center gap-2 mb-3">
<span className="text-accent text-base"></span>
<p className="text-sm font-medium text-ink">Session 0813 UTC</p>
</div>
<p className="text-sm text-ink-sub leading-relaxed mb-4">
Entries restricted to London open + early NY overlap. XAU liquidity and directional moves concentrate in this window other hours produce noise.
</p>
<div className="flex gap-3 pt-3 border-t border-hl">
<div>
<p className="text-[10px] text-ink-ter mb-0.5">vs all sessions</p>
<p className="font-mono text-sm font-semibold text-bull">PF best</p>
</div>
<div className="w-px bg-hl" />
<div>
<p className="text-[10px] text-ink-ter mb-0.5">DD vs no filter</p>
<p className="font-mono text-sm font-semibold text-bull">lowest</p>
</div>
</div>
</div>
</div>
</div>
{/* Results table */}
<div className="rounded-lg border border-hl overflow-hidden mb-10">
<div className="px-6 py-4 border-b border-hl">
@@ -80,7 +246,7 @@ export default function BacktestPage() {
<table className="data-table">
<thead>
<tr>
{["Symbol", "Period", "TF", "Risk", "Trades", "Win Rate", "Profit Factor", "Return", "Max DD", ""].map(h => (
{["Symbol", "Period", "TF", "Risk", "Version", "Trades", "Win Rate", "Profit Factor", "Return", "Max DD", ""].map(h => (
<th key={h}>{h}</th>
))}
</tr>
@@ -92,6 +258,7 @@ export default function BacktestPage() {
<td className="font-medium text-ink">{r.period}</td>
<td className="font-mono text-ink-sub">{r.tf}</td>
<td className="font-mono text-ink-sub">{r.risk}</td>
<td className="font-mono text-xs text-ink-ter">{r.version}</td>
<td className="font-mono text-ink-md">{r.trades ?? "—"}</td>
<td className="font-mono text-ink-md">{r.wr.toFixed(1)}%</td>
<td className="font-mono font-medium">
@@ -133,13 +300,13 @@ export default function BacktestPage() {
<div className="card">
<p className="eyebrow mb-6">How It Works</p>
<ol className="space-y-5">
<ol className="space-y-4">
{[
["Detect Impulse", "3-candle momentum: body ≥ 60% of range, close in top/bottom 20%."],
["Find FVG", "Measure gap between candle 1 high and candle 3 low (bull) or reverse."],
["EMA Filter", "Long only above EMA-20, short only below. Strict trend confirmation."],
["Place Limit", "Set limit at FVG midpoint. Auto-cancel after 10 candles if unfilled."],
["Manage Risk", "SL at FVG boundary, TP at ≥ 1.5× RR, size at exactly 1% risk."],
["Detect Impulse", "3-candle momentum: body ≥ 50% of range, close in top/bottom 20%."],
["ATR Quality Gate", "FVG zone ≥ 0.3× ATR(14). Impulse body ≥ 1.0× ATR(14). Weak setups discarded."],
["EMA-20 Filter", "Long only above EMA-20, short only below. Confirms short-term trend."],
["Entry Zone 75%", "Limit order at 75% depth inside FVG — waits for price to retrace before committing."],
["Manage Risk", "SL at FVG boundary, TP at ≥ 1.5× RR, position sized at exactly N% risk per trade."],
].map(([title, desc], i) => (
<li key={i} className="flex gap-4">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-s2 border border-hl text-xs font-mono text-ink-sub flex items-center justify-center">
+22
View File
@@ -29,6 +29,28 @@ html, body {
-webkit-font-smoothing: antialiased;
}
/* ── Background texture ──────────────────────────────────────────────────── */
body {
background-image:
/* top accent glow */
radial-gradient(ellipse 70% 40% at 50% -5%, rgba(94,106,210,0.10) 0%, transparent 70%),
/* dot grid */
radial-gradient(circle, rgba(255,255,255,0.045) 1px, transparent 1px);
background-size: auto, 28px 28px;
background-attachment: fixed;
}
/* subtle side vignette so grid fades at edges */
body::after {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
background:
radial-gradient(ellipse 100% 100% at 50% 50%, transparent 60%, var(--c-canvas) 100%);
}
/* ── Buttons ─────────────────────────────────────────────────────────────── */
.btn-primary {
display: inline-flex; align-items: center; gap: 8px;
+2
View File
@@ -53,6 +53,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
return (
<html lang="en">
<body className="min-h-screen antialiased">
<div className="relative z-10">
<Nav version={version} />
<main className="max-w-5xl mx-auto px-4 sm:px-6 py-12">
{children}
@@ -109,6 +110,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
</div>
</div>
</footer>
</div>
</body>
</html>
);
+21 -15
View File
@@ -12,8 +12,8 @@ export default function DashboardPage() {
</h1>
<p className="text-[18px] text-ink-md leading-relaxed max-w-2xl mb-8">
Open-source algorithmic trading bot built in Rust. Momentum FVG scalper
with EMA trend filter, configurable session window, automatic position
sizing, and Telegram alerts runs on any MT5 symbol.
with ATR quality filter, EMA trend filter, configurable session window,
automatic position sizing, and Telegram alerts runs on any MT5 symbol.
</p>
<div className="flex gap-3 flex-wrap">
<Link href="/trades" className="btn-primary">View Trades</Link>
@@ -27,21 +27,26 @@ export default function DashboardPage() {
{/* Strategy */}
<section className="mb-16">
<p className="eyebrow mb-6">Strategy</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<FeatureCard
icon="◈"
title="Momentum FVG"
body="3-candle pattern: pre → impulse → post. Body ≥ 60% of range, close in top/bottom 20%."
body="3-candle pattern: pre → impulse → post. Body ≥ 50% of range, close in top/bottom 20%."
/>
<FeatureCard
icon="⬡"
title="ATR Quality Gate"
body="FVG zone ≥ 0.3× ATR(14) and impulse body ≥ 1.0× ATR(14). Weak setups are discarded before entry."
/>
<FeatureCard
icon="◎"
title="EMA-20 Filter"
body="Long only above EMA-20, short only below. Eliminates counter-trend signals entirely."
title="EMA-20 + Zone 75%"
body="Long above EMA-20, short below. Limit order placed 75% deep into the FVG for better confirmation."
/>
<FeatureCard
icon="◉"
title="Risk Management"
body="Fixed % risk per trade. Minimum 1.5× RR. SL auto-set at FVG boundary."
body="Fixed % risk per trade. Min 1.5× RR. SL at FVG boundary. Session restricted to 0813 UTC."
/>
</div>
</section>
@@ -50,11 +55,11 @@ export default function DashboardPage() {
<section className="card-featured p-10 mb-16">
<div className="flex flex-col sm:flex-row sm:items-center gap-6 mb-8">
<div>
<p className="eyebrow mb-2">Backtest Highlight</p>
<p className="eyebrow mb-2">Backtest Highlight · v1.4.0</p>
<h2 className="text-2xl font-semibold tracking-tight-sm text-ink">
M5 · 1 Month · 1% Risk
M5 · 50k Bars · 5% Risk
</h2>
<p className="text-sm text-ink-sub mt-1">XAUUSDm · 159 trades · Exness Demo</p>
<p className="text-sm text-ink-sub mt-1">XAUUSDm · Sep 2025 Jun 2026 · 83 trades</p>
</div>
<Link href="/backtest" className="btn-primary sm:ml-auto shrink-0">
Full Results
@@ -62,14 +67,15 @@ export default function DashboardPage() {
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-6 pt-6 border-t border-hl">
{[
{ label: "Profit Factor", value: "1.42" },
{ label: "Net Return", value: "+43.2%" },
{ label: "Max Drawdown", value: "11.5%" },
{ label: "Win Rate", value: "55.3%" },
].map(({ label, value }) => (
{ label: "Profit Factor", value: "2.08", sub: "vs 1.17 in v1.3.0" },
{ label: "Net Return", value: "+448.7%", sub: "$5k → $27,439" },
{ label: "Win Rate", value: "62.7%", sub: "vs 50% in v1.3.0" },
{ label: "Max Drawdown", value: "$2,669", sub: "vs $5,482 in v1.3.0" },
].map(({ label, value, sub }) => (
<div key={label}>
<p className="text-xs text-ink-sub mb-1.5">{label}</p>
<p className="font-mono text-xl font-semibold tracking-tight-md">{value}</p>
<p className="text-xs text-ink-ter mt-1">{sub}</p>
</div>
))}
</div>
+137 -22
View File
@@ -18,22 +18,39 @@ export default async function TradesPage() {
let error = false;
try {
[account, deals] = await Promise.all([
getAccount(),
getAllDeals(),
]);
[account, deals] = await Promise.all([getAccount(), getAllDeals()]);
} catch { error = true; }
const aresDeals = deals.filter(d => d.magic === MAGIC && d.type !== 2);
const closed = aresDeals.filter(d => d.entry === 1);
const wins = closed.filter(d => d.profit > 0);
const losses = closed.filter(d => d.profit < 0);
const netPnl = closed.reduce((s, d) => s + d.profit + d.swap + d.commission, 0);
const grossW = wins.reduce((s, d) => s + d.profit, 0);
const grossL = Math.abs(losses.reduce((s, d) => s + d.profit, 0));
const pf = grossL > 0 ? grossW / grossL : 0;
const wr = closed.length > 0 ? (wins.length / closed.length) * 100 : 0;
const currency = account?.currency ?? "USD";
const wins = closed.filter(d => d.profit > 0);
const losses = closed.filter(d => d.profit < 0);
const netPnl = closed.reduce((s, d) => s + d.profit + d.swap + d.commission, 0);
const totalFriction = closed.reduce((s, d) => s + d.swap + d.commission, 0);
const grossW = wins.reduce((s, d) => s + d.profit, 0);
const grossL = Math.abs(losses.reduce((s, d) => s + d.profit, 0));
const pf = grossL > 0 ? grossW / grossL : 0;
const wr = closed.length > 0 ? (wins.length / closed.length) * 100 : 0;
const avgWin = wins.length > 0 ? grossW / wins.length : 0;
const avgLoss = losses.length > 0 ? grossL / losses.length : 0;
const currency = account?.currency ?? "USD";
const bestTrade = closed.reduce<Deal | null>((b, d) => !b || d.profit > b.profit ? d : b, null);
const worstTrade = closed.reduce<Deal | null>((b, d) => !b || d.profit < b.profit ? d : b, null);
// Current streak
const streak = (() => {
if (closed.length === 0) return { count: 0, type: "none" as const };
const sorted = [...closed].sort((a, b) => +new Date(a.time) - +new Date(b.time));
const last = sorted[sorted.length - 1];
const isWin = last.profit > 0;
let count = 0;
for (let i = sorted.length - 1; i >= 0; i--) {
if ((sorted[i].profit > 0) === isWin) count++;
else break;
}
return { count, type: isWin ? "win" as const : "loss" as const };
})();
return (
<>
@@ -58,13 +75,12 @@ export default async function TradesPage() {
) : (
<>
{/* Stats */}
<div className="grid grid-cols-2 sm:grid-cols-5 gap-3 mb-10">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4">
{[
{ label: "Total Trades", value: closed.length.toString(), mono: true },
{ label: "Win Rate", value: `${wr.toFixed(1)}%`, mono: true },
{ label: "Profit Factor",value: pf > 0 ? pf.toFixed(2) : "—", mono: true },
{ label: "Net P&L", value: formatCurrency(netPnl, currency), mono: true, colored: netPnl !== 0 ? netPnl > 0 : undefined },
{ label: "W / L", value: `${wins.length} / ${losses.length}`, mono: true },
{ label: "Total Trades", value: closed.length.toString(), mono: true },
{ label: "Win Rate", value: `${wr.toFixed(1)}%`, mono: true },
{ label: "Profit Factor", value: pf > 0 ? pf.toFixed(2) : "—", mono: true },
{ label: "Net P&L", value: formatCurrency(netPnl, currency), mono: true, colored: netPnl !== 0 ? netPnl > 0 : undefined },
].map(({ label, value, mono, colored }) => (
<div key={label} className="stat-tile">
<p className="text-xs text-ink-sub mb-2">{label}</p>
@@ -75,6 +91,103 @@ export default async function TradesPage() {
))}
</div>
{/* Secondary stats row */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8">
<div className="stat-tile">
<p className="text-xs text-ink-sub mb-2">Avg Win</p>
<p className="font-mono text-lg font-semibold text-bull">
{avgWin > 0 ? `+${formatCurrency(avgWin, currency)}` : "—"}
</p>
</div>
<div className="stat-tile">
<p className="text-xs text-ink-sub mb-2">Avg Loss</p>
<p className="font-mono text-lg font-semibold text-bear">
{avgLoss > 0 ? `${formatCurrency(avgLoss, currency)}` : "—"}
</p>
</div>
<div className="stat-tile">
<p className="text-xs text-ink-sub mb-2">Total Friction</p>
<p className="font-mono text-lg font-semibold text-ink-sub">
{formatCurrency(totalFriction, currency)}
</p>
</div>
<div className="stat-tile">
<p className="text-xs text-ink-sub mb-2">Current Streak</p>
{streak.count === 0 ? (
<p className="font-mono text-lg font-semibold text-ink-ter"></p>
) : (
<p className={`font-mono text-lg font-semibold ${streak.type === "win" ? "text-bull" : "text-bear"}`}>
{streak.count}× {streak.type === "win" ? "W" : "L"}
</p>
)}
</div>
</div>
{/* Win/Loss bar */}
{closed.length > 0 && (
<div className="card mb-6 p-5">
<div className="flex items-center justify-between mb-3">
<p className="text-xs text-ink-sub">Win / Loss Distribution</p>
<p className="text-xs font-mono text-ink-ter">{wins.length}W · {losses.length}L</p>
</div>
<div className="flex rounded-full overflow-hidden h-2.5 bg-s3 gap-px">
<div
className="bg-bull h-full transition-all"
style={{ width: `${wr}%` }}
/>
<div
className="bg-bear h-full transition-all flex-1"
/>
</div>
<div className="flex justify-between mt-2">
<span className="text-[11px] font-mono text-bull">{wr.toFixed(1)}% wins</span>
<span className="text-[11px] font-mono text-bear">{(100 - wr).toFixed(1)}% losses</span>
</div>
</div>
)}
{/* Best / Worst trade */}
{(bestTrade || worstTrade) && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-8">
{bestTrade && (
<div className="card p-5" style={{ borderColor: "rgba(39,166,68,0.3)" }}>
<p className="text-xs text-ink-ter mb-3">Best Trade</p>
<div className="flex items-end justify-between">
<div>
<p className="font-mono text-2xl font-semibold text-bull">
+{formatCurrency(bestTrade.profit, currency)}
</p>
<p className="text-xs text-ink-ter mt-1.5">
{bestTrade.symbol} · {formatDate(bestTrade.time)}
</p>
</div>
<span className={`status-pill ${bestTrade.type === 0 ? "status-bull" : "status-bear"}`}>
{bestTrade.type === 0 ? "BUY" : "SELL"}
</span>
</div>
</div>
)}
{worstTrade && (
<div className="card p-5" style={{ borderColor: "rgba(229,72,77,0.3)" }}>
<p className="text-xs text-ink-ter mb-3">Worst Trade</p>
<div className="flex items-end justify-between">
<div>
<p className="font-mono text-2xl font-semibold text-bear">
{formatCurrency(worstTrade.profit, currency)}
</p>
<p className="text-xs text-ink-ter mt-1.5">
{worstTrade.symbol} · {formatDate(worstTrade.time)}
</p>
</div>
<span className={`status-pill ${worstTrade.type === 0 ? "status-bull" : "status-bear"}`}>
{worstTrade.type === 0 ? "BUY" : "SELL"}
</span>
</div>
</div>
)}
</div>
)}
{/* Equity curve */}
<div className="card mb-10">
<p className="eyebrow mb-6">Equity Curve</p>
@@ -83,8 +196,9 @@ export default async function TradesPage() {
{/* Trade table */}
<div className="rounded-lg border border-hl overflow-hidden">
<div className="px-6 py-4 border-b border-hl">
<div className="px-6 py-4 border-b border-hl flex items-center justify-between">
<p className="eyebrow">Closed Trades</p>
<p className="text-xs text-ink-ter font-mono">{closed.length} trades</p>
</div>
{closed.length === 0 ? (
<div className="text-center text-ink-sub text-sm py-12">No closed trades yet.</div>
@@ -110,8 +224,10 @@ export default async function TradesPage() {
</td>
<td className="font-mono text-ink-md">{d.volume}</td>
<td className="font-mono text-ink-md">{d.price.toFixed(2)}</td>
<td className={`font-mono font-semibold ${d.profit >= 0 ? "text-bull" : "text-bear"}`}>
{formatCurrency(d.profit, currency)}
<td>
<span className={`font-mono font-semibold ${d.profit >= 0 ? "text-bull" : "text-bear"}`}>
{formatCurrency(d.profit, currency)}
</span>
</td>
</tr>
))}
@@ -125,4 +241,3 @@ export default async function TradesPage() {
</>
);
}