feat: show current exposure instead of lifetime stats on the overview
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 43s
Deploy / deploy (push) Successful in 25s

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 14:08:59 +02:00
parent 824c15cf69
commit f8d62e4074
+35 -11
View File
@@ -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 (
<div className="space-y-8 animate-slide-up">
@@ -120,7 +140,7 @@ export default function DashboardPage() {
)}
{/* Metric strip */}
{(trades.isLoading || performance.isLoading) ? (
{(trades.isLoading || openTrades.isLoading) ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<SkeletonCard /><SkeletonCard /><SkeletonCard /><SkeletonCard />
</div>
@@ -138,15 +158,19 @@ export default function DashboardPage() {
valueClass={qualifiedSetups.length > 0 ? 'text-blue-300' : 'text-gray-100'}
/>
<Metric
label="Hit Rate"
value={stats?.hit_rate != null ? `${stats.hit_rate.toFixed(1)}%` : '—'}
sub={stats ? `${stats.wins}W / ${stats.losses}L evaluated` : 'no outcomes yet'}
label="Open Risk"
value={exposure.count > 0 ? `$${exposure.riskUsd.toFixed(0)}` : '—'}
sub={exposure.count > 0 ? `${exposure.count} open · risk to stops` : 'no open trades'}
/>
<Metric
label="Expectancy"
value={fmtR(stats?.avg_r ?? null)}
valueClass={rColor(stats?.avg_r ?? null)}
sub="average R per trade"
label="Unrealized"
value={exposure.rPriced > 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'
}
/>
</div>
)}