Accuracy audit fixes: alerts carry validated per-TF trail plans + 13 more

Five-agent audit of the signal path; all fixes are delivery/measurement
side. Regression-proven behavior-neutral on validated signals: the 5yr
M15 replay reproduces the pinned book exactly (+210.65R PF 1.35
WR 57.8% MaxDD 33.90R). Tests 215 -> 221.

- TradeSetup carries trail_arm_r/trail_dist_r stamped from the producing
  config; Telegram trail plan reads them (was showing default 0.6/0.75
  instead of the validated M15 arm 0.70 / M5 dist 0.85)
- Alert dedup: fingerprint = (signal, quality, direction); entry movement
  compared against last-ALERTED entry/risk (old SL-dist bucket churned on
  ATR drift -> near-duplicate alerts every candle)
- Fresh fetches enforce the closed-bar boundary check with one 3s grace
  retry, then warn (feed lag no longer silently analyzes the prior bar)
- Backtest max DD includes the zero baseline (opening losing streaks
  were undercounted; pinned M15 DD unchanged)
- Replay loads the regime-filter TF when the H4-ADX gate is on (was a
  silent no-op for M5/M15 primaries)
- clear_cache under _cache_lock; dead ROC zero-cross branches removed
  from both engines (provably unreachable); RSS dates via
  parsedate_to_datetime (named zones no longer stamped UTC); event risk
  grades from the soonest event; .env inline comments stripped;
  auto-scan skips NEUTRAL like the breach path; report_to_dict honors
  the weight-0 skip rule and emits strategy/trail params; PB funnel
  counts spread_gate/accepted; session-filter fail-open paths log loudly
- Docs corrected to match validated code (H&S neckline = lower peak,
  exhaustion anchors farthest tracked swing) with re-validation warnings

Claude-Session: https://claude.ai/code/session_01R5soRUeRBi8kHMB99csrJy
This commit is contained in:
Harwinder Sekhon
2026-07-23 10:31:13 -04:00
parent f814485615
commit 3a95a37a0c
11 changed files with 329 additions and 68 deletions
+39 -2
View File
@@ -50,6 +50,12 @@ class TradeSetup:
pip_value: float
# Entry archetype ("GENERIC", "XAU_PB") — selects trail params.
strategy: str = "GENERIC"
# Trailing exit params stamped from the PRODUCING config, so per-TF
# factory overrides (e.g. M15 arm 0.70, M5 dist 0.85) reach every
# consumer. None = legacy/unstamped setup — formatters fall back to
# platform defaults; 0.0 means trailing is genuinely disabled.
trail_arm_r: Optional[float] = None
trail_dist_r: Optional[float] = None
@dataclass
@@ -228,7 +234,12 @@ class SignalAggregator:
report.action_recommendation = "NO TRADE — Outside tradeable session"
report.trade_setup = None
except Exception:
pass
# Fail-open by design (an unparseable timestamp shouldn't
# kill the scan) but never silently: this filter guards
# real money hours.
logger.exception(
f"{symbol}: live session filter errored — filter SKIPPED this scan"
)
return report
@@ -538,6 +549,7 @@ class SignalAggregator:
min_tp = (self.config.signals.round_trip_cost
* self.config.signals.min_tp_cost_multiple)
if 1.5 * sl_dist < min_tp:
_reject("spread_gate")
return
report.trade_setup = TradeSetup(
@@ -553,7 +565,10 @@ class SignalAggregator:
reward_pips=1.5 * sl_dist / pip_value,
pip_value=pip_value,
strategy="XAU_PB",
trail_arm_r=self.config.signals.pb_trail_arm_r,
trail_dist_r=self.config.signals.pb_trail_dist_r,
)
_reject("accepted") # funnel tick, mirrors the CP path
if report.signal_quality not in ("A+", "A", "B"):
report.signal_quality = "B"
report.final_signal = (SignalStrength.BUY if is_buy
@@ -629,6 +644,10 @@ class SignalAggregator:
).hour
except Exception:
close_h = None
logger.warning(
f"{report.symbol}: bar-close hour underivable — "
f"SELL hour gate skipped"
)
if close_h in cfg.sell_blocked_hours_utc:
_reject("sell_hour")
return
@@ -683,6 +702,8 @@ class SignalAggregator:
reward_pips=height / pip_value,
pip_value=pip_value,
strategy="XAU_CP",
trail_arm_r=self.config.signals.trail_arm_r,
trail_dist_r=self.config.signals.trail_dist_r,
)
if report.signal_quality not in ("A+", "A", "B"):
report.signal_quality = "B"
@@ -814,7 +835,10 @@ class SignalAggregator:
f"DAY FILTER: {day_names.get(bar_wd, '?')} not in allowed days"
)
except Exception:
pass
# Fail-open by design, but never silently (see live wrapper).
logger.exception(
f"{symbol}: session/day filter errored — filter SKIPPED"
)
# ── 6c. Transitioning regime quality boost ──────────────────
if report.market_regime == "transitioning" and report.signal_quality in ("B", "C", "D"):
@@ -922,6 +946,10 @@ class SignalAggregator:
).hour
except Exception:
close_h = None
logger.warning(
f"{report.symbol}: bar-close hour underivable — "
f"SELL hour gate skipped"
)
if close_h in self.config.xau_strategy.sell_blocked_hours_utc:
report.warnings.append(
f"SELL FILTER: {close_h:02}:00 UTC entry blocked for shorts"
@@ -1048,6 +1076,8 @@ class SignalAggregator:
risk_pips=round(sl_distance / pip_value, 1),
reward_pips=round(tp_distance / pip_value, 1),
pip_value=pip_value,
trail_arm_r=self.config.signals.trail_arm_r,
trail_dist_r=self.config.signals.trail_dist_r,
)
def _check_mtf_agreement(self, report: SignalReport) -> bool:
@@ -1165,6 +1195,11 @@ class SignalAggregator:
atr = primary.key_levels.get("atr", 0)
if atr > 0 and score != 0:
# Distance is measured from the FARTHEST of the 3 tracked
# swings (sorted()[0] = deepest low / highest high), so the
# penalty fires earlier than a nearest-swing rule would.
# Calibrated as-is in every validated book — changing the
# anchor is a signal-logic change requiring re-validation.
if score > 0:
swing_lows = sorted(
v for k, v in primary.key_levels.items() if k.startswith("swing_low_")
@@ -1445,6 +1480,8 @@ class SignalAggregator:
risk_pips=risk_pips,
reward_pips=reward_pips,
pip_value=pip_value,
trail_arm_r=self.config.signals.trail_arm_r,
trail_dist_r=self.config.signals.trail_dist_r,
)
def _collect_reasons(self, report: SignalReport):