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 { useActivation } from '../hooks/useActivation';
|
||||||
import { useTrades } from '../hooks/useTrades';
|
import { useTrades } from '../hooks/useTrades';
|
||||||
import { useWatchlist } from '../hooks/useWatchlist';
|
import { useWatchlist } from '../hooks/useWatchlist';
|
||||||
import { usePerformance } from '../hooks/usePerformance';
|
import { usePaperTrades } from '../hooks/usePaperTrades';
|
||||||
import { useMarketRegime } from '../hooks/useMarketRegime';
|
import { useMarketRegime } from '../hooks/useMarketRegime';
|
||||||
import { regimeColor, regimeDot, regimeHeadline } from '../lib/regime';
|
import { regimeColor, regimeDot, regimeHeadline } from '../lib/regime';
|
||||||
import { Callout } from '../components/ui/Callout';
|
import { Callout } from '../components/ui/Callout';
|
||||||
@@ -11,6 +11,7 @@ import { Section } from '../components/ui/Section';
|
|||||||
import { OpenTradesPanel } from '../components/dashboard/OpenTradesPanel';
|
import { OpenTradesPanel } from '../components/dashboard/OpenTradesPanel';
|
||||||
import { SkeletonCard, SkeletonTable } from '../components/ui/Skeleton';
|
import { SkeletonCard, SkeletonTable } from '../components/ui/Skeleton';
|
||||||
import { formatPrice } from '../lib/format';
|
import { formatPrice } from '../lib/format';
|
||||||
|
import { tradePnl } from '../lib/paperTrade';
|
||||||
import { recommendationActionLabel } from '../lib/recommendation';
|
import { recommendationActionLabel } from '../lib/recommendation';
|
||||||
import { qualifiesSetup, activationSummary, primaryTargetProbability } from '../lib/qualification';
|
import { qualifiesSetup, activationSummary, primaryTargetProbability } from '../lib/qualification';
|
||||||
import type { TradeSetup } from '../lib/types';
|
import type { TradeSetup } from '../lib/types';
|
||||||
@@ -27,6 +28,11 @@ function rColor(value: number | null): string {
|
|||||||
return 'text-gray-300';
|
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' }: {
|
function Metric({ label, value, sub, valueClass = 'text-gray-100' }: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -57,7 +63,7 @@ export default function DashboardPage() {
|
|||||||
const trades = useTrades();
|
const trades = useTrades();
|
||||||
const watchlist = useWatchlist();
|
const watchlist = useWatchlist();
|
||||||
const activation = useActivation();
|
const activation = useActivation();
|
||||||
const performance = usePerformance();
|
const openTrades = usePaperTrades('open');
|
||||||
const regime = useMarketRegime();
|
const regime = useMarketRegime();
|
||||||
|
|
||||||
const qualifiedSetups = useMemo(
|
const qualifiedSetups = useMemo(
|
||||||
@@ -90,7 +96,21 @@ export default function DashboardPage() {
|
|||||||
weekday: 'long', month: 'long', day: 'numeric',
|
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 (
|
return (
|
||||||
<div className="space-y-8 animate-slide-up">
|
<div className="space-y-8 animate-slide-up">
|
||||||
@@ -120,7 +140,7 @@ export default function DashboardPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Metric strip */}
|
{/* Metric strip */}
|
||||||
{(trades.isLoading || performance.isLoading) ? (
|
{(trades.isLoading || openTrades.isLoading) ? (
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<SkeletonCard /><SkeletonCard /><SkeletonCard /><SkeletonCard />
|
<SkeletonCard /><SkeletonCard /><SkeletonCard /><SkeletonCard />
|
||||||
</div>
|
</div>
|
||||||
@@ -138,15 +158,19 @@ export default function DashboardPage() {
|
|||||||
valueClass={qualifiedSetups.length > 0 ? 'text-blue-300' : 'text-gray-100'}
|
valueClass={qualifiedSetups.length > 0 ? 'text-blue-300' : 'text-gray-100'}
|
||||||
/>
|
/>
|
||||||
<Metric
|
<Metric
|
||||||
label="Hit Rate"
|
label="Open Risk"
|
||||||
value={stats?.hit_rate != null ? `${stats.hit_rate.toFixed(1)}%` : '—'}
|
value={exposure.count > 0 ? `$${exposure.riskUsd.toFixed(0)}` : '—'}
|
||||||
sub={stats ? `${stats.wins}W / ${stats.losses}L evaluated` : 'no outcomes yet'}
|
sub={exposure.count > 0 ? `${exposure.count} open · risk to stops` : 'no open trades'}
|
||||||
/>
|
/>
|
||||||
<Metric
|
<Metric
|
||||||
label="Expectancy"
|
label="Unrealized"
|
||||||
value={fmtR(stats?.avg_r ?? null)}
|
value={exposure.rPriced > 0 ? fmtR(exposure.unrealR) : '—'}
|
||||||
valueClass={rColor(stats?.avg_r ?? null)}
|
valueClass={rColor(exposure.rPriced > 0 ? exposure.unrealR : null)}
|
||||||
sub="average R per trade"
|
sub={
|
||||||
|
exposure.count > 0
|
||||||
|
? `${money(exposure.unrealUsd)} · ${exposure.winners}▲ ${exposure.losers}▼`
|
||||||
|
: 'mark-to-market'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user