diff --git a/.gitignore b/.gitignore index a070dc4..156ef20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ /target .env .ares_state_*.json + +# web portfolio +web/.next/ +web/node_modules/ +web/.env.local diff --git a/web/.env.local.example b/web/.env.local.example new file mode 100644 index 0000000..cc2efdf --- /dev/null +++ b/web/.env.local.example @@ -0,0 +1,3 @@ +# MT5 bridge URL — server-side only, never exposed to browser +# On Vercel: add this as an Environment Variable (not NEXT_PUBLIC_) +MT5_BASE_URL=https://mt5.romys.my.id diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..c77e368 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,5 @@ +.next/ +node_modules/ +.env.local +.env*.local +*.log diff --git a/web/app/api/account/route.ts b/web/app/api/account/route.ts new file mode 100644 index 0000000..5afb144 --- /dev/null +++ b/web/app/api/account/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; +import { getAccount } from "@/lib/mt5"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + try { + const data = await getAccount(); + return NextResponse.json(data); + } catch (e) { + return NextResponse.json({ error: String(e) }, { status: 502 }); + } +} diff --git a/web/app/api/deals/route.ts b/web/app/api/deals/route.ts new file mode 100644 index 0000000..dabe322 --- /dev/null +++ b/web/app/api/deals/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; +import { NextRequest } from "next/server"; +import { getDeals } from "@/lib/mt5"; + +export const dynamic = "force-dynamic"; + +export async function GET(req: NextRequest) { + const { searchParams } = req.nextUrl; + const dateFrom = searchParams.get("date_from") ?? "2025-01-01T00:00:00"; + const dateTo = searchParams.get("date_to") ?? new Date().toISOString().slice(0, 19); + const symbol = searchParams.get("symbol") ?? undefined; + try { + const data = await getDeals(dateFrom, dateTo, symbol); + return NextResponse.json(data); + } catch (e) { + return NextResponse.json({ error: String(e) }, { status: 502 }); + } +} diff --git a/web/app/api/orders/route.ts b/web/app/api/orders/route.ts new file mode 100644 index 0000000..351060a --- /dev/null +++ b/web/app/api/orders/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; +import { getOrders } from "@/lib/mt5"; +import { NextRequest } from "next/server"; + +export const dynamic = "force-dynamic"; + +export async function GET(req: NextRequest) { + const symbol = req.nextUrl.searchParams.get("symbol") ?? undefined; + try { + const data = await getOrders(symbol); + return NextResponse.json(data); + } catch (e) { + return NextResponse.json({ error: String(e) }, { status: 502 }); + } +} diff --git a/web/app/api/positions/route.ts b/web/app/api/positions/route.ts new file mode 100644 index 0000000..7a77f05 --- /dev/null +++ b/web/app/api/positions/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; +import { getPositions } from "@/lib/mt5"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + try { + const data = await getPositions(); + return NextResponse.json(data); + } catch (e) { + return NextResponse.json({ error: String(e) }, { status: 502 }); + } +} diff --git a/web/app/backtest/page.tsx b/web/app/backtest/page.tsx new file mode 100644 index 0000000..f13e363 --- /dev/null +++ b/web/app/backtest/page.tsx @@ -0,0 +1,155 @@ +import Link from "next/link"; + +const results = [ + { period: "1 Month", tf: "M5", risk: "1%", trades: 159, wr: 55.3, pf: 1.42, ret: 43.2, dd: -11.5, highlight: true }, + { period: "1 Month", tf: "M5", risk: "5%", trades: 159, wr: 55.3, pf: 1.26, ret: 390, dd: -156, highlight: false }, + { period: "1 Week", tf: "M5", risk: "1%", trades: 34, wr: 47.1, pf: 0.94, ret: -6.2, dd: -8.1, highlight: false }, + { period: "Yesterday", tf: "M1", risk: "1%", trades: 36, wr: 47.2, pf: 1.05, ret: 6.6, dd: -51, highlight: false }, + { period: "Yesterday", tf: "M5", risk: "1%", trades: 3, wr: 66.7, pf: null, ret: null, dd: null, highlight: false, note: "Too few trades" }, +]; + +const params = [ + ["Timeframe", "M5"], + ["Symbol", "XAUUSDm"], + ["EMA Period", "20"], + ["Min FVG Pips", "3"], + ["Min SL Pips", "5"], + ["Min RR", "1.5×"], + ["FVG Expiry", "10 candles"], + ["Body PCT Min", "60%"], + ["Close PCT Min", "80%"], +]; + +export default function BacktestPage() { + return ( + <> +
+

