Fix M5 signal quality: ATR ordering bug, noise gates, blocked-hour leak (+41R)

ATR was computed after EMAs so ribbon compression detection was dead code
on all timeframes — moved ATR first. Extended M5 swing S/R lookback from
50 to 120 bars. Added four M5-specific filters: min score, MTF agreement,
confirmation TF conviction, and SELL quality cap (A-grade M5 shorts enter
at dip extremes and lose). Fixed backtester blocked-hour check to use bar
close time matching the live bot. Added trade geometry config (spread gate,
SL floor, TP floor RR) and primary_tf mismatch guard in backtester.

Backtest: 1133 trades/+11.5R → 419 trades/+41.0R, PF 1.03→1.37, DD 21→11R.
Train/val split positive in both halves (+19.8R/+21.3R).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
harwinder
2026-07-09 12:48:36 -04:00
parent d95d31a852
commit db16f209bb
4 changed files with 144 additions and 5 deletions
+17
View File
@@ -1383,6 +1383,14 @@ def run_backtest(
primary_timeframe=bt_cfg.primary_tf,
confirmation_timeframes=list(bt_cfg.confirmation_tfs),
)
elif platform_cfg.primary_timeframe != bt_cfg.primary_tf:
# A silent mismatch yields 0 trades: the replayer walks
# bt_cfg.primary_tf bars while the aggregator looks for
# platform_cfg.primary_timeframe in the slices and never finds it.
raise ValueError(
f"primary_tf mismatch: BacktestConfig={bt_cfg.primary_tf.value} "
f"vs PlatformConfig={platform_cfg.primary_timeframe.value}"
)
provider = ForexDataProvider(platform_cfg)
@@ -1464,6 +1472,15 @@ def run_backtest(
if not (bt_filter.session_start_hour <= bar_hour < bt_filter.session_end_hour):
skipped_reasons["SESSION_WINDOW"] = skipped_reasons.get("SESSION_WINDOW", 0) + 1
continue
# Blocked hours (live checks datetime.now().hour ≈ bar close).
# analyze_from_dataframes step 6b checks bar-open timestamp,
# which is one bar behind — misses the last 5min of each hour.
_is_xau = PAIR_INFO[bt_cfg.pair]["display"] == "XAU/USD"
_blocked = (platform_cfg.xau_strategy.blocked_hours_utc
if _is_xau else bt_filter.blocked_hours_utc)
if bar_hour in _blocked:
skipped_reasons["SESSION_WINDOW"] = skipped_reasons.get("SESSION_WINDOW", 0) + 1
continue
# Cooldown.
if pos - last_entry_pos < bt_cfg.cooldown_bars: