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)
|
if "candlestick" not in disabled: self._detect_candlestick_patterns(df, result)
|
||||||
|
|
||||||
# ── Classify market regime ─────────────────────────────────
|
# ── Classify market regime ─────────────────────────────────
|
||||||
self._classify_regime(df, result)
|
self._classify_regime(result)
|
||||||
|
|
||||||
# ── Regime-aware RSI reinterpretation ──────────────────────
|
# ── Regime-aware RSI reinterpretation ──────────────────────
|
||||||
if self.config.rsi_mode == "regime_aware" and "rsi" not in disabled:
|
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):
|
def _compute_structure_sr(self, df: pd.DataFrame, result: TechnicalAnalysis):
|
||||||
"""Detect swing high/low structure levels for S/R and trade setup."""
|
"""Detect swing high/low structure levels for S/R and trade setup."""
|
||||||
base_lookback = self.config.swing_sr_lookback
|
base_lookback = self.config.swing_sr_lookback
|
||||||
if result.timeframe in ("1min", "5min"):
|
if result.timeframe == "5min":
|
||||||
base_lookback = max(base_lookback, 120)
|
base_lookback = max(base_lookback, 120)
|
||||||
lookback = min(base_lookback, len(df) - 1)
|
lookback = min(base_lookback, len(df) - 1)
|
||||||
if lookback < 10:
|
if lookback < 10:
|
||||||
@@ -1096,7 +1096,7 @@ class TechnicalEngine:
|
|||||||
confidence=conf, description=desc, weight=rsi_ind.weight,
|
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)."""
|
"""Classify the market regime (trending vs ranging vs transitioning)."""
|
||||||
adx_sig = result.indicators.get("adx")
|
adx_sig = result.indicators.get("adx")
|
||||||
ema_sig = result.indicators.get("ema_trend")
|
ema_sig = result.indicators.get("ema_trend")
|
||||||
|
|||||||
+3
-35
@@ -150,13 +150,6 @@ class SignalReport:
|
|||||||
class SignalAggregator:
|
class SignalAggregator:
|
||||||
"""Master aggregator for live signal analysis."""
|
"""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):
|
def __init__(self, config: PlatformConfig):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.data_provider = make_data_provider(config)
|
self.data_provider = make_data_provider(config)
|
||||||
@@ -660,16 +653,10 @@ class SignalAggregator:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Build an XAU_PB trade setup if a pullback candidate passes gates."""
|
"""Build an XAU_PB trade setup if a pullback candidate passes gates."""
|
||||||
cfg = self.config.xau_strategy
|
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)
|
pb = self._detect_m5_pullback(report, primary_df)
|
||||||
if pb is None:
|
if pb is None:
|
||||||
return
|
return
|
||||||
_reject("detected")
|
|
||||||
direction = pb["direction"]
|
direction = pb["direction"]
|
||||||
is_buy = direction == "BUY"
|
is_buy = direction == "BUY"
|
||||||
|
|
||||||
@@ -677,7 +664,6 @@ class SignalAggregator:
|
|||||||
# price mid-pullback scores weaker on the momentum indicators).
|
# price mid-pullback scores weaker on the momentum indicators).
|
||||||
signed = report.final_score if is_buy else -report.final_score
|
signed = report.final_score if is_buy else -report.final_score
|
||||||
if signed < cfg.m5_pullback_min_score:
|
if signed < cfg.m5_pullback_min_score:
|
||||||
_reject("score")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Confirmation TF must agree (reuse Gate-3 threshold).
|
# Confirmation TF must agree (reuse Gate-3 threshold).
|
||||||
@@ -692,7 +678,6 @@ class SignalAggregator:
|
|||||||
confirmed = True
|
confirmed = True
|
||||||
break
|
break
|
||||||
if not confirmed:
|
if not confirmed:
|
||||||
_reject("confirmation_tf")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Never against multi-TF consensus (mirror of gate 6f).
|
# 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))
|
bear_n = sum(1 for s in sigs if s in (SignalStrength.SELL, SignalStrength.STRONG_SELL))
|
||||||
thr = self.config.signals.mtf_agreement_threshold
|
thr = self.config.signals.mtf_agreement_threshold
|
||||||
if (is_buy and bear_n >= thr) or (not is_buy and bull_n >= thr):
|
if (is_buy and bear_n >= thr) or (not is_buy and bull_n >= thr):
|
||||||
_reject("mtf_opposition")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Volatility gates match the breakout path.
|
# Volatility gates match the breakout path.
|
||||||
if report.volatility_regime in cfg.m5_blocked_vol_regimes:
|
if report.volatility_regime in cfg.m5_blocked_vol_regimes:
|
||||||
_reject("vol_regime")
|
|
||||||
return
|
return
|
||||||
if (not is_buy and report.volatility_regime
|
if (not is_buy and report.volatility_regime
|
||||||
and report.volatility_regime not in cfg.sell_vol_regimes):
|
and report.volatility_regime not in cfg.sell_vol_regimes):
|
||||||
_reject("sell_vol")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
atr = report.primary_analysis.key_levels.get("atr", 0.0)
|
atr = report.primary_analysis.key_levels.get("atr", 0.0)
|
||||||
@@ -731,7 +713,6 @@ class SignalAggregator:
|
|||||||
min_tp = (self.config.signals.round_trip_cost
|
min_tp = (self.config.signals.round_trip_cost
|
||||||
* self.config.signals.min_tp_cost_multiple)
|
* self.config.signals.min_tp_cost_multiple)
|
||||||
if 1.5 * sl_dist < min_tp:
|
if 1.5 * sl_dist < min_tp:
|
||||||
_reject("spread_gate")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
report.trade_setup = TradeSetup(
|
report.trade_setup = TradeSetup(
|
||||||
@@ -751,7 +732,6 @@ class SignalAggregator:
|
|||||||
trail_arm_r=self.config.signals.pb_trail_arm_r,
|
trail_arm_r=self.config.signals.pb_trail_arm_r,
|
||||||
trail_dist_r=self.config.signals.pb_trail_dist_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"):
|
if report.signal_quality not in ("A+", "A", "B"):
|
||||||
report.signal_quality = "B"
|
report.signal_quality = "B"
|
||||||
report.final_signal = (SignalStrength.BUY if is_buy
|
report.final_signal = (SignalStrength.BUY if is_buy
|
||||||
@@ -778,10 +758,6 @@ class SignalAggregator:
|
|||||||
structural gates plus the validated directional filters below.
|
structural gates plus the validated directional filters below.
|
||||||
"""
|
"""
|
||||||
cfg = self.config.xau_strategy
|
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
|
pa = report.primary_analysis
|
||||||
if pa is None:
|
if pa is None:
|
||||||
@@ -794,7 +770,6 @@ class SignalAggregator:
|
|||||||
pat = detect_chart_pattern(primary_df, atr, cfg)
|
pat = detect_chart_pattern(primary_df, atr, cfg)
|
||||||
if pat is None:
|
if pat is None:
|
||||||
return
|
return
|
||||||
_reject("detected")
|
|
||||||
direction = pat["direction"]
|
direction = pat["direction"]
|
||||||
is_buy = direction == "BUY"
|
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))
|
bear_n = sum(1 for s in sigs if s in (SignalStrength.SELL, SignalStrength.STRONG_SELL))
|
||||||
thr = self.config.signals.mtf_agreement_threshold
|
thr = self.config.signals.mtf_agreement_threshold
|
||||||
if (is_buy and bear_n >= thr) or (not is_buy and bull_n >= thr):
|
if (is_buy and bear_n >= thr) or (not is_buy and bull_n >= thr):
|
||||||
_reject("mtf_opposition")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Directional protections — gold's macro-uptrend asymmetry is
|
# Directional protections — gold's macro-uptrend asymmetry is
|
||||||
@@ -813,7 +787,6 @@ class SignalAggregator:
|
|||||||
if not is_buy:
|
if not is_buy:
|
||||||
if (report.volatility_regime
|
if (report.volatility_regime
|
||||||
and report.volatility_regime not in cfg.sell_vol_regimes):
|
and report.volatility_regime not in cfg.sell_vol_regimes):
|
||||||
_reject("sell_vol")
|
|
||||||
return
|
return
|
||||||
if cfg.sell_blocked_hours_utc:
|
if cfg.sell_blocked_hours_utc:
|
||||||
try:
|
try:
|
||||||
@@ -832,18 +805,15 @@ class SignalAggregator:
|
|||||||
f"CP SELL hour gate FAILS CLOSED (short rejected)"
|
f"CP SELL hour gate FAILS CLOSED (short rejected)"
|
||||||
)
|
)
|
||||||
if close_h is None or close_h in cfg.sell_blocked_hours_utc:
|
if close_h is None or close_h in cfg.sell_blocked_hours_utc:
|
||||||
_reject("sell_hour")
|
|
||||||
return
|
return
|
||||||
if self.config.primary_timeframe == Timeframe.M5:
|
if self.config.primary_timeframe == Timeframe.M5:
|
||||||
rsi_ind = pa.indicators.get("rsi")
|
rsi_ind = pa.indicators.get("rsi")
|
||||||
if rsi_ind is not None and rsi_ind.value < cfg.m5_sell_min_rsi:
|
if rsi_ind is not None and rsi_ind.value < cfg.m5_sell_min_rsi:
|
||||||
_reject("m5_sell_rsi")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# M5 chop-transition volatility block (validated, both ways).
|
# M5 chop-transition volatility block (validated, both ways).
|
||||||
if (self.config.primary_timeframe == Timeframe.M5
|
if (self.config.primary_timeframe == Timeframe.M5
|
||||||
and report.volatility_regime in cfg.m5_blocked_vol_regimes):
|
and report.volatility_regime in cfg.m5_blocked_vol_regimes):
|
||||||
_reject("vol_regime")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
entry = report.current_price
|
entry = report.current_price
|
||||||
@@ -864,12 +834,10 @@ class SignalAggregator:
|
|||||||
# the RR floor or the spread gate, the pattern is untradeable.
|
# the RR floor or the spread gate, the pattern is untradeable.
|
||||||
rr = height / sl_dist if sl_dist > 0 else 0.0
|
rr = height / sl_dist if sl_dist > 0 else 0.0
|
||||||
if rr < self.config.signals.min_rr_ratio - 1e-9:
|
if rr < self.config.signals.min_rr_ratio - 1e-9:
|
||||||
_reject("rr")
|
|
||||||
return
|
return
|
||||||
min_tp = (self.config.signals.round_trip_cost
|
min_tp = (self.config.signals.round_trip_cost
|
||||||
* self.config.signals.min_tp_cost_multiple)
|
* self.config.signals.min_tp_cost_multiple)
|
||||||
if height < min_tp:
|
if height < min_tp:
|
||||||
_reject("spread_gate")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
report.trade_setup = TradeSetup(
|
report.trade_setup = TradeSetup(
|
||||||
@@ -903,7 +871,6 @@ class SignalAggregator:
|
|||||||
target = report.bullish_reasons if is_buy else report.bearish_reasons
|
target = report.bullish_reasons if is_buy else report.bearish_reasons
|
||||||
target.insert(0, marker)
|
target.insert(0, marker)
|
||||||
self._clean_accepted_cp_warnings(report)
|
self._clean_accepted_cp_warnings(report)
|
||||||
_reject("accepted")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _clean_accepted_cp_warnings(report: SignalReport) -> None:
|
def _clean_accepted_cp_warnings(report: SignalReport) -> None:
|
||||||
@@ -1300,7 +1267,8 @@ class SignalAggregator:
|
|||||||
|
|
||||||
# ── Internals ──────────────────────────────────────────────────
|
# ── 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(
|
def _generate_xau_atr_trade_setup(
|
||||||
self, report: SignalReport, atr: float
|
self, report: SignalReport, atr: float
|
||||||
@@ -1317,7 +1285,7 @@ class SignalAggregator:
|
|||||||
else:
|
else:
|
||||||
is_buy = report.final_score >= 0
|
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:
|
if report.signal_quality == "A" and report.exhaustion_count >= 2:
|
||||||
quality_scale = 1.0
|
quality_scale = 1.0
|
||||||
tp_mult = cfg.tp_atr_multiplier * quality_scale
|
tp_mult = cfg.tp_atr_multiplier * quality_scale
|
||||||
|
|||||||
Reference in New Issue
Block a user