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';
}
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 (
<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 }) {
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 (
<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. */
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 (
<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">
<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) : '—'}`
} />
<dl className="grid grid-cols-2 gap-x-8 gap-y-3 md:grid-cols-4 xl:grid-cols-2">
<Detail
label="P&L"
value={p ? `${money(p.pnl)} · ${p.pct >= 0 ? '+' : ''}${p.pct.toFixed(1)}%` : '—'}
valueClass={p ? pnlColor(p.pnl) : 'text-gray-500'}
/>
<Detail
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'}
/>
<Detail label="entry → now" value={
`${formatPrice(trade.entry_price)}${trade.current_price != null ? formatPrice(trade.current_price) : '—'}`
} />
<Detail
label={trailMoved ? 'trail' : 'stop'}
value={
@@ -123,34 +172,36 @@ function TradeDetail({ trade, exitLabel, exitMode, atrMultiplier, trailingPct, o
}
/>
<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={
<>
{formatPrice(trade.target)}
{exitMode !== 'target' && (
<span className="ml-1.5 text-[10px] text-gray-500">screening only</span>
)}
opened {opened}
{holdText && <span className={maxHoldColor(trade)}> · {holdText}</span>}
</>
}
/>
<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}
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>
<Fact label="screening target" value={formatPrice(trade.target)} />
{exitRuleText && <Fact label="exit" value={exitRuleText} />}
<button
onClick={onClose}
disabled={closing}
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
</button>
</div>
{ohlcv.data && (
<div>
<p className="num text-[9.5px] uppercase tracking-[0.16em] text-gray-500">
@@ -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 (
<li key={t.id}>
<div
@@ -257,11 +307,7 @@ 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] ${
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]'
}`}
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]"
>
<span className="flex items-center gap-2">
<Link
@@ -276,15 +322,10 @@ export function OpenTradesPanel() {
<span className="num hidden text-xs text-gray-400 sm:block">
{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>
)}
<div className={`min-w-0 ${hasMaxHold ? 'space-y-1.5' : ''}`}>
<RBar r={p?.r ?? null} max={rMax} />
{hasMaxHold && <HoldProgress trade={t} />}
</div>
<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>
@@ -295,7 +336,6 @@ export function OpenTradesPanel() {
{open && (
<TradeDetail
trade={t}
exitLabel={exitLabel}
exitMode={exitMode}
atrMultiplier={atrMultiplier}
trailingPct={trailingPct}