From da5c220997f8f431a3ab8b07bca714766133890e Mon Sep 17 00:00:00 2001 From: Harwinder Sekhon Date: Sun, 6 Sep 2026 17:46:33 -0400 Subject: [PATCH] signal_aggregator + engine: drop write-only counters and unreachable branches (no behaviour change) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SignalAggregator.pb_funnel / cp_funnel and the 19 _reject() ticks inside _try_m5_pullback / _try_chart_pattern: class-level rejection counters nothing in the repo reads. Every gate keeps its early return; only the counter bump before it is gone. - _TP_QUALITY_SCALE: the "B"/"C" entries and the 0.75 default could never be selected — the XAU ATR generator is only called for A+/A. The lookup is now a direct index so a violated invariant fails loudly instead of scaling silently. - engine._classify_regime no longer takes the unused DataFrame argument, and the structure S/R branch tests the 5-minute frame only (M1 is never a primary). Parity: 12-month M5 and H1 books regenerated byte-identical (md5 a28fed4becd056466c26d6a947e1a0e5 / d20405ef64f069770c258bb9bed1ac90). Suite: 461 passed, 0 failed. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0156YhogLytzj9P2iH1skGbL --- engine.py | 6 +++--- signal_aggregator.py | 38 +++----------------------------------- 2 files changed, 6 insertions(+), 38 deletions(-) diff --git a/engine.py b/engine.py index 5232636..8ef9760 100755 --- a/engine.py +++ b/engine.py @@ -153,7 +153,7 @@ class TechnicalEngine: if "candlestick" not in disabled: self._detect_candlestick_patterns(df, result) # ── Classify market regime ───────────────────────────────── - self._classify_regime(df, result) + self._classify_regime(result) # ── Regime-aware RSI reinterpretation ────────────────────── if self.config.rsi_mode == "regime_aware" and "rsi" not in disabled: @@ -873,7 +873,7 @@ class TechnicalEngine: def _compute_structure_sr(self, df: pd.DataFrame, result: TechnicalAnalysis): """Detect swing high/low structure levels for S/R and trade setup.""" base_lookback = self.config.swing_sr_lookback - if result.timeframe in ("1min", "5min"): + if result.timeframe == "5min": base_lookback = max(base_lookback, 120) lookback = min(base_lookback, len(df) - 1) if lookback < 10: @@ -1096,7 +1096,7 @@ class TechnicalEngine: confidence=conf, description=desc, weight=rsi_ind.weight, ) - def _classify_regime(self, df: pd.DataFrame, result: TechnicalAnalysis): + def _classify_regime(self, result: TechnicalAnalysis): """Classify the market regime (trending vs ranging vs transitioning).""" adx_sig = result.indicators.get("adx") ema_sig = result.indicators.get("ema_trend") diff --git a/signal_aggregator.py b/signal_aggregator.py index 25f40c3..414a490 100755 --- a/signal_aggregator.py +++ b/signal_aggregator.py @@ -150,13 +150,6 @@ class SignalReport: class SignalAggregator: """Master aggregator for live signal analysis.""" - # Diagnostic rejection counters for the M5 pullback route (class-level, - # shared across instances). Nothing in the repo reads them; they are - # cheap and kept for ad-hoc inspection from a REPL. - pb_funnel: Dict[str, int] = {} - # Same, for the chart-pattern route (XAU_CP). - cp_funnel: Dict[str, int] = {} - def __init__(self, config: PlatformConfig): self.config = config self.data_provider = make_data_provider(config) @@ -660,16 +653,10 @@ class SignalAggregator: ) -> None: """Build an XAU_PB trade setup if a pullback candidate passes gates.""" cfg = self.config.xau_strategy - # Rejection funnel counters (class-level diagnostic, cheap). - funnel = SignalAggregator.pb_funnel - - def _reject(reason: str) -> None: - funnel[reason] = funnel.get(reason, 0) + 1 pb = self._detect_m5_pullback(report, primary_df) if pb is None: return - _reject("detected") direction = pb["direction"] is_buy = direction == "BUY" @@ -677,7 +664,6 @@ class SignalAggregator: # price mid-pullback scores weaker on the momentum indicators). signed = report.final_score if is_buy else -report.final_score if signed < cfg.m5_pullback_min_score: - _reject("score") return # Confirmation TF must agree (reuse Gate-3 threshold). @@ -692,7 +678,6 @@ class SignalAggregator: confirmed = True break if not confirmed: - _reject("confirmation_tf") return # Never against multi-TF consensus (mirror of gate 6f). @@ -701,16 +686,13 @@ class SignalAggregator: bear_n = sum(1 for s in sigs if s in (SignalStrength.SELL, SignalStrength.STRONG_SELL)) thr = self.config.signals.mtf_agreement_threshold if (is_buy and bear_n >= thr) or (not is_buy and bull_n >= thr): - _reject("mtf_opposition") return # Volatility gates match the breakout path. if report.volatility_regime in cfg.m5_blocked_vol_regimes: - _reject("vol_regime") return if (not is_buy and report.volatility_regime and report.volatility_regime not in cfg.sell_vol_regimes): - _reject("sell_vol") return atr = report.primary_analysis.key_levels.get("atr", 0.0) @@ -731,7 +713,6 @@ 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( @@ -751,7 +732,6 @@ class SignalAggregator: 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 @@ -778,10 +758,6 @@ class SignalAggregator: structural gates plus the validated directional filters below. """ cfg = self.config.xau_strategy - funnel = SignalAggregator.cp_funnel - - def _reject(reason: str) -> None: - funnel[reason] = funnel.get(reason, 0) + 1 pa = report.primary_analysis if pa is None: @@ -794,7 +770,6 @@ class SignalAggregator: pat = detect_chart_pattern(primary_df, atr, cfg) if pat is None: return - _reject("detected") direction = pat["direction"] is_buy = direction == "BUY" @@ -804,7 +779,6 @@ class SignalAggregator: bear_n = sum(1 for s in sigs if s in (SignalStrength.SELL, SignalStrength.STRONG_SELL)) thr = self.config.signals.mtf_agreement_threshold if (is_buy and bear_n >= thr) or (not is_buy and bull_n >= thr): - _reject("mtf_opposition") return # Directional protections — gold's macro-uptrend asymmetry is @@ -813,7 +787,6 @@ class SignalAggregator: if not is_buy: if (report.volatility_regime and report.volatility_regime not in cfg.sell_vol_regimes): - _reject("sell_vol") return if cfg.sell_blocked_hours_utc: try: @@ -832,18 +805,15 @@ class SignalAggregator: f"CP SELL hour gate FAILS CLOSED (short rejected)" ) if close_h is None or close_h in cfg.sell_blocked_hours_utc: - _reject("sell_hour") return if self.config.primary_timeframe == Timeframe.M5: rsi_ind = pa.indicators.get("rsi") if rsi_ind is not None and rsi_ind.value < cfg.m5_sell_min_rsi: - _reject("m5_sell_rsi") return # M5 chop-transition volatility block (validated, both ways). if (self.config.primary_timeframe == Timeframe.M5 and report.volatility_regime in cfg.m5_blocked_vol_regimes): - _reject("vol_regime") return entry = report.current_price @@ -864,12 +834,10 @@ class SignalAggregator: # the RR floor or the spread gate, the pattern is untradeable. rr = height / sl_dist if sl_dist > 0 else 0.0 if rr < self.config.signals.min_rr_ratio - 1e-9: - _reject("rr") return min_tp = (self.config.signals.round_trip_cost * self.config.signals.min_tp_cost_multiple) if height < min_tp: - _reject("spread_gate") return report.trade_setup = TradeSetup( @@ -903,7 +871,6 @@ class SignalAggregator: target = report.bullish_reasons if is_buy else report.bearish_reasons target.insert(0, marker) self._clean_accepted_cp_warnings(report) - _reject("accepted") @staticmethod def _clean_accepted_cp_warnings(report: SignalReport) -> None: @@ -1300,7 +1267,8 @@ class SignalAggregator: # ── Internals ────────────────────────────────────────────────── - _TP_QUALITY_SCALE = {"A+": 2.0, "A": 1.5, "B": 1.0, "C": 1.0} + # Only A+/A reach the XAU ATR generator (gate at the call site). + _TP_QUALITY_SCALE = {"A+": 2.0, "A": 1.5} def _generate_xau_atr_trade_setup( self, report: SignalReport, atr: float @@ -1317,7 +1285,7 @@ class SignalAggregator: else: is_buy = report.final_score >= 0 - quality_scale = self._TP_QUALITY_SCALE.get(report.signal_quality, 0.75) + quality_scale = self._TP_QUALITY_SCALE[report.signal_quality] if report.signal_quality == "A" and report.exhaustion_count >= 2: quality_scale = 1.0 tp_mult = cfg.tp_atr_multiplier * quality_scale