research: --fixed-rr-tp backtest flag + 2026-08-05 verdict (NOT shipped)

Hard TP at N×risk beats the trail on all 2024+ windows but the grid is
monotone to 3R (trend artifact) and M15 rr20 goes negative in 2021+2023
with doubled DD — trailing exits stay. Flag is research-only; its
Module 22 tests land with the next commit (shared test_all.py).

Claude-Session: https://claude.ai/code/session_017mbXs9HRawZhD1uZ3ze7zQ
This commit is contained in:
Harwinder Sekhon
2026-08-06 22:20:43 -04:00
parent b693f757f8
commit 525a1cd1c1
3 changed files with 173 additions and 1 deletions
+48 -1
View File
@@ -1441,6 +1441,12 @@ class BacktestConfig:
# Round-trip transaction cost in price units, charged on every trade
# (XAU/USD retail spread ≈ $0.30). 0 disables.
spread: float = 0.30
# RESEARCH (2026-08): >0 replaces the exit model with a hard TP at
# this multiple of risk from entry (2.0 = fixed 1:2 RR); trailing is
# disabled for ALL trades, including per-trade XAU_PB overrides.
# Entries/stops/gates are untouched, so the signal book is identical
# to the baseline — only the exit policy changes. Never shipped.
fixed_rr_tp: float = 0.0
# Replay worker processes for the bar loop. 0 = auto (cpu_count - 2,
# capped at 12), 1 = sequential in-process. Signal evaluation per bar
# is pure (past data only), so it fans out across forked workers; the
@@ -1703,6 +1709,25 @@ def _apply_cooldown(
return accepted
def _apply_fixed_rr_tp(trades: List[Trade], mult: float) -> None:
"""RESEARCH: replace each trade's TP with a hard target at
`mult` × |entry − sl| and zero the per-trade trail params so the
XAU_PB overrides cannot resurrect trailing. Risk is measured off the
final (re-anchored) entry/SL, so call this after
`_reanchor_entries_to_raw`. The caller must also pass
trail_arm_r=0 to `simulate_trades` — dispatch is on the run-level arg."""
for t in trades:
risk = abs(t.entry_price - t.sl)
t.tp1 = (t.entry_price + mult * risk if t.direction == "BUY"
else t.entry_price - mult * risk)
t.tp2 = t.tp1 # static branch never reads tp2; CSV hygiene
t.trail_arm_r = 0.0
t.trail_dist_r = 0.0
t.rr_planned = mult
if t.pip_value:
t.reward_pips = mult * risk / t.pip_value
def run_backtest(
bt_cfg: BacktestConfig,
platform_cfg: Optional[PlatformConfig] = None,
@@ -1853,10 +1878,17 @@ def run_backtest(
use_cache=bt_cfg.use_cache, clean=False,
)
_reanchor_entries_to_raw(trades, signal_ts_list, sim_df)
if bt_cfg.fixed_rr_tp > 0:
_apply_fixed_rr_tp(trades, bt_cfg.fixed_rr_tp)
logger.info(
f"RESEARCH override: fixed-RR TP = {bt_cfg.fixed_rr_tp} x risk; "
f"trailing exit DISABLED for all {len(trades)} trades (incl. XAU_PB)"
)
run_arm = 0.0 if bt_cfg.fixed_rr_tp > 0 else platform_cfg.signals.trail_arm_r
t_sim = time.perf_counter()
simulate_trades(trades, sim_df, max_bars=bt_cfg.max_hold_bars,
spread=bt_cfg.spread,
trail_arm_r=platform_cfg.signals.trail_arm_r,
trail_arm_r=run_arm,
trail_dist_r=platform_cfg.signals.trail_dist_r)
logger.info(
f"Trade simulation took {time.perf_counter() - t_sim:.2f}s "
@@ -2200,6 +2232,10 @@ def _parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
help="Override signals.trail_arm_r for this run (research knob)")
p.add_argument("--trail-dist", type=float, default=None,
help="Override signals.trail_dist_r for this run (research knob)")
p.add_argument("--fixed-rr-tp", type=float, default=None,
help="RESEARCH: exit at a hard TP of MULT x risk (e.g. 2.0 = "
"fixed 1:2 RR) instead of the trailing stop; "
"entries/stops/gates unchanged.")
p.add_argument("--quality", default="A+,A,B,C", help="Comma-separated allowed grades")
p.add_argument("--strategies", default="",
help="Comma-separated strategy tags to include (XAU,XAU_PB,XAU_CP,GENERIC). Empty = all.")
@@ -2323,6 +2359,16 @@ def main(argv: Optional[List[str]] = None) -> int:
# make_config; the fallback must match (live == backtest).
from app_config import apply_pair_overrides # noqa: E402
apply_pair_overrides(platform_cfg)
if args.fixed_rr_tp is not None:
if args.fixed_rr_tp <= 0:
raise SystemExit("--fixed-rr-tp must be > 0")
if (args.trail_arm is not None or args.trail_dist is not None or
any(s.split("=", 1)[0].strip().startswith(("signals.trail",
"signals.pb_trail"))
for s in (args.set or []))):
raise SystemExit(
"--fixed-rr-tp conflicts with --trail-arm/--trail-dist/"
"--set signals.*trail* (the flag disables trailing entirely)")
if args.swing_sr_lookback > 0:
platform_cfg.technical.swing_sr_lookback = args.swing_sr_lookback
if args.trail_arm is not None:
@@ -2369,6 +2415,7 @@ def main(argv: Optional[List[str]] = None) -> int:
strategies_only=[s.strip() for s in args.strategies.split(",") if s.strip()],
spread=spread,
workers=args.workers,
fixed_rr_tp=args.fixed_rr_tp or 0.0,
)
logger.info(