Add M5 pullback entry archetype (XAU_PB): +140R combined on 12mo

Second entry pattern alongside the Keltner-breakout: enter on the
retrace to EMA21/KC-mid in an established trend (EMA >=3/4 aligned,
ADX>=20, retrace 0.8-6.0 ATR, resumption bar closed back in trend
direction, RSI in the pullback zone not the extended zone). Detector
lives in the aggregator so live and GPU-backtest paths share one
implementation. Own gates (composite floor 15, confirmation-TF
agreement, MTF opposition, vol regimes) with a diagnostic funnel.

PB stops anchor to the pullback swing (~$8 vs ~$25 breakout), so PB
trades get their own trail arm (0.8R vs 0.6R global) via per-trade
trail overrides in the backtester and strategy-aware Telegram alerts.

12mo M5 validation: PB alone +38.3R/254 trades, positive both halves
and all four full quarters; core breakout trades undisturbed
(+101.9R vs +105.0 baseline). Combined book: 549 trades (~10.6/wk),
+140.1R, PF 1.78, DD 11.3R vs 311 trades/+105.0R before.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
harwinder
2026-07-09 14:21:59 -04:00
parent 3ac36525b0
commit dad2432d4d
5 changed files with 270 additions and 21 deletions
+26 -13
View File
@@ -1094,7 +1094,10 @@ class Trade:
final_confidence: float
market_regime: str
volatility_regime: str = ""
strategy_tag: str = "" # "XAU", "EUR_PB", "GBP_PB", "GENERIC"
strategy_tag: str = "" # "XAU", "XAU_PB", "EUR_PB", "GBP_PB", "GENERIC"
# Per-trade trailing overrides (0 = use the run-level params).
trail_arm_r: float = 0.0
trail_dist_r: float = 0.0
# Per-instructions: entry reasoning and indicator breakdown ─────
entry_reasons: List[str] = field(default_factory=list)
@@ -1194,6 +1197,9 @@ def _simulate_trailing(
t.exit_reason = "NO_DATA"
continue
arm = t.trail_arm_r if t.trail_arm_r > 0 else trail_arm_r
dist = t.trail_dist_r if t.trail_dist_r > 0 else trail_dist_r
cur_sl = t.sl
peak = t.entry_price
exit_price = None
@@ -1217,9 +1223,9 @@ def _simulate_trailing(
peak = max(peak, fav) if buy else min(peak, fav)
exc = ((peak - t.entry_price) if buy
else (t.entry_price - peak)) / risk
if exc >= trail_arm_r:
lvl = (peak - trail_dist_r * risk if buy
else peak + trail_dist_r * risk)
if exc >= arm:
lvl = (peak - dist * risk if buy
else peak + dist * risk)
cur_sl = max(cur_sl, lvl) if buy else min(cur_sl, lvl)
if exit_price is None:
@@ -1617,16 +1623,19 @@ def run_backtest(
if report.signal_quality not in bt_cfg.quality_filter:
continue
# Strategy tag (from bullish_reasons[0] which the aggregator pre-pends).
# Strategy tag (from reasons[0] which the aggregator pre-pends).
# Pullback SELLs mark bearish_reasons, so check both lists.
tag = "GENERIC"
if report.bullish_reasons:
first = report.bullish_reasons[0]
if "XAU STRATEGY" in first:
tag = "XAU"
elif "EUR STRATEGY" in first:
tag = "EUR_PB"
elif "GBP STRATEGY" in first:
tag = "GBP_PB"
first_bull = report.bullish_reasons[0] if report.bullish_reasons else ""
first_bear = report.bearish_reasons[0] if report.bearish_reasons else ""
if "XAU PULLBACK" in first_bull or "XAU PULLBACK" in first_bear:
tag = "XAU_PB"
elif "XAU STRATEGY" in first_bull:
tag = "XAU"
elif "EUR STRATEGY" in first_bull:
tag = "EUR_PB"
elif "GBP STRATEGY" in first_bull:
tag = "GBP_PB"
if bt_cfg.strategies_only and tag not in bt_cfg.strategies_only:
continue
@@ -1684,6 +1693,10 @@ def run_backtest(
indicators_opposing=oppose,
indicators_neutral=neutral,
indicators_total=total,
trail_arm_r=(platform_cfg.signals.pb_trail_arm_r
if tag == "XAU_PB" else 0.0),
trail_dist_r=(platform_cfg.signals.pb_trail_dist_r
if tag == "XAU_PB" else 0.0),
))
last_entry_pos = pos