refactor: simplify open position details
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user