Files
ares/web/app/trades/page.tsx
T
romysaputrasihanandaandClaude Sonnet 4.6 9716fc0955 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>
2026-06-11 14:51:55 +07:00

138 lines
5.4 KiB
TypeScript

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;
}