Draw moving trailing stops on open-position charts.
Reconstruct the ATR/% trail bar-by-bar so the mini chart shows a ratchet path instead of a flat current level, and de-emphasize screening targets under trailing exit modes.
This commit is contained in:
@@ -247,40 +247,161 @@ export function RadarChart({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* TradeChart — close path since entry + entry / stop / target levels */
|
/* TradeChart — close path since entry + entry / stop trail / target */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/** Wilder ATR series matching paper_trade_service._atr_series_from_rows. */
|
||||||
|
function wilderAtrSeries(bars: OHLCVBar[], period = 14): (number | null)[] {
|
||||||
|
const n = bars.length;
|
||||||
|
const out: (number | null)[] = Array(n).fill(null);
|
||||||
|
if (n < period + 1) return out;
|
||||||
|
const tr: number[] = Array(n).fill(0);
|
||||||
|
for (let i = 1; i < n; i++) {
|
||||||
|
const h = bars[i].high;
|
||||||
|
const l = bars[i].low;
|
||||||
|
const pc = bars[i - 1].close;
|
||||||
|
tr[i] = Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc));
|
||||||
|
}
|
||||||
|
let running = 0;
|
||||||
|
for (let i = 1; i <= period; i++) running += tr[i];
|
||||||
|
running /= period;
|
||||||
|
let rounded = Math.round(running * 10000) / 10000;
|
||||||
|
out[period] = rounded > 0 ? rounded : null;
|
||||||
|
for (let j = period + 1; j < n; j++) {
|
||||||
|
running = (running * (period - 1) + tr[j]) / period;
|
||||||
|
rounded = Math.round(running * 10000) / 10000;
|
||||||
|
out[j] = rounded > 0 ? rounded : null;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-bar stop level after that bar's update — mirrors the production trail
|
||||||
|
* ratchet so the chart can show a *moving* trail, not a flat line at the
|
||||||
|
* current level.
|
||||||
|
*/
|
||||||
|
function buildStopPath(
|
||||||
|
bars: OHLCVBar[],
|
||||||
|
direction: string,
|
||||||
|
entry: number,
|
||||||
|
initStop: number,
|
||||||
|
openedDate: string,
|
||||||
|
mode: 'atr_trailing' | 'trailing' | 'fixed',
|
||||||
|
atrMultiplier = 3,
|
||||||
|
trailingPct = 12,
|
||||||
|
): number[] {
|
||||||
|
const long = direction !== 'short';
|
||||||
|
const out = bars.map(() => initStop);
|
||||||
|
if (mode === 'fixed') return out;
|
||||||
|
|
||||||
|
let stop = initStop;
|
||||||
|
let anchor = entry;
|
||||||
|
const atr = mode === 'atr_trailing' ? wilderAtrSeries(bars) : null;
|
||||||
|
const trailFrac = trailingPct / 100;
|
||||||
|
|
||||||
|
for (let i = 0; i < bars.length; i++) {
|
||||||
|
const d = bars[i].date.slice(0, 10);
|
||||||
|
if (d <= openedDate) {
|
||||||
|
out[i] = initStop;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (mode === 'atr_trailing' && atr) {
|
||||||
|
const close = bars[i].close;
|
||||||
|
if (long) {
|
||||||
|
anchor = Math.max(anchor, close);
|
||||||
|
const a = atr[i];
|
||||||
|
if (a != null) {
|
||||||
|
const next = anchor - atrMultiplier * a;
|
||||||
|
if (next < close) stop = Math.max(stop, next);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
anchor = Math.min(anchor, close);
|
||||||
|
const a = atr[i];
|
||||||
|
if (a != null) {
|
||||||
|
const next = anchor + atrMultiplier * a;
|
||||||
|
if (next > close) stop = Math.min(stop, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// % trailing: peak on high/low, ratchet only.
|
||||||
|
if (long) {
|
||||||
|
anchor = Math.max(anchor, bars[i].high);
|
||||||
|
stop = Math.max(initStop, anchor * (1 - trailFrac));
|
||||||
|
} else {
|
||||||
|
anchor = Math.min(anchor, bars[i].low);
|
||||||
|
stop = Math.min(initStop, anchor * (1 + trailFrac));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[i] = stop;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
export function TradeChart({
|
export function TradeChart({
|
||||||
direction, entry, stop, target, bars, openedAt,
|
direction,
|
||||||
|
entry,
|
||||||
|
initialStop,
|
||||||
|
target,
|
||||||
|
bars,
|
||||||
|
openedAt,
|
||||||
|
exitMode = 'atr_trailing',
|
||||||
|
atrMultiplier = 3,
|
||||||
|
trailingPct = 12,
|
||||||
}: {
|
}: {
|
||||||
direction: string;
|
direction: string;
|
||||||
entry: number;
|
entry: number;
|
||||||
/** Current stop level (trailing stop if active, else the initial stop). */
|
/** Hard stop from the setup (floor for the trail). */
|
||||||
stop: number;
|
initialStop: number;
|
||||||
target: number;
|
target: number;
|
||||||
bars: OHLCVBar[];
|
bars: OHLCVBar[];
|
||||||
openedAt: string;
|
openedAt: string;
|
||||||
|
exitMode?: 'time' | 'trailing' | 'atr_trailing' | 'target';
|
||||||
|
atrMultiplier?: number;
|
||||||
|
trailingPct?: number;
|
||||||
}) {
|
}) {
|
||||||
const openedDate = openedAt.slice(0, 10);
|
const openedDate = openedAt.slice(0, 10);
|
||||||
const firstIdx = bars.findIndex((b) => b.date.slice(0, 10) >= openedDate);
|
const firstIdx = bars.findIndex((b) => b.date.slice(0, 10) >= openedDate);
|
||||||
// Fresh trades have few bars since entry — pad with context so the chart
|
// Fresh trades have few bars since entry — pad with context so the chart
|
||||||
// still reads (gray before entry, colored after).
|
// still reads (gray before entry, colored after).
|
||||||
const CONTEXT_BARS = 10;
|
const CONTEXT_BARS = 10;
|
||||||
const start = firstIdx === -1 ? Math.max(0, bars.length - CONTEXT_BARS) : Math.max(0, firstIdx - CONTEXT_BARS);
|
const start = firstIdx === -1
|
||||||
const series = bars.slice(start).map((b) => b.close);
|
? Math.max(0, bars.length - CONTEXT_BARS)
|
||||||
|
: Math.max(0, firstIdx - CONTEXT_BARS);
|
||||||
|
const window = bars.slice(start);
|
||||||
|
const series = window.map((b) => b.close);
|
||||||
const entryIdx = firstIdx === -1 ? series.length - 1 : firstIdx - start;
|
const entryIdx = firstIdx === -1 ? series.length - 1 : firstIdx - start;
|
||||||
if (series.length < 2) return null;
|
if (series.length < 2) return null;
|
||||||
|
|
||||||
|
const trailMode: 'atr_trailing' | 'trailing' | 'fixed' =
|
||||||
|
exitMode === 'atr_trailing' || exitMode === 'trailing' ? exitMode : 'fixed';
|
||||||
|
const stopPathFull = buildStopPath(
|
||||||
|
bars,
|
||||||
|
direction,
|
||||||
|
entry,
|
||||||
|
initialStop,
|
||||||
|
openedDate,
|
||||||
|
trailMode,
|
||||||
|
atrMultiplier,
|
||||||
|
trailingPct,
|
||||||
|
);
|
||||||
|
const stopPath = stopPathFull.slice(start);
|
||||||
|
const trailActive = trailMode !== 'fixed';
|
||||||
|
// Target is a screening artifact under trailing policies — only draw when
|
||||||
|
// it is an actual exit.
|
||||||
|
const showTarget = exitMode === 'target';
|
||||||
|
|
||||||
const w = 560; const h = 150;
|
const w = 560; const h = 150;
|
||||||
const padL = 8; const padR = 96; const padT = 10; const padB = 12;
|
const padL = 8; const padR = 96; const padT = 10; const padB = 12;
|
||||||
const isShort = direction === 'short';
|
const isShort = direction === 'short';
|
||||||
const vals = [...series, stop, entry];
|
const vals = [...series, ...stopPath, entry, initialStop];
|
||||||
|
if (showTarget) vals.push(target);
|
||||||
let lo = Math.min(...vals);
|
let lo = Math.min(...vals);
|
||||||
let hi = Math.max(...vals);
|
let hi = Math.max(...vals);
|
||||||
const range = hi - lo || 1;
|
const range = hi - lo || 1;
|
||||||
// Only stretch the scale to the target when it doesn't flatten the price action.
|
if (showTarget) {
|
||||||
const targetFits = isShort ? lo - target <= range * 0.9 : target - hi <= range * 0.9;
|
const targetFits = isShort ? lo - target <= range * 0.9 : target - hi <= range * 0.9;
|
||||||
if (targetFits) { lo = Math.min(lo, target); hi = Math.max(hi, target); }
|
if (targetFits) { lo = Math.min(lo, target); hi = Math.max(hi, target); }
|
||||||
|
}
|
||||||
const pad = (hi - lo) * 0.07;
|
const pad = (hi - lo) * 0.07;
|
||||||
lo -= pad; hi += pad;
|
lo -= pad; hi += pad;
|
||||||
const plotW = w - padL - padR;
|
const plotW = w - padL - padR;
|
||||||
@@ -292,36 +413,97 @@ export function TradeChart({
|
|||||||
.slice(from, to + 1)
|
.slice(from, to + 1)
|
||||||
.map((v, i) => `${i === 0 ? 'M' : 'L'}${px(from + i).toFixed(1)},${py(v).toFixed(1)}`)
|
.map((v, i) => `${i === 0 ? 'M' : 'L'}${px(from + i).toFixed(1)},${py(v).toFixed(1)}`)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
// Step-style trail path so ratchet days read as jumps, not smoothed slopes.
|
||||||
|
const stopSeg = (from: number, to: number) => {
|
||||||
|
if (to < from) return '';
|
||||||
|
let d = `M${px(from).toFixed(1)},${py(stopPath[from]).toFixed(1)}`;
|
||||||
|
for (let i = from + 1; i <= to; i++) {
|
||||||
|
d += `H${px(i).toFixed(1)}V${py(stopPath[i]).toFixed(1)}`;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
};
|
||||||
const contextPath = entryIdx > 0 ? seg(0, entryIdx) : null;
|
const contextPath = entryIdx > 0 ? seg(0, entryIdx) : null;
|
||||||
const tradePath = seg(entryIdx, series.length - 1);
|
const tradePath = seg(entryIdx, series.length - 1);
|
||||||
|
const trailPath = stopSeg(entryIdx, series.length - 1);
|
||||||
const last = series[series.length - 1];
|
const last = series[series.length - 1];
|
||||||
|
const liveStop = stopPath[stopPath.length - 1] ?? initialStop;
|
||||||
|
const trailMoved = Math.abs(liveStop - initialStop) > 1e-6;
|
||||||
const perShare = isShort ? entry - last : last - entry;
|
const perShare = isShort ? entry - last : last - entry;
|
||||||
const col = perShare >= 0 ? 'var(--up)' : 'var(--down)';
|
const col = perShare >= 0 ? 'var(--up)' : 'var(--down)';
|
||||||
const labelX = w - padR + 10;
|
const labelX = w - padR + 10;
|
||||||
const entryY = py(entry);
|
const entryY = py(entry);
|
||||||
let stopY = py(stop);
|
let stopLabelY = py(liveStop);
|
||||||
if (Math.abs(stopY - entryY) < 11) stopY = entryY + (stopY >= entryY ? 11 : -11);
|
if (Math.abs(stopLabelY - entryY) < 11) {
|
||||||
|
stopLabelY = entryY + (stopLabelY >= entryY ? 11 : -11);
|
||||||
|
}
|
||||||
|
const stopLabel = trailActive && trailMoved ? 'trail' : 'stop';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
viewBox={`0 0 ${w} ${h}`}
|
viewBox={`0 0 ${w} ${h}`}
|
||||||
className="hz-tradechart"
|
className="hz-tradechart"
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={`Price since entry ${fmt(entry)}, now ${fmt(last)}, stop ${fmt(stop)}, target ${fmt(target)}`}
|
aria-label={
|
||||||
|
`Price since entry ${fmt(entry)}, now ${fmt(last)}, ${stopLabel} ${fmt(liveStop)}`
|
||||||
|
+ (showTarget ? `, target ${fmt(target)}` : '')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
{/* Initial hard stop as a quiet dashed reference once the trail has lifted */}
|
||||||
|
{trailActive && trailMoved && (
|
||||||
|
<>
|
||||||
|
<line
|
||||||
|
x1={padL}
|
||||||
|
x2={w - padR}
|
||||||
|
y1={py(initialStop)}
|
||||||
|
y2={py(initialStop)}
|
||||||
|
stroke="var(--down)"
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
opacity="0.35"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<line x1={padL} x2={w - padR} y1={entryY} y2={entryY} stroke="var(--ink-3)" strokeWidth="1" opacity="0.5" />
|
<line x1={padL} x2={w - padR} y1={entryY} y2={entryY} stroke="var(--ink-3)" strokeWidth="1" opacity="0.5" />
|
||||||
<text x={labelX} y={entryY + 3.5} className="hz-lvl">entry {fmt(entry)}</text>
|
<text x={labelX} y={entryY + 3.5} className="hz-lvl">entry {fmt(entry)}</text>
|
||||||
<line x1={padL} x2={w - padR} y1={py(stop)} y2={py(stop)} stroke="var(--down)" strokeWidth="1.5" opacity="0.85" />
|
|
||||||
<text x={labelX} y={stopY + 3.5} className="hz-lvl hz-lvl-down">stop {fmt(stop)}</text>
|
{/* Moving trail (or flat initial stop when no trailing policy) */}
|
||||||
{targetFits ? (
|
{trailPath && (
|
||||||
<>
|
<path
|
||||||
<line x1={padL} x2={w - padR} y1={py(target)} y2={py(target)} stroke="var(--up)" strokeWidth="1" opacity="0.6" />
|
d={trailPath}
|
||||||
<text x={labelX} y={py(target) + 3.5} className="hz-lvl hz-lvl-up">target {fmt(target)}</text>
|
fill="none"
|
||||||
</>
|
stroke="var(--down)"
|
||||||
) : (
|
strokeWidth="1.5"
|
||||||
<text x={labelX} y={isShort ? h - padB : padT + 4} className="hz-lvl hz-lvl-up">
|
strokeLinejoin="round"
|
||||||
target {fmt(target)} {isShort ? '↓' : '↑'}
|
strokeLinecap="round"
|
||||||
</text>
|
opacity="0.9"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
<text x={labelX} y={stopLabelY + 3.5} className="hz-lvl hz-lvl-down">
|
||||||
|
{stopLabel} {fmt(liveStop)}
|
||||||
|
</text>
|
||||||
|
|
||||||
|
{showTarget && (() => {
|
||||||
|
const tFits = isShort
|
||||||
|
? py(target) <= h - padB && py(target) >= padT
|
||||||
|
: py(target) <= h - padB && py(target) >= padT;
|
||||||
|
const inRange = isShort
|
||||||
|
? lo - target <= (hi - lo) * 2
|
||||||
|
: target - hi <= (hi - lo) * 2;
|
||||||
|
if (inRange && tFits) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<line x1={padL} x2={w - padR} y1={py(target)} y2={py(target)} stroke="var(--up)" strokeWidth="1" opacity="0.6" />
|
||||||
|
<text x={labelX} y={py(target) + 3.5} className="hz-lvl hz-lvl-up">target {fmt(target)}</text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<text x={labelX} y={isShort ? h - padB : padT + 4} className="hz-lvl hz-lvl-up">
|
||||||
|
target {fmt(target)} {isShort ? '↓' : '↑'}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{contextPath && (
|
{contextPath && (
|
||||||
<path d={contextPath} fill="none" stroke="var(--ink-3)" strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" opacity="0.7" />
|
<path d={contextPath} fill="none" stroke="var(--ink-3)" strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" opacity="0.7" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -46,21 +46,31 @@ function Detail({ label, value, valueClass = 'text-gray-100' }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Expanded row: full trade detail + price chart with entry/stop/target levels. */
|
/** Expanded row: full trade detail + price chart with entry / trail path. */
|
||||||
function TradeDetail({ trade, exitLabel, onClose, closing }: {
|
function TradeDetail({ trade, exitLabel, exitMode, atrMultiplier, trailingPct, onClose, closing }: {
|
||||||
trade: PaperTrade;
|
trade: PaperTrade;
|
||||||
exitLabel: string | null;
|
exitLabel: string | null;
|
||||||
|
exitMode: 'time' | 'trailing' | 'atr_trailing' | 'target';
|
||||||
|
atrMultiplier: number;
|
||||||
|
trailingPct: number;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
closing: boolean;
|
closing: boolean;
|
||||||
}) {
|
}) {
|
||||||
const p = tradePnl(trade);
|
const p = tradePnl(trade);
|
||||||
const stopLevel = trade.trailing_stop ?? trade.stop_loss;
|
const stopLevel = trade.trailing_stop ?? trade.stop_loss;
|
||||||
|
const trailMoved = trade.trailing_stop != null
|
||||||
|
&& Math.abs(trade.trailing_stop - trade.stop_loss) > 1e-6;
|
||||||
const ohlcv = useQuery({
|
const ohlcv = useQuery({
|
||||||
queryKey: ['ohlcv', trade.symbol],
|
queryKey: ['ohlcv', trade.symbol],
|
||||||
queryFn: () => getOHLCV(trade.symbol),
|
queryFn: () => getOHLCV(trade.symbol),
|
||||||
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 chartHint = exitMode === 'target'
|
||||||
|
? 'since entry · entry / stop / target'
|
||||||
|
: exitMode === 'time'
|
||||||
|
? 'since entry · entry / stop'
|
||||||
|
: 'since entry · entry / initial stop / trail';
|
||||||
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 sm:grid-cols-4">
|
||||||
@@ -83,19 +93,30 @@ function TradeDetail({ trade, exitLabel, onClose, closing }: {
|
|||||||
valueClass={trade.alpha_pct != null ? pnlColor(trade.alpha_pct) : 'text-gray-500'}
|
valueClass={trade.alpha_pct != null ? pnlColor(trade.alpha_pct) : 'text-gray-500'}
|
||||||
/>
|
/>
|
||||||
<Detail
|
<Detail
|
||||||
label="stop"
|
label={trailMoved ? 'trail' : 'stop'}
|
||||||
value={
|
value={
|
||||||
<>
|
<>
|
||||||
{formatPrice(stopLevel)}
|
{formatPrice(stopLevel)}
|
||||||
{trade.trailing_stop != null && (
|
{trade.trailing_stop != null && (
|
||||||
<span className="ml-1.5 text-[10px] text-gray-500">
|
<span className="ml-1.5 text-[10px] text-gray-500">
|
||||||
trailing{trade.trailing_distance_pct != null ? ` · ${Math.abs(trade.trailing_distance_pct).toFixed(1)}% away` : ''}
|
{trailMoved ? 'trailing' : 'at initial'}
|
||||||
|
{trade.trailing_distance_pct != null ? ` · ${Math.abs(trade.trailing_distance_pct).toFixed(1)}% away` : ''}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Detail label="target" value={formatPrice(trade.target)} />
|
<Detail
|
||||||
|
label="target"
|
||||||
|
value={
|
||||||
|
<>
|
||||||
|
{formatPrice(trade.target)}
|
||||||
|
{exitMode !== 'target' && (
|
||||||
|
<span className="ml-1.5 text-[10px] text-gray-500">screening only</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Detail label="exit rule" value={exitLabel ?? 'target/stop'} />
|
<Detail label="exit rule" value={exitLabel ?? 'target/stop'} />
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<button
|
<button
|
||||||
@@ -110,16 +131,19 @@ function TradeDetail({ trade, exitLabel, onClose, closing }: {
|
|||||||
{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">
|
||||||
since entry · entry / stop / target
|
{chartHint}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<TradeChart
|
<TradeChart
|
||||||
direction={trade.direction}
|
direction={trade.direction}
|
||||||
entry={trade.entry_price}
|
entry={trade.entry_price}
|
||||||
stop={stopLevel}
|
initialStop={trade.stop_loss}
|
||||||
target={trade.target}
|
target={trade.target}
|
||||||
bars={ohlcv.data}
|
bars={ohlcv.data}
|
||||||
openedAt={trade.opened_at}
|
openedAt={trade.opened_at}
|
||||||
|
exitMode={exitMode}
|
||||||
|
atrMultiplier={atrMultiplier}
|
||||||
|
trailingPct={trailingPct}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,11 +169,14 @@ export function OpenTradesPanel() {
|
|||||||
}
|
}
|
||||||
}, [trades]);
|
}, [trades]);
|
||||||
|
|
||||||
|
const exitMode = policy?.mode ?? 'atr_trailing';
|
||||||
|
const atrMultiplier = policy?.atr_multiplier ?? 3;
|
||||||
|
const trailingPct = policy?.trailing_pct ?? 12;
|
||||||
const exitLabel = policy
|
const exitLabel = policy
|
||||||
? policy.mode === 'atr_trailing'
|
? policy.mode === 'atr_trailing'
|
||||||
? `${(policy.atr_multiplier ?? 3).toFixed(1)}x ATR trailing stop / ${policy.hold_days}d max`
|
? `${atrMultiplier.toFixed(1)}x ATR trailing stop / ${policy.hold_days}d max`
|
||||||
: policy.mode === 'trailing'
|
: policy.mode === 'trailing'
|
||||||
? `trailing ${Math.round(policy.trailing_pct)}%`
|
? `trailing ${Math.round(trailingPct)}%`
|
||||||
: policy.mode === 'time'
|
: policy.mode === 'time'
|
||||||
? `${policy.hold_days}d hold`
|
? `${policy.hold_days}d hold`
|
||||||
: 'target/stop'
|
: 'target/stop'
|
||||||
@@ -231,6 +258,9 @@ export function OpenTradesPanel() {
|
|||||||
<TradeDetail
|
<TradeDetail
|
||||||
trade={t}
|
trade={t}
|
||||||
exitLabel={exitLabel}
|
exitLabel={exitLabel}
|
||||||
|
exitMode={exitMode}
|
||||||
|
atrMultiplier={atrMultiplier}
|
||||||
|
trailingPct={trailingPct}
|
||||||
closing={close.isPending}
|
closing={close.isPending}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
if (window.confirm(`Close ${t.shares} ${t.symbol} at the current price?`)) {
|
if (window.confirm(`Close ${t.shares} ${t.symbol} at the current price?`)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user