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 && (
-
- {holdText ?? '—'}
-
- )}
+
+
+ {hasMaxHold && }
+
{p?.r != null ? `${p.r >= 0 ? '+' : ''}${p.r.toFixed(2)}R` : '—'}
@@ -295,7 +336,6 @@ export function OpenTradesPanel() {
{open && (