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). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017mbXs9HRawZhD1uZ3ze7zQ
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user