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:
@@ -95,6 +95,43 @@ class TestTakeProfitBucket:
|
||||
assert b["avg_r"] is None
|
||||
|
||||
|
||||
class TestTrailingExits:
|
||||
def test_locks_gain_on_pullback(self):
|
||||
# Runs to 120, then a 10% trail (from peak 120 → 108) is pierced on the drop.
|
||||
res = bt._trailing_exits("long", 100.0, 90.0, (0.10,), [_bar(120, 110, 118), _bar(130, 100, 105)], 30)
|
||||
assert res[10] == pytest.approx(0.8) # (108-100)/100 / 0.10 risk
|
||||
|
||||
def test_initial_stop_caps_loss(self):
|
||||
# Trail (20%) is looser than the initial stop → initial stop governs = -1R.
|
||||
res = bt._trailing_exits("long", 100.0, 90.0, (0.20,), [_bar(101, 89, 90)], 30)
|
||||
assert res[20] == pytest.approx(-1.0)
|
||||
|
||||
def test_timeout_exits_at_close(self):
|
||||
res = bt._trailing_exits("long", 100.0, 90.0, (0.20,), [_bar(105, 98, 104), _bar(106, 100, 105)], 30)
|
||||
assert res[20] == pytest.approx(0.5) # close 105 → +5% / 10% risk
|
||||
|
||||
def test_multiple_widths_one_pass(self):
|
||||
# Tighter trail locks in more here (exit at 114 vs 108).
|
||||
res = bt._trailing_exits("long", 100.0, 90.0, (0.10, 0.05), [_bar(120, 110, 118), _bar(130, 100, 105)], 30)
|
||||
assert res[10] == pytest.approx(0.8)
|
||||
assert res[5] == pytest.approx(1.4)
|
||||
|
||||
|
||||
class TestTrailingBucket:
|
||||
def test_bucket(self):
|
||||
cands = [
|
||||
{"trail_r": {5: 1.4, 10: 0.8}},
|
||||
{"trail_r": {5: -1.0, 10: -1.0}},
|
||||
{"trail_r": {5: 0.5, 10: 0.5}},
|
||||
]
|
||||
b = bt._trailing_bucket(cands, 5)
|
||||
assert b["total"] == 3
|
||||
assert b["wins"] == 2
|
||||
assert b["win_rate"] == pytest.approx(66.7, abs=0.1)
|
||||
assert b["total_r"] == pytest.approx(0.9, abs=0.01)
|
||||
assert b["avg_r"] == pytest.approx(0.3, abs=0.01)
|
||||
|
||||
|
||||
def test_bucket_stats_counts_and_expectancy():
|
||||
cands = [
|
||||
_cand(70, OUTCOME_TARGET_HIT, 3.0), # +3R win
|
||||
|
||||
Reference in New Issue
Block a user