signal_aggregator + engine: drop write-only counters and unreachable branches (no behaviour change)
- 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0156YhogLytzj9P2iH1skGbL
This commit is contained in:
@@ -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")
|
||||
|
||||
+3
-35
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user