refactor: simplify open position details
This commit is contained in:
@@ -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
|
|
||||||
label="max hold"
|
|
||||||
value={maxHoldText(trade)}
|
|
||||||
valueClass={maxHoldColor(trade)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="flex items-end">
|
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={closing}
|
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"
|
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
|
Sell at market
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
|
<div className={`min-w-0 ${hasMaxHold ? 'space-y-1.5' : ''}`}>
|
||||||
<RBar r={p?.r ?? null} max={rMax} />
|
<RBar r={p?.r ?? null} max={rMax} />
|
||||||
{hasMaxHold && (
|
{hasMaxHold && <HoldProgress trade={t} />}
|
||||||
<span
|
</div>
|
||||||
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'}`}>
|
<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}
|
||||||
|
|||||||
Reference in New Issue
Block a user