feat: trailing-stop exit sweep in the backtest
Third exit model alongside target-vs-stop and the fixed take-profit. The TP sweep showed the edge lives in the fat tail (avg R keeps rising as you let winners run), but a fixed wide target is win-rate-brutal and gives everything back on a reversal. A trailing stop harvests the tail while protecting gains. Per setup the replay computes the realized R for several trail widths (3/5/7/10/ 15/20%) in a single conservative pass — stop ratchets up via max(initial_stop, peak*(1-trail)), exit on the pullback or at the horizon close, R vs the initial risk. Aggregated into a trailing sweep (win rate = share closed in profit, avg R, total R) over the qualified set and shown as a new table in the Backtest panel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,10 @@ export function BacktestPanel() {
|
||||
report?.take_profit_sweep && report.take_profit_sweep.length > 0
|
||||
? Math.max(...report.take_profit_sweep.map((r) => r.avg_r ?? -Infinity))
|
||||
: null;
|
||||
const bestTrailAvgR =
|
||||
report?.trailing_sweep && report.trailing_sweep.length > 0
|
||||
? Math.max(...report.trailing_sweep.map((r) => r.avg_r ?? -Infinity))
|
||||
: null;
|
||||
|
||||
const run = useMutation({
|
||||
mutationFn: () => triggerJob('backtest'),
|
||||
@@ -286,6 +290,52 @@ export function BacktestPanel() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{report.trailing_sweep && report.trailing_sweep.length > 0 && (
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-medium uppercase tracking-widest text-gray-500">
|
||||
Trailing-stop exit
|
||||
</p>
|
||||
<p className="mb-2 text-[11px] text-gray-500">
|
||||
Let it run, but exit when price gives back <span className="text-gray-300">X% from its
|
||||
peak</span> (the stop only ratchets up, never below the initial stop). Captures the tail
|
||||
without the fixed take-profit's all-or-nothing miss, and protects gains. In R vs the initial
|
||||
risk. <span className="text-gray-300">Win Rate = share closed in profit.</span> ★ = best avg R.
|
||||
</p>
|
||||
<div className="glass overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.06] text-left text-xs uppercase tracking-wider text-gray-500">
|
||||
<th className="px-4 py-2.5">Trail</th>
|
||||
<th className="px-4 py-2.5 text-right">Setups</th>
|
||||
<th className="px-4 py-2.5 text-right">Profitable</th>
|
||||
<th className="px-4 py-2.5 text-right">Win Rate</th>
|
||||
<th className="px-4 py-2.5 text-right">Avg R</th>
|
||||
<th className="px-4 py-2.5 text-right">Total R</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{report.trailing_sweep.map((row) => {
|
||||
const best = row.avg_r != null && row.avg_r === bestTrailAvgR;
|
||||
return (
|
||||
<tr key={row.trail_pct} className={`border-b border-white/[0.04] ${best ? 'bg-emerald-400/[0.06]' : ''}`}>
|
||||
<td className="num px-4 py-2.5 text-gray-200">
|
||||
{best && <span className="mr-1 text-emerald-300">★</span>}
|
||||
{row.trail_pct}%
|
||||
</td>
|
||||
<td className="num px-4 py-2.5 text-right text-gray-200">{row.total}</td>
|
||||
<td className="num px-4 py-2.5 text-right text-emerald-400">{row.wins}</td>
|
||||
<td className="num px-4 py-2.5 text-right text-gray-200">{fmtPct(row.win_rate)}</td>
|
||||
<td className={`num px-4 py-2.5 text-right font-semibold ${rColor(row.avg_r)}`}>{fmtR(row.avg_r)}</td>
|
||||
<td className={`num px-4 py-2.5 text-right ${rColor(row.total_r)}`}>{fmtR(row.total_r)}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-medium uppercase tracking-widest text-gray-500">
|
||||
Probability calibration
|
||||
|
||||
@@ -239,6 +239,15 @@ export interface BacktestTakeProfitRow {
|
||||
total_r: number | null;
|
||||
}
|
||||
|
||||
export interface BacktestTrailingRow {
|
||||
trail_pct: number;
|
||||
total: number;
|
||||
wins: number;
|
||||
win_rate: number | null;
|
||||
avg_r: number | null;
|
||||
total_r: number | null;
|
||||
}
|
||||
|
||||
export interface BacktestSignalEvalRow {
|
||||
signal: string;
|
||||
weeks: number;
|
||||
@@ -262,6 +271,7 @@ export interface BacktestReport {
|
||||
min_momentum_percentile: number;
|
||||
sweep: BacktestSweepRow[];
|
||||
take_profit_sweep?: BacktestTakeProfitRow[];
|
||||
trailing_sweep?: BacktestTrailingRow[];
|
||||
calibration: BacktestCalibrationRow[];
|
||||
signal_eval?: BacktestSignalEvalRow[];
|
||||
signal_eval_note?: string;
|
||||
|
||||
Reference in New Issue
Block a user