Quantitative Analysis

+

+ Backtest Results +

+

+ Historical simulation on real MT5 tick data. Includes spread costs, commission, and slippage. +

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

M5 · 1 Month · 1% Risk

+

XAUUSDm · Exness Demo

+
+ 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" }, + ].map(({ label, value, note }) => ( +
+

{label}

+

{value}

+

{note}

+
+ ))} +
+
+ + {/* Results table */} +
+
+

All Runs

+
+
+ + + + {["Period", "TF", "Risk", "Trades", "Win Rate", "Profit Factor", "Return", "Max DD", ""].map(h => ( + + ))} + + + + {results.map((r, i) => ( + + + + + + + + + + + + ))} + +
{h}
{r.period}{r.tf}{r.risk}{r.trades}{r.wr.toFixed(1)}% + {r.pf != null + ? = 1.3 ? "text-bull" : r.pf < 1 ? "text-bear" : "text-ink-md"}>{r.pf.toFixed(2)} + : } + + {r.ret != null + ? = 0 ? "text-bull" : "text-bear"}>{r.ret >= 0 ? "+" : ""}{r.ret.toFixed(1)}% + : } + + {r.dd != null + ? 30 ? "text-bear" : Math.abs(r.dd) > 15 ? "text-amber-400" : "text-ink-md"}>{r.dd.toFixed(1)}% + : } + {r.note ?? ""}
+
+
+ + {/* Params + How it works */} +
+
+

Parameters

+
+ {params.map(([k, v]) => ( +
+
{k}
+
{v}
+
+ ))} +
+
+ +
+

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."], + ].map(([title, desc], i) => ( +
  1. + + {i + 1} + +
    +

    {title}

    +

    {desc}

    +
    +
  2. + ))} +
+
+
+ + {/* Disclaimer */} +
+

+ Disclaimer — + Backtests simulate on historical data and do not account for requotes, broker restrictions, or changing market regimes. + Past results do not guarantee future performance. For informational purposes only. +

