feat: show current exposure instead of lifetime stats on the overview
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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user