mirror of
https://github.com/RomySaputraSihananda/ares.git
synced 2026-07-27 18:47:53 +00:00
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:
+200
-33
@@ -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 08–13 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 08–13 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 08–13 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 08–13 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 08–13 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:00–13: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:00–13: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 (08–13 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 08–13 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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 08–13 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
@@ -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() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user