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 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R5soRUeRBi8kHMB99csrJy
This commit is contained in:
+39
-2
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user