diff --git a/web/app/backtest/page.tsx b/web/app/backtest/page.tsx index 9ef3d61..bad153e 100644 --- a/web/app/backtest/page.tsx +++ b/web/app/backtest/page.tsx @@ -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 ( <>
@@ -40,27 +67,27 @@ export default function BacktestPage() { Backtest Results

- 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.

{/* Highlight */} -
+
- Recommended -

M5 · 1 Month · 1% Risk

-

XAUUSDm · Exness Demo

+ v1.4.0 · Best Result +

M5 · 50k Bars · 5% Risk

+

XAUUSDm · Sep 2025 – Jun 2026

View Live Trades →
{[ - { 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 }) => (

{label}

@@ -71,6 +98,145 @@ export default function BacktestPage() {
+ {/* Equity curve */} +
+
+
+

Equity Progression

+

50k M5 bars · 5% risk · XAUUSDm

+
+
+

$27,439

+

from $5,000

+
+
+
+ + {/* 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 ( + + + + ${(val / 1000).toFixed(0)}k + + + ); + })} + {/* Area fill */} + + + + + + + + {/* Line */} + + {/* Dots + labels */} + {equityPoints.map((p, i) => { + const x = (i / (equityPoints.length - 1)) * w; + const y = h - ((p.value - minEquity) / (maxEquity - minEquity)) * h; + return ( + + + + {p.label} + + + ); + })} + +
+
+ + {/* Filter impact */} +
+

What Changed in v1.4.0

+
+
+
+ +

ATR Quality Gate

+
+

+ FVG zone ≥ 0.3× ATR(14) and impulse body ≥ 1.0× ATR(14). Weak, low-energy setups are discarded before entry. +

+
+
+

Trades cut

+

224 → 83

+
+
+
+

PF impact

+

1.17 → 2.08

+
+
+
+ +
+
+ +

Entry Zone 75%

+
+

+ Limit order placed 75% deep into the FVG instead of the midpoint. Forces price to retest the level, confirming it as real support/resistance. +

+
+
+

DD reduction

+

−63%

+
+
+
+

Win rate

+

50% → 62.7%

+
+
+
+ +
+
+ +

Session 08–13 UTC

+
+

+ Entries restricted to London open + early NY overlap. XAU liquidity and directional moves concentrate in this window — other hours produce noise. +

+
+
+

vs all sessions

+

PF ↑ best

+
+
+
+

DD vs no filter

+

lowest

+
+
+
+
+
+ {/* Results table */}
@@ -80,7 +246,7 @@ export default function BacktestPage() { - {["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 => ( ))} @@ -92,6 +258,7 @@ export default function BacktestPage() { + - ))} @@ -125,4 +241,3 @@ export default async function TradesPage() { ); } -
{h}
{r.period} {r.tf} {r.risk}{r.version} {r.trades ?? "—"} {r.wr.toFixed(1)}% @@ -133,13 +300,13 @@ export default function BacktestPage() {

How It Works

-
    +
      {[ - ["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) => (
    1. diff --git a/web/app/globals.css b/web/app/globals.css index c2e4119..0e3a774 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -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; diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 7ac8f08..b3a6293 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -53,6 +53,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo return ( +
+ ); diff --git a/web/app/page.tsx b/web/app/page.tsx index a735236..b2d3e0e 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -12,8 +12,8 @@ export default function DashboardPage() {

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.

View Trades @@ -27,21 +27,26 @@ export default function DashboardPage() { {/* Strategy */}

Strategy

-
+
+
@@ -50,11 +55,11 @@ export default function DashboardPage() {
-

Backtest Highlight

+

Backtest Highlight · v1.4.0

- M5 · 1 Month · 1% Risk + M5 · 50k Bars · 5% Risk

-

XAUUSDm · 159 trades · Exness Demo

+

XAUUSDm · Sep 2025 – Jun 2026 · 83 trades

Full Results → @@ -62,14 +67,15 @@ export default function DashboardPage() {
{[ - { 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 }) => (

{label}

{value}

+

{sub}

))}
diff --git a/web/app/trades/page.tsx b/web/app/trades/page.tsx index dfb04eb..14c95fb 100644 --- a/web/app/trades/page.tsx +++ b/web/app/trades/page.tsx @@ -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((b, d) => !b || d.profit > b.profit ? d : b, null); + const worstTrade = closed.reduce((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 */} -
+
{[ - { 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 }) => (

{label}

@@ -75,6 +91,103 @@ export default async function TradesPage() { ))}
+ {/* Secondary stats row */} +
+
+

Avg Win

+

+ {avgWin > 0 ? `+${formatCurrency(avgWin, currency)}` : "—"} +

+
+
+

Avg Loss

+

+ {avgLoss > 0 ? `−${formatCurrency(avgLoss, currency)}` : "—"} +

+
+
+

Total Friction

+

+ {formatCurrency(totalFriction, currency)} +

+
+
+

Current Streak

+ {streak.count === 0 ? ( +

+ ) : ( +

+ {streak.count}× {streak.type === "win" ? "W" : "L"} +

+ )} +
+
+ + {/* Win/Loss bar */} + {closed.length > 0 && ( +
+
+

Win / Loss Distribution

+

{wins.length}W · {losses.length}L

+
+
+
+
+
+
+ {wr.toFixed(1)}% wins + {(100 - wr).toFixed(1)}% losses +
+
+ )} + + {/* Best / Worst trade */} + {(bestTrade || worstTrade) && ( +
+ {bestTrade && ( +
+

Best Trade

+
+
+

+ +{formatCurrency(bestTrade.profit, currency)} +

+

+ {bestTrade.symbol} · {formatDate(bestTrade.time)} +

+
+ + {bestTrade.type === 0 ? "BUY" : "SELL"} + +
+
+ )} + {worstTrade && ( +
+

Worst Trade

+
+
+

+ {formatCurrency(worstTrade.profit, currency)} +

+

+ {worstTrade.symbol} · {formatDate(worstTrade.time)} +

+
+ + {worstTrade.type === 0 ? "BUY" : "SELL"} + +
+
+ )} +
+ )} + {/* Equity curve */}

Equity Curve

@@ -83,8 +196,9 @@ export default async function TradesPage() { {/* Trade table */}
-
+

Closed Trades

+

{closed.length} trades

{closed.length === 0 ? (
No closed trades yet.
@@ -110,8 +224,10 @@ export default async function TradesPage() {
{d.volume} {d.price.toFixed(2)}= 0 ? "text-bull" : "text-bear"}`}> - {formatCurrency(d.profit, currency)} + + = 0 ? "text-bull" : "text-bear"}`}> + {formatCurrency(d.profit, currency)} +