From f8d62e4074d9ff96c6e60483c3ca339ec433172f Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Fri, 26 Jun 2026 14:08:59 +0200 Subject: [PATCH] feat: show current exposure instead of lifetime stats on the overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The overview's Hit Rate and Expectancy were static lifetime aggregates — they barely move day to day and aren't actionable at a glance. Replace them with the current state from open paper trades: - Open Risk: total $ at risk to stops across open positions. - Unrealized: summed unrealized R (mark-to-market), with $ P&L and win/loss count. Computed in the frontend from the already-loaded open trades (tradePnl) — no backend change. The detailed lifetime stats remain on Signals → Track Record. Co-Authored-By: Claude Opus 4.8 --- frontend/src/pages/DashboardPage.tsx | 46 +++++++++++++++++++++------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index fd0f42d..93c5bd1 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import { useActivation } from '../hooks/useActivation'; import { useTrades } from '../hooks/useTrades'; import { useWatchlist } from '../hooks/useWatchlist'; -import { usePerformance } from '../hooks/usePerformance'; +import { usePaperTrades } from '../hooks/usePaperTrades'; import { useMarketRegime } from '../hooks/useMarketRegime'; import { regimeColor, regimeDot, regimeHeadline } from '../lib/regime'; import { Callout } from '../components/ui/Callout'; @@ -11,6 +11,7 @@ import { Section } from '../components/ui/Section'; import { OpenTradesPanel } from '../components/dashboard/OpenTradesPanel'; import { SkeletonCard, SkeletonTable } from '../components/ui/Skeleton'; import { formatPrice } from '../lib/format'; +import { tradePnl } from '../lib/paperTrade'; import { recommendationActionLabel } from '../lib/recommendation'; import { qualifiesSetup, activationSummary, primaryTargetProbability } from '../lib/qualification'; import type { TradeSetup } from '../lib/types'; @@ -27,6 +28,11 @@ function rColor(value: number | null): string { return 'text-gray-300'; } +function money(value: number): string { + const sign = value >= 0 ? '+' : '−'; + return `${sign}$${Math.abs(value).toFixed(2)}`; +} + function Metric({ label, value, sub, valueClass = 'text-gray-100' }: { label: string; value: string; @@ -57,7 +63,7 @@ export default function DashboardPage() { const trades = useTrades(); const watchlist = useWatchlist(); const activation = useActivation(); - const performance = usePerformance(); + const openTrades = usePaperTrades('open'); const regime = useMarketRegime(); const qualifiedSetups = useMemo( @@ -90,7 +96,21 @@ export default function DashboardPage() { weekday: 'long', month: 'long', day: 'numeric', }); - const stats = performance.data?.overall; + // Current exposure from open paper trades: $ at risk to stops + mark-to-market. + const exposure = useMemo(() => { + const rows = openTrades.data ?? []; + let riskUsd = 0, unrealUsd = 0, unrealR = 0, rPriced = 0, winners = 0, losers = 0; + for (const t of rows) { + riskUsd += Math.abs(t.entry_price - t.stop_loss) * t.shares; + const p = tradePnl(t); + if (!p) continue; + unrealUsd += p.pnl; + if (p.r != null) { unrealR += p.r; rPriced += 1; } + if (p.pnl > 0) winners += 1; + else if (p.pnl < 0) losers += 1; + } + return { count: rows.length, riskUsd, unrealUsd, unrealR, rPriced, winners, losers }; + }, [openTrades.data]); return (
@@ -120,7 +140,7 @@ export default function DashboardPage() { )} {/* Metric strip */} - {(trades.isLoading || performance.isLoading) ? ( + {(trades.isLoading || openTrades.isLoading) ? (
@@ -138,15 +158,19 @@ export default function DashboardPage() { valueClass={qualifiedSetups.length > 0 ? 'text-blue-300' : 'text-gray-100'} /> 0 ? `$${exposure.riskUsd.toFixed(0)}` : '—'} + sub={exposure.count > 0 ? `${exposure.count} open · risk to stops` : 'no open trades'} /> 0 ? fmtR(exposure.unrealR) : '—'} + valueClass={rColor(exposure.rPriced > 0 ? exposure.unrealR : null)} + sub={ + exposure.count > 0 + ? `${money(exposure.unrealUsd)} · ${exposure.winners}▲ ${exposure.losers}▼` + : 'mark-to-market' + } />
)}