diff --git a/web/app/trades/page.tsx b/web/app/trades/page.tsx index b0dd83f..78b6ea2 100644 --- a/web/app/trades/page.tsx +++ b/web/app/trades/page.tsx @@ -19,11 +19,8 @@ export default async function TradesPage() { ]); } 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 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); @@ -76,7 +73,7 @@ export default async function TradesPage() { {/* Equity curve */}

Equity Curve

- +
{/* Trade table */} @@ -124,19 +121,3 @@ export default async function TradesPage() { ); } -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; -} diff --git a/web/components/EquityChart.tsx b/web/components/EquityChart.tsx index 04a8798..f15f2ac 100644 --- a/web/components/EquityChart.tsx +++ b/web/components/EquityChart.tsx @@ -1,77 +1,152 @@ "use client"; +import { useState } from "react"; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, - ResponsiveContainer, + ReferenceLine, ResponsiveContainer, } from "recharts"; import { formatCurrency } from "@/lib/format"; -interface DataPoint { date: string; equity: number } +interface Deal { + time: string; + profit: number; + swap: number; + commission: number; +} -export default function EquityChart({ data, currency = "USD" }: { - data: DataPoint[]; +type Period = "1D" | "7D" | "30D" | "ALL"; +const PERIODS: Period[] = ["1D", "7D", "30D", "ALL"]; + +function periodStart(p: Period): Date { + const now = new Date(); + if (p === "1D") return new Date(now.getTime() - 86_400_000); + if (p === "7D") return new Date(now.getTime() - 7 * 86_400_000); + if (p === "30D") return new Date(now.getTime() - 30 * 86_400_000); + return new Date("2000-01-01"); +} + +function fmtTick(iso: string, p: Period) { + const d = new Date(iso); + if (p === "1D") return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }); + return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); +} + +export default function EquityChart({ deals, currency = "USD" }: { + deals: Deal[]; currency?: string; }) { - if (data.length < 2) { + const [period, setPeriod] = useState("ALL"); + + const since = periodStart(period); + const sorted = deals + .filter(d => new Date(d.time) >= since) + .sort((a, b) => +new Date(a.time) - +new Date(b.time)); + + let cum = 0; + const pts = [ + { label: "Start", pnl: 0 }, + ...sorted.map(d => { + cum += d.profit + d.swap + d.commission; + return { label: fmtTick(d.time, period), pnl: parseFloat(cum.toFixed(2)) }; + }), + ]; + + const netPnl = pts[pts.length - 1]?.pnl ?? 0; + const isPos = netPnl >= 0; + + const minPnl = Math.min(0, ...pts.map(d => d.pnl)); + const maxPnl = Math.max(0, ...pts.map(d => d.pnl)); + const range = maxPnl - minPnl || 1; + const zeroPct = `${((maxPnl / range) * 100).toFixed(1)}%`; + + if (pts.length < 2) { return ( -
- Not enough data to render chart. +
+ No trades in this period.
); } - const first = data[0].equity; - const last = data[data.length - 1].equity; - const isPos = last >= first; - const color = isPos ? "#27a644" : "#e5484d"; - const min = Math.min(...data.map(d => d.equity)); - const max = Math.max(...data.map(d => d.equity)); - return ( - - - - - - - - - - - `$${v.toFixed(0)}`} - width={60} - /> - [formatCurrency(v, currency), "Equity"]} - contentStyle={{ - background: "#141516", - border: "1px solid #34343a", - borderRadius: "8px", - fontSize: "12px", - fontFamily: "'JetBrains Mono', monospace", - color: "#f7f8f8", - }} - labelStyle={{ color: "#8a8f98", marginBottom: 4 }} - cursor={{ stroke: "#34343a", strokeWidth: 1 }} - /> - - - +
+ {/* Period tabs + net P&L */} +
+
+ {PERIODS.map(p => ( + + ))} +
+ + {isPos ? "+" : ""}{formatCurrency(netPnl, currency)} + +
+ + + + + + + + + + + + + + + + + + + + + + + `$${v.toFixed(0)}`} + width={60} + /> + [`${v >= 0 ? "+" : ""}${formatCurrency(v, currency)}`, "P&L"]} + contentStyle={{ + background: "#141516", + border: "1px solid #34343a", + borderRadius: "8px", + fontSize: "12px", + fontFamily: "'JetBrains Mono', monospace", + color: "#f7f8f8", + }} + labelStyle={{ color: "#8a8f98", marginBottom: 4 }} + cursor={{ stroke: "#34343a", strokeWidth: 1 }} + /> + + + +
); }