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
co-authored by Claude Sonnet 4.6
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 = { export const metadata: Metadata = {
title: "Backtest Results", 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<{ 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; trades: number | null; wr: number; pf: number | null; ret: number | null; dd: number | null;
highlight: boolean; note?: string; 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 (~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: "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: "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: "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: "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: "1 Month", tf: "M5", risk: "5%", trades: 159, wr: 55.3, pf: 1.26, ret: 390, dd: -156, highlight: false }, { 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" },
{ 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 },
]; ];
const params = [ const params = [
["Timeframe", "M5"], ["Timeframe", "M5"],
["Symbols", "XAUUSDm · BTCUSDm"], ["Symbol", "XAUUSDm"],
["Session", "08:0013:00 UTC"], ["Session", "08:0013:00 UTC"],
["EMA Period", "20"], ["EMA Period", "20"],
["Min FVG Pips", "1"], ["ATR FVG Filter", "≥ 0.3× ATR(14)"],
["Min SL Pips", "5"], ["ATR Impulse Filter","≥ 1.0× ATR(14)"],
["Min RR", "1.5×"], ["Entry Zone", "75% into FVG"],
["FVG Expiry", "10 candles"], ["Min RR", "1.5×"],
["Body PCT Min", "60%"], ["FVG Expiry", "10 candles"],
["Close PCT Min", "80%"], ["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() { 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 ( return (
<> <>
<section className="pt-6 pb-12"> <section className="pt-6 pb-12">
@@ -40,27 +67,27 @@ export default function BacktestPage() {
Backtest Results Backtest Results
</h1> </h1>
<p className="text-[15px] text-ink-sub max-w-xl"> <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. London session filter (0813 UTC) applied.
</p> </p>
</section> </section>
{/* Highlight */} {/* 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 className="flex flex-col sm:flex-row sm:items-start gap-4 mb-8">
<div> <div>
<span className="status-pill status-bull mb-3 inline-block">Recommended</span> <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 · 1 Month · 1% Risk</h2> <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 · Exness Demo</p> <p className="text-sm text-ink-sub mt-1">XAUUSDm · Sep 2025 Jun 2026</p>
</div> </div>
<Link href="/trades" className="btn-primary sm:ml-auto shrink-0">View Live Trades </Link> <Link href="/trades" className="btn-primary sm:ml-auto shrink-0">View Live Trades </Link>
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-6 pt-8 border-t border-hl"> <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: "Profit Factor", value: "2.08", note: "> 2.0 = strong" },
{ label: "Net Return", value: "+43.2%", note: "on $600 balance" }, { label: "Net Return", value: "+448.7%", note: "$5,000 → $27,439" },
{ label: "Max Drawdown", value: "11.5%", note: "manageable" }, { label: "Win Rate", value: "62.7%", note: "83 trades" },
{ label: "Win Rate", value: "55.3%", note: "159 trades" }, { label: "vs v1.3.0", value: "+274pp", note: "v1.3.0 was +174.7%" },
].map(({ label, value, note }) => ( ].map(({ label, value, note }) => (
<div key={label}> <div key={label}>
<p className="text-xs text-ink-sub mb-1.5">{label}</p> <p className="text-xs text-ink-sub mb-1.5">{label}</p>
@@ -71,6 +98,145 @@ export default function BacktestPage() {
</div> </div>
</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 */} {/* Results table */}
<div className="rounded-lg border border-hl overflow-hidden mb-10"> <div className="rounded-lg border border-hl overflow-hidden mb-10">
<div className="px-6 py-4 border-b border-hl"> <div className="px-6 py-4 border-b border-hl">
@@ -80,7 +246,7 @@ export default function BacktestPage() {
<table className="data-table"> <table className="data-table">
<thead> <thead>
<tr> <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> <th key={h}>{h}</th>
))} ))}
</tr> </tr>
@@ -92,6 +258,7 @@ export default function BacktestPage() {
<td className="font-medium text-ink">{r.period}</td> <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.tf}</td>
<td className="font-mono text-ink-sub">{r.risk}</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.trades ?? "—"}</td>
<td className="font-mono text-ink-md">{r.wr.toFixed(1)}%</td> <td className="font-mono text-ink-md">{r.wr.toFixed(1)}%</td>
<td className="font-mono font-medium"> <td className="font-mono font-medium">
@@ -133,13 +300,13 @@ export default function BacktestPage() {
<div className="card"> <div className="card">
<p className="eyebrow mb-6">How It Works</p> <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%."], ["Detect Impulse", "3-candle momentum: body ≥ 50% of range, close in top/bottom 20%."],
["Find FVG", "Measure gap between candle 1 high and candle 3 low (bull) or reverse."], ["ATR Quality Gate", "FVG zone ≥ 0.3× ATR(14). Impulse body ≥ 1.0× ATR(14). Weak setups discarded."],
["EMA Filter", "Long only above EMA-20, short only below. Strict trend confirmation."], ["EMA-20 Filter", "Long only above EMA-20, short only below. Confirms short-term trend."],
["Place Limit", "Set limit at FVG midpoint. Auto-cancel after 10 candles if unfilled."], ["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, size at exactly 1% risk."], ["Manage Risk", "SL at FVG boundary, TP at ≥ 1.5× RR, position sized at exactly N% risk per trade."],
].map(([title, desc], i) => ( ].map(([title, desc], i) => (
<li key={i} className="flex gap-4"> <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"> <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; -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 ─────────────────────────────────────────────────────────────── */ /* ── Buttons ─────────────────────────────────────────────────────────────── */
.btn-primary { .btn-primary {
display: inline-flex; align-items: center; gap: 8px; 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 ( return (
<html lang="en"> <html lang="en">
<body className="min-h-screen antialiased"> <body className="min-h-screen antialiased">
<div className="relative z-10">
<Nav version={version} /> <Nav version={version} />
<main className="max-w-5xl mx-auto px-4 sm:px-6 py-12"> <main className="max-w-5xl mx-auto px-4 sm:px-6 py-12">
{children} {children}
@@ -109,6 +110,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
</div> </div>
</div> </div>
</footer> </footer>
</div>
</body> </body>
</html> </html>
); );
+21 -15
View File
@@ -12,8 +12,8 @@ export default function DashboardPage() {
</h1> </h1>
<p className="text-[18px] text-ink-md leading-relaxed max-w-2xl mb-8"> <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 Open-source algorithmic trading bot built in Rust. Momentum FVG scalper
with EMA trend filter, configurable session window, automatic position with ATR quality filter, EMA trend filter, configurable session window,
sizing, and Telegram alerts runs on any MT5 symbol. automatic position sizing, and Telegram alerts runs on any MT5 symbol.
</p> </p>
<div className="flex gap-3 flex-wrap"> <div className="flex gap-3 flex-wrap">
<Link href="/trades" className="btn-primary">View Trades</Link> <Link href="/trades" className="btn-primary">View Trades</Link>
@@ -27,21 +27,26 @@ export default function DashboardPage() {
{/* Strategy */} {/* Strategy */}
<section className="mb-16"> <section className="mb-16">
<p className="eyebrow mb-6">Strategy</p> <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 <FeatureCard
icon="◈" icon="◈"
title="Momentum FVG" 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 <FeatureCard
icon="◎" icon="◎"
title="EMA-20 Filter" title="EMA-20 + Zone 75%"
body="Long only above EMA-20, short only below. Eliminates counter-trend signals entirely." body="Long above EMA-20, short below. Limit order placed 75% deep into the FVG for better confirmation."
/> />
<FeatureCard <FeatureCard
icon="◉" icon="◉"
title="Risk Management" 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> </div>
</section> </section>
@@ -50,11 +55,11 @@ export default function DashboardPage() {
<section className="card-featured p-10 mb-16"> <section className="card-featured p-10 mb-16">
<div className="flex flex-col sm:flex-row sm:items-center gap-6 mb-8"> <div className="flex flex-col sm:flex-row sm:items-center gap-6 mb-8">
<div> <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"> <h2 className="text-2xl font-semibold tracking-tight-sm text-ink">
M5 · 1 Month · 1% Risk M5 · 50k Bars · 5% Risk
</h2> </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> </div>
<Link href="/backtest" className="btn-primary sm:ml-auto shrink-0"> <Link href="/backtest" className="btn-primary sm:ml-auto shrink-0">
Full Results Full Results
@@ -62,14 +67,15 @@ export default function DashboardPage() {
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-6 pt-6 border-t border-hl"> <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: "Profit Factor", value: "2.08", sub: "vs 1.17 in v1.3.0" },
{ label: "Net Return", value: "+43.2%" }, { label: "Net Return", value: "+448.7%", sub: "$5k → $27,439" },
{ label: "Max Drawdown", value: "11.5%" }, { label: "Win Rate", value: "62.7%", sub: "vs 50% in v1.3.0" },
{ label: "Win Rate", value: "55.3%" }, { label: "Max Drawdown", value: "$2,669", sub: "vs $5,482 in v1.3.0" },
].map(({ label, value }) => ( ].map(({ label, value, sub }) => (
<div key={label}> <div key={label}>
<p className="text-xs text-ink-sub mb-1.5">{label}</p> <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="font-mono text-xl font-semibold tracking-tight-md">{value}</p>
<p className="text-xs text-ink-ter mt-1">{sub}</p>
</div> </div>
))} ))}
</div> </div>
+137 -22
View File
@@ -18,22 +18,39 @@ export default async function TradesPage() {
let error = false; let error = false;
try { try {
[account, deals] = await Promise.all([ [account, deals] = await Promise.all([getAccount(), getAllDeals()]);
getAccount(),
getAllDeals(),
]);
} catch { error = true; } } catch { error = true; }
const aresDeals = deals.filter(d => d.magic === MAGIC && d.type !== 2); const aresDeals = deals.filter(d => d.magic === MAGIC && d.type !== 2);
const closed = aresDeals.filter(d => d.entry === 1); const closed = aresDeals.filter(d => d.entry === 1);
const wins = closed.filter(d => d.profit > 0); const wins = closed.filter(d => d.profit > 0);
const losses = 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 netPnl = closed.reduce((s, d) => s + d.profit + d.swap + d.commission, 0);
const grossW = wins.reduce((s, d) => s + d.profit, 0); const totalFriction = closed.reduce((s, d) => s + d.swap + d.commission, 0);
const grossL = Math.abs(losses.reduce((s, d) => s + d.profit, 0)); const grossW = wins.reduce((s, d) => s + d.profit, 0);
const pf = grossL > 0 ? grossW / grossL : 0; const grossL = Math.abs(losses.reduce((s, d) => s + d.profit, 0));
const wr = closed.length > 0 ? (wins.length / closed.length) * 100 : 0; const pf = grossL > 0 ? grossW / grossL : 0;
const currency = account?.currency ?? "USD"; 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 ( return (
<> <>
@@ -58,13 +75,12 @@ export default async function TradesPage() {
) : ( ) : (
<> <>
{/* Stats */} {/* 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: "Total Trades", value: closed.length.toString(), mono: true },
{ label: "Win Rate", value: `${wr.toFixed(1)}%`, mono: true }, { label: "Win Rate", value: `${wr.toFixed(1)}%`, mono: true },
{ label: "Profit Factor",value: pf > 0 ? pf.toFixed(2) : "—", 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: "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 },
].map(({ label, value, mono, colored }) => ( ].map(({ label, value, mono, colored }) => (
<div key={label} className="stat-tile"> <div key={label} className="stat-tile">
<p className="text-xs text-ink-sub mb-2">{label}</p> <p className="text-xs text-ink-sub mb-2">{label}</p>
@@ -75,6 +91,103 @@ export default async function TradesPage() {
))} ))}
</div> </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 */} {/* Equity curve */}
<div className="card mb-10"> <div className="card mb-10">
<p className="eyebrow mb-6">Equity Curve</p> <p className="eyebrow mb-6">Equity Curve</p>
@@ -83,8 +196,9 @@ export default async function TradesPage() {
{/* Trade table */} {/* Trade table */}
<div className="rounded-lg border border-hl overflow-hidden"> <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="eyebrow">Closed Trades</p>
<p className="text-xs text-ink-ter font-mono">{closed.length} trades</p>
</div> </div>
{closed.length === 0 ? ( {closed.length === 0 ? (
<div className="text-center text-ink-sub text-sm py-12">No closed trades yet.</div> <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>
<td className="font-mono text-ink-md">{d.volume}</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 text-ink-md">{d.price.toFixed(2)}</td>
<td className={`font-mono font-semibold ${d.profit >= 0 ? "text-bull" : "text-bear"}`}> <td>
{formatCurrency(d.profit, currency)} <span className={`font-mono font-semibold ${d.profit >= 0 ? "text-bull" : "text-bear"}`}>
{formatCurrency(d.profit, currency)}
</span>
</td> </td>
</tr> </tr>
))} ))}
@@ -125,4 +241,3 @@ export default async function TradesPage() {
</> </>
); );
} }