+
+ + ); +} diff --git a/web/app/globals.css b/web/app/globals.css new file mode 100644 index 0000000..c2e4119 --- /dev/null +++ b/web/app/globals.css @@ -0,0 +1,128 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + color-scheme: dark; + --c-canvas: #010102; + --c-s1: #0f1011; + --c-s2: #141516; + --c-s3: #18191a; + --c-hl: #23252a; + --c-hl-strong: #34343a; + --c-ink: #f7f8f8; + --c-ink-md: #d0d6e0; + --c-ink-sub: #8a8f98; + --c-ink-ter: #62666d; + --c-accent: #5e6ad2; + --c-accent-hov: #828fff; + --c-bull: #27a644; + --c-bear: #e5484d; +} + +html, body { + background-color: var(--c-canvas) !important; + color: var(--c-ink); + font-family: 'Inter', -apple-system, system-ui, sans-serif; + -webkit-font-smoothing: antialiased; +} + +/* ── Buttons ─────────────────────────────────────────────────────────────── */ +.btn-primary { + display: inline-flex; align-items: center; gap: 8px; + border-radius: 8px; background: var(--c-accent); color: #fff; + font-size: 14px; font-weight: 500; padding: 8px 14px; + transition: background 150ms; border: none; cursor: pointer; text-decoration: none; +} +.btn-primary:hover { background: var(--c-accent-hov); } +.btn-primary:active { background: #5e69d1; } + +.btn-secondary { + display: inline-flex; align-items: center; gap: 8px; + border-radius: 8px; background: var(--c-s1); color: var(--c-ink); + font-size: 14px; font-weight: 500; padding: 8px 14px; + border: 1px solid var(--c-hl); transition: background 150ms; cursor: pointer; text-decoration: none; +} +.btn-secondary:hover { background: var(--c-s2); } + +.btn-inverse { + display: inline-flex; align-items: center; gap: 8px; + border-radius: 8px; background: #fff; color: #000; + font-size: 14px; font-weight: 500; padding: 8px 14px; + transition: background 150ms; border: none; cursor: pointer; text-decoration: none; +} +.btn-inverse:hover { background: #f5f5f5; } + +/* ── Cards ───────────────────────────────────────────────────────────────── */ +.card { + background: var(--c-s1); + border-radius: 12px; + border: 1px solid var(--c-hl); + padding: 24px; +} +.card-featured { + background: var(--c-s2); + border-radius: 12px; + border: 1px solid var(--c-hl-strong); + padding: 24px; +} + +/* ── Stat tile ───────────────────────────────────────────────────────────── */ +.stat-tile { + background: var(--c-s1); + border-radius: 12px; + border: 1px solid var(--c-hl); + padding: 20px; +} + +/* ── Status pills ────────────────────────────────────────────────────────── */ +.status-pill { + display: inline-flex; align-items: center; + border-radius: 9999px; padding: 2px 8px; + font-size: 12px; font-weight: 500; +} +.status-bull { background: rgba(39,166,68,.12); color: var(--c-bull); } +.status-bear { background: rgba(229,72,77,.12); color: var(--c-bear); } +.status-muted { background: var(--c-s2); color: var(--c-ink-md); } + +/* ── Eyebrow label ───────────────────────────────────────────────────────── */ +.eyebrow { + font-size: 13px; font-weight: 500; + letter-spacing: 0.04em; text-transform: uppercase; + color: var(--c-ink-sub); +} + +/* ── Data table ──────────────────────────────────────────────────────────── */ +.data-table { width: 100%; font-size: 14px; } +.data-table thead tr { + border-bottom: 1px solid var(--c-hl); + background: var(--c-s1); + text-align: left; +} +.data-table thead th { + padding: 12px 16px; + font-size: 11px; font-weight: 500; + letter-spacing: 0.06em; text-transform: uppercase; + color: var(--c-ink-sub); white-space: nowrap; +} +.data-table tbody tr { + border-bottom: 1px solid var(--c-hl); + transition: background 100ms; +} +.data-table tbody tr:last-child { border-bottom: none; } +.data-table tbody tr:hover { background: var(--c-s2); } +.data-table tbody td { padding: 12px 16px; } + +/* ── Scrollbar ───────────────────────────────────────────────────────────── */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: var(--c-s1); } +::-webkit-scrollbar-thumb { background: var(--c-hl); border-radius: 4px; } + +/* ── Pulse animation ─────────────────────────────────────────────────────── */ +@keyframes pulse-dot { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.3; } +} +.pulse-dot { animation: pulse-dot 2s ease-in-out infinite; } diff --git a/web/app/layout.tsx b/web/app/layout.tsx new file mode 100644 index 0000000..310d909 --- /dev/null +++ b/web/app/layout.tsx @@ -0,0 +1,50 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import Nav from "@/components/Nav"; + +export const metadata: Metadata = { + title: "ARES — Automated Trading Bot", + description: "Live forward-test results for ARES, an M5 Momentum FVG scalper built in Rust.", + openGraph: { + title: "ARES Trading Bot", + description: "Live forward-test · M5 Momentum FVG · XAUUSDm", + type: "website", + }, +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +