mirror of
https://github.com/RomySaputraSihananda/ares.git
synced 2026-08-21 06:38:09 +00:00
feat(web): add Next.js portfolio with live MT5 dashboard
- Linear design system (dark canvas, lavender accent) - Live account stats, open positions, pending orders (30s polling) - Trade history with equity curve chart - Backtest results table - Proxy API routes — MT5_BASE_URL server-side only, never exposed to browser - Deploy on Vercel: set MT5_BASE_URL env var Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
4fdc3da4ed
commit
9716fc0955
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<section className="pt-6 pb-12">
|
||||
<p className="eyebrow mb-4">Quantitative Analysis</p>
|
||||
<h1 className="text-4xl font-semibold tracking-[-0.032em] text-ink mb-3">
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Highlight */}
|
||||
<div className="card-featured p-10 mb-10">
|
||||
<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>
|
||||
</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" },
|
||||
].map(({ label, value, note }) => (
|
||||
<div key={label}>
|
||||
<p className="text-xs text-ink-sub mb-1.5">{label}</p>
|
||||
<p className="font-mono text-xl font-semibold text-ink">{value}</p>
|
||||
<p className="text-xs text-ink-ter mt-1">{note}</p>
|
||||
</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">
|
||||
<p className="eyebrow">All Runs</p>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{["Period", "TF", "Risk", "Trades", "Win Rate", "Profit Factor", "Return", "Max DD", ""].map(h => (
|
||||
<th key={h}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{results.map((r, i) => (
|
||||
<tr key={i} className={r.highlight ? "bg-s2" : ""}>
|
||||
<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-ink-md">{r.trades}</td>
|
||||
<td className="font-mono text-ink-md">{r.wr.toFixed(1)}%</td>
|
||||
<td className="font-mono font-medium">
|
||||
{r.pf != null
|
||||
? <span className={r.pf >= 1.3 ? "text-bull" : r.pf < 1 ? "text-bear" : "text-ink-md"}>{r.pf.toFixed(2)}</span>
|
||||
: <span className="text-ink-ter">—</span>}
|
||||
</td>
|
||||
<td className="font-mono font-medium">
|
||||
{r.ret != null
|
||||
? <span className={r.ret >= 0 ? "text-bull" : "text-bear"}>{r.ret >= 0 ? "+" : ""}{r.ret.toFixed(1)}%</span>
|
||||
: <span className="text-ink-ter">—</span>}
|
||||
</td>
|
||||
<td className="font-mono">
|
||||
{r.dd != null
|
||||
? <span className={Math.abs(r.dd) > 30 ? "text-bear" : Math.abs(r.dd) > 15 ? "text-amber-400" : "text-ink-md"}>{r.dd.toFixed(1)}%</span>
|
||||
: <span className="text-ink-ter">—</span>}
|
||||
</td>
|
||||
<td className="text-xs text-ink-ter">{r.note ?? ""}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Params + How it works */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 mb-10">
|
||||
<div className="card">
|
||||
<p className="eyebrow mb-6">Parameters</p>
|
||||
<dl className="divide-y divide-hl">
|
||||
{params.map(([k, v]) => (
|
||||
<div key={k} className="flex justify-between py-3">
|
||||
<dt className="text-sm text-ink-sub">{k}</dt>
|
||||
<dd className="font-mono text-sm font-medium text-ink">{v}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<p className="eyebrow mb-6">How It Works</p>
|
||||
<ol className="space-y-5">
|
||||
{[
|
||||
["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) => (
|
||||
<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">
|
||||
{i + 1}
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-ink">{title}</p>
|
||||
<p className="text-sm text-ink-sub mt-0.5 leading-relaxed">{desc}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disclaimer */}
|
||||
<div className="rounded-lg border border-hl p-5">
|
||||
<p className="text-xs text-ink-ter leading-relaxed">
|
||||
<span className="text-ink-sub font-medium">Disclaimer — </span>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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; }
|
||||
@@ -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 (
|
||||
<html lang="en">
|
||||
<body className="min-h-screen antialiased">
|
||||
<Nav />
|
||||
<main className="max-w-5xl mx-auto px-4 sm:px-6 py-12">
|
||||
{children}
|
||||
</main>
|
||||
<footer className="border-t border-hl mt-section">
|
||||
<div className="max-w-5xl mx-auto px-6 py-16 grid grid-cols-1 sm:grid-cols-3 gap-10">
|
||||
<div>
|
||||
<span className="font-mono text-xs tracking-widest text-ink-sub uppercase">ARES</span>
|
||||
<p className="text-sm text-ink-sub mt-3 leading-relaxed">
|
||||
M5 Momentum FVG Scalper<br />Built in Rust · XAUUSDm
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-ink-sub uppercase tracking-eyebrow mb-4">Navigation</p>
|
||||
<ul className="space-y-2 text-sm text-ink-sub">
|
||||
<li><a href="/" className="hover:text-ink transition-colors">Dashboard</a></li>
|
||||
<li><a href="/trades" className="hover:text-ink transition-colors">Trades</a></li>
|
||||
<li><a href="/backtest" className="hover:text-ink transition-colors">Backtest</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-ink-sub uppercase tracking-eyebrow mb-4">Disclaimer</p>
|
||||
<p className="text-xs text-ink-ter leading-relaxed">
|
||||
Past performance does not guarantee future results. Demo account forward test. Not financial advice.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import Link from "next/link";
|
||||
import LiveDashboard from "@/components/LiveDashboard";
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
{/* Hero */}
|
||||
<section className="pt-6 pb-16">
|
||||
<p className="eyebrow mb-5">Live Forward Test · Demo Account</p>
|
||||
<h1 className="text-5xl font-semibold tracking-[-0.032em] text-ink leading-[1.10] mb-5">
|
||||
ARES Trading Bot
|
||||
</h1>
|
||||
<p className="text-[18px] text-ink-md leading-relaxed max-w-2xl mb-8">
|
||||
M5 Momentum FVG scalper built in Rust. Targets Fair Value Gaps on XAUUSDm
|
||||
with EMA-20 trend filter, automatic position sizing, and Telegram alerts.
|
||||
</p>
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
<Link href="/trades" className="btn-primary">View Trades</Link>
|
||||
<Link href="/backtest" className="btn-secondary">Backtest Results</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Live data — client-side polling */}
|
||||
<LiveDashboard />
|
||||
|
||||
{/* Strategy */}
|
||||
<section className="mb-16">
|
||||
<p className="eyebrow mb-6">Strategy</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<FeatureCard
|
||||
icon="◈"
|
||||
title="Momentum FVG"
|
||||
body="3-candle pattern: pre → impulse → post. Body ≥ 60% of range, close in top/bottom 20%."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon="◎"
|
||||
title="EMA-20 Filter"
|
||||
body="Long only above EMA-20, short only below. Eliminates counter-trend signals entirely."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon="◉"
|
||||
title="Risk Management"
|
||||
body="Fixed % risk per trade. Minimum 1.5× RR. SL auto-set at FVG boundary."
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Backtest CTA */}
|
||||
<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>
|
||||
<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 · 159 trades · Exness Demo</p>
|
||||
</div>
|
||||
<Link href="/backtest" className="btn-primary sm:ml-auto shrink-0">
|
||||
Full Results →
|
||||
</Link>
|
||||
</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 }) => (
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FeatureCard({ icon, title, body }: { icon: string; title: string; body: string }) {
|
||||
return (
|
||||
<div className="card">
|
||||
<span className="text-accent text-lg mb-4 block">{icon}</span>
|
||||
<p className="font-medium text-[15px] text-ink mb-2 tracking-tight">{title}</p>
|
||||
<p className="text-sm text-ink-sub leading-relaxed">{body}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { getAccount, getDeals, type Deal } from "@/lib/mt5";
|
||||
import { formatCurrency, formatDate } from "@/lib/format";
|
||||
import EquityChart from "@/components/EquityChart";
|
||||
|
||||
export const revalidate = 60;
|
||||
const MAGIC = 19730;
|
||||
|
||||
export default async function TradesPage() {
|
||||
let deals: Deal[] = [];
|
||||
let account = null;
|
||||
let error = false;
|
||||
|
||||
try {
|
||||
const now = new Date().toISOString().slice(0, 19);
|
||||
[account, deals] = await Promise.all([
|
||||
getAccount(),
|
||||
getDeals("2025-01-01T00:00:00", now),
|
||||
]);
|
||||
} catch { error = true; }
|
||||
|
||||
const aresDeals = deals.filter(d => d.magic === MAGIC && d.type !== 2);
|
||||
const startBalance = deals.filter(d => d.type === 2).reduce((s, d) => s + d.profit, 600);
|
||||
const equityData = buildEquityCurve(deals, startBalance);
|
||||
|
||||
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";
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="pt-6 pb-12">
|
||||
<p className="eyebrow mb-4">Live Forward Test</p>
|
||||
<h1 className="text-4xl font-semibold tracking-[-0.032em] text-ink mb-3">
|
||||
Trade History
|
||||
</h1>
|
||||
<p className="text-[15px] text-ink-sub">
|
||||
All closed trades from ARES · magic {MAGIC}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{error ? (
|
||||
<div className="card text-center text-ink-sub text-sm mb-12 py-10">
|
||||
Unable to fetch trade data.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-5 gap-3 mb-10">
|
||||
{[
|
||||
{ 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 },
|
||||
].map(({ label, value, mono, colored }) => (
|
||||
<div key={label} className="stat-tile">
|
||||
<p className="text-xs text-ink-sub mb-2">{label}</p>
|
||||
<p className={`${mono ? "font-mono" : ""} text-xl font-semibold ${
|
||||
colored === true ? "text-bull" : colored === false ? "text-bear" : "text-ink"
|
||||
}`}>{value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Equity curve */}
|
||||
<div className="card mb-10">
|
||||
<p className="eyebrow mb-6">Equity Curve</p>
|
||||
<EquityChart data={equityData} currency={currency} />
|
||||
</div>
|
||||
|
||||
{/* Trade table */}
|
||||
<div className="rounded-lg border border-hl overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-hl">
|
||||
<p className="eyebrow">Closed Trades</p>
|
||||
</div>
|
||||
{closed.length === 0 ? (
|
||||
<div className="text-center text-ink-sub text-sm py-12">No closed trades yet.</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{["Time", "Symbol", "Side", "Volume", "Price", "P&L"].map(h => (
|
||||
<th key={h}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{closed.slice().reverse().map((d) => (
|
||||
<tr key={d.ticket}>
|
||||
<td className="text-ink-sub text-xs whitespace-nowrap">{formatDate(d.time)}</td>
|
||||
<td className="font-mono font-medium text-ink">{d.symbol || "—"}</td>
|
||||
<td>
|
||||
<span className={`status-pill ${d.type === 0 ? "status-bull" : "status-bear"}`}>
|
||||
{d.type === 0 ? "BUY" : "SELL"}
|
||||
</span>
|
||||
</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>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function buildEquityCurve(deals: Deal[], start: number) {
|
||||
const sorted = deals
|
||||
.filter(d => d.magic === MAGIC && d.entry === 1)
|
||||
.sort((a, b) => +new Date(a.time) - +new Date(b.time));
|
||||
|
||||
let eq = start;
|
||||
const pts = [{ date: "Start", equity: eq }];
|
||||
for (const d of sorted) {
|
||||
eq += d.profit + d.swap + d.commission;
|
||||
pts.push({
|
||||
date: new Date(d.time).toLocaleDateString("en-US", { month: "short", day: "numeric" }),
|
||||
equity: parseFloat(eq.toFixed(2)),
|
||||
});
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
Reference in New Issue
Block a user