refactor: simplify open position details
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 1m13s
Deploy / deploy (push) Successful in 38s

This commit is contained in:
2026-08-04 12:23:57 +02:00
parent d29d603158
commit 2435abacaf
@@ -22,13 +22,12 @@ function pnlColor(v: number): string {
return 'text-gray-300'; return 'text-gray-300';
} }
function maxHoldText(trade: PaperTrade, compact = false): string | null { function maxHoldText(trade: PaperTrade): string | null {
const remaining = trade.sessions_remaining; const remaining = trade.sessions_remaining;
if (remaining == null) return null; if (remaining == null) return null;
const held = trade.sessions_held ?? 0; const held = trade.sessions_held ?? 0;
if (remaining < 0) return compact ? 'past max hold' : `${held} held · past max hold`; if (remaining < 0) return `${held} held · past max hold`;
if (remaining === 0) return compact ? 'max hold reached' : `${held} held · max hold reached`; if (remaining === 0) return `${held} held · max hold reached`;
if (compact) return `${remaining} ${remaining === 1 ? 'session' : 'sessions'} left`;
return `${held} held · ${remaining} remaining`; return `${held} held · ${remaining} remaining`;
} }
@@ -40,6 +39,46 @@ function maxHoldColor(trade: PaperTrade): string {
return remaining <= warningAt ? 'text-amber-300' : 'text-gray-400'; 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 (
<div
className="relative h-[3px] rounded-full bg-white/[0.06]"
role="progressbar"
aria-label="Holding period"
aria-valuemin={0}
aria-valuemax={total}
aria-valuenow={Math.min(held, total)}
aria-valuetext={remaining < 0
? `${held} sessions held, past maximum hold`
: `${held} sessions held, ${remaining} remaining`}
title="Holding-period progress — click for the exact session count"
>
<span
className={`absolute inset-y-0 left-0 rounded-full ${color}`}
style={{ width: `${elapsedPct}%` }}
/>
<span
className={`absolute top-1/2 h-[5px] w-[2px] -translate-x-1/2 -translate-y-1/2 rounded-full ${color}`}
style={{ left: `${elapsedPct}%` }}
/>
</div>
);
}
function DirTag({ direction }: { direction: string }) { function DirTag({ direction }: { direction: string }) {
const isLong = direction === 'long'; const isLong = direction === 'long';
return ( 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 (
<span className="num inline-flex items-baseline gap-1.5 whitespace-nowrap">
<span className="text-[9px] uppercase tracking-[0.14em] text-gray-600">{label}</span>
<span className={`text-[11px] ${valueClass}`}>{value}</span>
</span>
);
}
/** Expanded row: full trade detail + price chart with entry / trail path. */ /** 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; trade: PaperTrade;
exitLabel: string | null;
exitMode: 'time' | 'trailing' | 'atr_trailing' | 'target'; exitMode: 'time' | 'trailing' | 'atr_trailing' | 'target';
atrMultiplier: number; atrMultiplier: number;
trailingPct: number; trailingPct: number;
@@ -84,30 +135,28 @@ function TradeDetail({ trade, exitLabel, exitMode, atrMultiplier, trailingPct, o
staleTime: 5 * 60_000, staleTime: 5 * 60_000,
}); });
const opened = new Date(trade.opened_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); 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' const chartHint = trailMoved || exitMode === 'atr_trailing' || exitMode === 'trailing'
? 'entry · now · stop · trail · gate' ? 'entry · now · stop · trail · gate'
: 'entry · now · stop · gate'; : 'entry · now · stop · gate';
return ( return (
<div className="flex flex-col gap-4 px-2 pb-4 pt-1"> <div className="flex flex-col gap-4 px-2 pb-4 pt-1">
<dl className="grid grid-cols-2 gap-x-8 gap-y-3 sm:grid-cols-4"> <dl className="grid grid-cols-2 gap-x-8 gap-y-3 md:grid-cols-4 xl:grid-cols-2">
<Detail label="opened" value={`${opened} · ${trade.shares} shares`} />
<Detail label="entry → now" value={
`${formatPrice(trade.entry_price)}${trade.current_price != null ? formatPrice(trade.current_price) : '—'}`
} />
<Detail <Detail
label="P&L" label="P&L"
value={p ? `${money(p.pnl)} · ${p.pct >= 0 ? '+' : ''}${p.pct.toFixed(1)}%` : '—'} value={p ? `${money(p.pnl)} · ${p.pct >= 0 ? '+' : ''}${p.pct.toFixed(1)}%` : '—'}
valueClass={p ? pnlColor(p.pnl) : 'text-gray-500'} valueClass={p ? pnlColor(p.pnl) : 'text-gray-500'}
/> />
<Detail <Detail label="entry → now" value={
label="alpha vs SPY" `${formatPrice(trade.entry_price)}${trade.current_price != null ? formatPrice(trade.current_price) : '—'}`
value={ } />
trade.alpha_pct != null
? `${trade.alpha_pct >= 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'}
/>
<Detail <Detail
label={trailMoved ? 'trail' : 'stop'} label={trailMoved ? 'trail' : 'stop'}
value={ value={
@@ -123,34 +172,36 @@ function TradeDetail({ trade, exitLabel, exitMode, atrMultiplier, trailingPct, o
} }
/> />
<Detail <Detail
label="target" label="alpha vs SPY"
value={
trade.alpha_pct != null
? `${trade.alpha_pct >= 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'}
/>
</dl>
<div className="flex flex-wrap items-center gap-x-5 gap-y-2 border-t border-white/[0.06] pt-3">
<Fact label="position" value={`${trade.shares} shares`} />
<Fact
label="holding"
value={ value={
<> <>
{formatPrice(trade.target)} opened {opened}
{exitMode !== 'target' && ( {holdText && <span className={maxHoldColor(trade)}> · {holdText}</span>}
<span className="ml-1.5 text-[10px] text-gray-500">screening only</span>
)}
</> </>
} }
/> />
<Detail label="exit rule" value={exitLabel ?? 'target/stop'} /> <Fact label="screening target" value={formatPrice(trade.target)} />
{maxHoldText(trade) && ( {exitRuleText && <Fact label="exit" value={exitRuleText} />}
<Detail <button
label="max hold" onClick={onClose}
value={maxHoldText(trade)} disabled={closing}
valueClass={maxHoldColor(trade)} className="ml-auto rounded-md border border-white/[0.1] px-3 py-1.5 text-[11px] text-gray-300 transition-colors hover:bg-white/[0.06] hover:text-white disabled:opacity-50"
/> >
)} Sell at market
<div className="flex items-end"> </button>
<button </div>
onClick={onClose}
disabled={closing}
className="rounded-md border border-white/[0.1] px-3 py-1.5 text-xs text-gray-300 transition-colors hover:bg-white/[0.06] hover:text-white disabled:opacity-50"
>
Sell at market
</button>
</div>
</dl>
{ohlcv.data && ( {ohlcv.data && (
<div> <div>
<p className="num text-[9.5px] uppercase tracking-[0.16em] text-gray-500"> <p className="num text-[9.5px] uppercase tracking-[0.16em] text-gray-500">
@@ -243,7 +294,6 @@ export function OpenTradesPanel() {
{rows.map((t) => { {rows.map((t) => {
const p = tradePnl(t); const p = tradePnl(t);
const open = expandedId === t.id; const open = expandedId === t.id;
const holdText = maxHoldText(t, true);
return ( return (
<li key={t.id}> <li key={t.id}>
<div <div
@@ -257,11 +307,7 @@ export function OpenTradesPanel() {
} }
}} }}
aria-expanded={open} 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] ${ 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]"
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"> <span className="flex items-center gap-2">
<Link <Link
@@ -276,15 +322,10 @@ export function OpenTradesPanel() {
<span className="num hidden text-xs text-gray-400 sm:block"> <span className="num hidden text-xs text-gray-400 sm:block">
{formatPrice(t.entry_price)} {t.current_price != null ? formatPrice(t.current_price) : '—'} {formatPrice(t.entry_price)} {t.current_price != null ? formatPrice(t.current_price) : '—'}
</span> </span>
<RBar r={p?.r ?? null} max={rMax} /> <div className={`min-w-0 ${hasMaxHold ? 'space-y-1.5' : ''}`}>
{hasMaxHold && ( <RBar r={p?.r ?? null} max={rMax} />
<span {hasMaxHold && <HoldProgress trade={t} />}
className={`num hidden text-right text-[11px] lg:block ${maxHoldColor(t)}`} </div>
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'}`}> <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` : '—'} {p?.r != null ? `${p.r >= 0 ? '+' : ''}${p.r.toFixed(2)}R` : '—'}
</span> </span>
@@ -295,7 +336,6 @@ export function OpenTradesPanel() {
{open && ( {open && (
<TradeDetail <TradeDetail
trade={t} trade={t}
exitLabel={exitLabel}
exitMode={exitMode} exitMode={exitMode}
atrMultiplier={atrMultiplier} atrMultiplier={atrMultiplier}
trailingPct={trailingPct} trailingPct={trailingPct}