feat: show max-hold session countdown

This commit is contained in:
2026-08-03 23:55:30 +02:00
parent 3a6900d45a
commit d431ee283d
5 changed files with 142 additions and 4 deletions
@@ -22,6 +22,21 @@ function pnlColor(v: number): string {
return 'text-gray-300';
}
function maxHoldText(trade: PaperTrade, compact = false): string | null {
const remaining = trade.sessions_remaining;
if (remaining == null) return null;
if (remaining <= 0) return 'exits today';
if (compact) return `${remaining} ${remaining === 1 ? 'session' : 'sessions'} left`;
const held = trade.sessions_held ?? 0;
return `${held} held · ${remaining} remaining`;
}
function maxHoldColor(trade: PaperTrade): string {
return trade.sessions_remaining != null && trade.sessions_remaining <= 5
? 'text-amber-300'
: 'text-gray-400';
}
function DirTag({ direction }: { direction: string }) {
const isLong = direction === 'long';
return (
@@ -116,6 +131,13 @@ function TradeDetail({ trade, exitLabel, exitMode, atrMultiplier, trailingPct, o
}
/>
<Detail label="exit rule" value={exitLabel ?? 'target/stop'} />
{maxHoldText(trade) && (
<Detail
label="max hold"
value={maxHoldText(trade)}
valueClass={maxHoldColor(trade)}
/>
)}
<div className="flex items-end">
<button
onClick={onClose}
@@ -173,13 +195,14 @@ export function OpenTradesPanel() {
const trailingPct = policy?.trailing_pct ?? 12;
const exitLabel = policy
? policy.mode === 'atr_trailing'
? `${atrMultiplier.toFixed(1)}x ATR trailing stop / ${policy.hold_days}d max`
? `${atrMultiplier.toFixed(1)}x ATR trailing stop / ${policy.hold_days} sessions max`
: policy.mode === 'trailing'
? `trailing ${Math.round(trailingPct)}%`
: policy.mode === 'time'
? `${policy.hold_days}d hold`
? `${policy.hold_days}-session hold`
: 'target/stop'
: null;
const hasMaxHold = exitMode === 'atr_trailing' || exitMode === 'time';
const rows = trades ?? [];
@@ -217,6 +240,7 @@ export function OpenTradesPanel() {
{rows.map((t) => {
const p = tradePnl(t);
const open = expandedId === t.id;
const holdText = maxHoldText(t, true);
return (
<li key={t.id}>
<div
@@ -230,7 +254,11 @@ export function OpenTradesPanel() {
}
}}
aria-expanded={open}
className="grid w-full cursor-pointer grid-cols-[110px_1fr_60px_16px] items-center gap-3 rounded-lg px-2 py-2.5 text-left transition-colors hover:bg-white/[0.03] sm:grid-cols-[130px_150px_1fr_70px_16px]"
className={`grid w-full cursor-pointer grid-cols-[110px_1fr_60px_16px] items-center gap-3 rounded-lg px-2 py-2.5 text-left transition-colors hover:bg-white/[0.03] ${
hasMaxHold
? 'sm:grid-cols-[130px_150px_1fr_70px_16px] lg:grid-cols-[130px_150px_1fr_110px_70px_16px]'
: 'sm:grid-cols-[130px_150px_1fr_70px_16px]'
}`}
>
<span className="flex items-center gap-2">
<Link
@@ -246,6 +274,14 @@ export function OpenTradesPanel() {
{formatPrice(t.entry_price)} {t.current_price != null ? formatPrice(t.current_price) : '—'}
</span>
<RBar r={p?.r ?? null} max={rMax} />
{hasMaxHold && (
<span
className={`num hidden text-right text-[11px] lg:block ${maxHoldColor(t)}`}
title="Maximum holding period; the stop may close this trade sooner."
>
{holdText ?? '—'}
</span>
)}
<span className={`num text-right text-[13px] font-semibold ${p?.r != null ? pnlColor(p.r) : 'text-gray-500'}`}>
{p?.r != null ? `${p.r >= 0 ? '+' : ''}${p.r.toFixed(2)}R` : '—'}
</span>
+2
View File
@@ -237,6 +237,8 @@ export interface PaperTrade {
close_reason: 'time' | 'trailing' | 'stop' | 'target' | 'manual' | null;
trailing_stop: number | null;
trailing_distance_pct: number | null;
sessions_held: number | null;
sessions_remaining: number | null;
}
export interface ExitPolicy {