diff --git a/frontend/src/components/dashboard/OpenTradesPanel.tsx b/frontend/src/components/dashboard/OpenTradesPanel.tsx index 0a70eea..451973b 100644 --- a/frontend/src/components/dashboard/OpenTradesPanel.tsx +++ b/frontend/src/components/dashboard/OpenTradesPanel.tsx @@ -22,13 +22,12 @@ function pnlColor(v: number): string { return 'text-gray-300'; } -function maxHoldText(trade: PaperTrade, compact = false): string | null { +function maxHoldText(trade: PaperTrade): string | null { const remaining = trade.sessions_remaining; if (remaining == null) return null; const held = trade.sessions_held ?? 0; - if (remaining < 0) return compact ? 'past max hold' : `${held} held · past max hold`; - if (remaining === 0) return compact ? 'max hold reached' : `${held} held · max hold reached`; - if (compact) return `${remaining} ${remaining === 1 ? 'session' : 'sessions'} left`; + if (remaining < 0) return `${held} held · past max hold`; + if (remaining === 0) return `${held} held · max hold reached`; return `${held} held · ${remaining} remaining`; } @@ -40,6 +39,46 @@ function maxHoldColor(trade: PaperTrade): string { return remaining <= warningAt ? 'text-amber-300' : 'text-gray-400'; } +/** Quiet secondary telemetry below the R bar. Exact timing stays in the + * expanded row; this only communicates how far through max hold the trade is. */ +function HoldProgress({ trade }: { trade: PaperTrade }) { + const held = trade.sessions_held; + const remaining = trade.sessions_remaining; + if (held == null || remaining == null) return null; + + const total = Math.max(1, held + Math.max(0, remaining)); + const elapsedPct = remaining <= 0 + ? 100 + : Math.min(100, Math.max(0, (held / total) * 100)); + const warningAt = Math.max(1, Math.ceil(total * 0.2)); + const urgent = remaining <= warningAt; + const color = urgent ? 'bg-amber-400/75' : 'bg-sky-400/40'; + + return ( +
+ + +
+ ); +} + function DirTag({ direction }: { direction: string }) { const isLong = direction === 'long'; return ( @@ -64,10 +103,22 @@ function Detail({ label, value, valueClass = 'text-gray-100' }: { ); } +function Fact({ label, value, valueClass = 'text-gray-300' }: { + label: string; + value: ReactNode; + valueClass?: string; +}) { + return ( + + {label} + {value} + + ); +} + /** Expanded row: full trade detail + price chart with entry / trail path. */ -function TradeDetail({ trade, exitLabel, exitMode, atrMultiplier, trailingPct, onClose, closing }: { +function TradeDetail({ trade, exitMode, atrMultiplier, trailingPct, onClose, closing }: { trade: PaperTrade; - exitLabel: string | null; exitMode: 'time' | 'trailing' | 'atr_trailing' | 'target'; atrMultiplier: number; trailingPct: number; @@ -84,30 +135,28 @@ function TradeDetail({ trade, exitLabel, exitMode, atrMultiplier, trailingPct, o staleTime: 5 * 60_000, }); const opened = new Date(trade.opened_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + const holdText = maxHoldText(trade); + const exitRuleText = exitMode === 'atr_trailing' + ? `${atrMultiplier.toFixed(1)}× ATR trail` + : exitMode === 'trailing' + ? `${Math.round(trailingPct)}% trailing stop` + : exitMode === 'target' + ? 'target / stop' + : null; const chartHint = trailMoved || exitMode === 'atr_trailing' || exitMode === 'trailing' ? 'entry · now · stop · trail · gate' : 'entry · now · stop · gate'; return (
-
- - +
= 0 ? '+' : ''}${p.pct.toFixed(1)}%` : '—'} valueClass={p ? pnlColor(p.pnl) : 'text-gray-500'} /> - = 0 ? '+' : ''}${trade.alpha_pct.toFixed(1)}%${trade.alpha_usd != null ? ` · ${money(trade.alpha_usd)}` : ''}` - : '—' - } - valueClass={trade.alpha_pct != null ? pnlColor(trade.alpha_pct) : 'text-gray-500'} - /> + = 0 ? '+' : ''}${trade.alpha_pct.toFixed(1)}%${trade.alpha_usd != null ? ` · ${money(trade.alpha_usd)}` : ''}` + : '—' + } + valueClass={trade.alpha_pct != null ? pnlColor(trade.alpha_pct) : 'text-gray-500'} + /> +
+
+ + - {formatPrice(trade.target)} - {exitMode !== 'target' && ( - screening only - )} + opened {opened} + {holdText && · {holdText}} } /> - - {maxHoldText(trade) && ( - - )} -
- -
-
+ + {exitRuleText && } + +
{ohlcv.data && (

@@ -243,7 +294,6 @@ export function OpenTradesPanel() { {rows.map((t) => { const p = tradePnl(t); const open = expandedId === t.id; - const holdText = maxHoldText(t, true); return (

  • {formatPrice(t.entry_price)} → {t.current_price != null ? formatPrice(t.current_price) : '—'} - - {hasMaxHold && ( - - )} +
    + + {hasMaxHold && } +
    {p?.r != null ? `${p.r >= 0 ? '+' : ''}${p.r.toFixed(2)}R` : '—'} @@ -295,7 +336,6 @@ export function OpenTradesPanel() { {open && (