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
+41 -20
View File
@@ -398,12 +398,17 @@ def format_signal_message(report: SignalReport, spread_detail: bool = True) -> s
lines.append(f"⚖️ RR: 1:{ts.risk_reward:.1f}")
# Trailing-stop management plan (matches backtested exit model:
# TP1 is advisory, the trail is the exit). Pullback setups run
# tighter stops and arm later.
sig_cfg = PlatformConfig().signals
# TP1 is advisory, the trail is the exit). Params are stamped on
# the setup by the producing config so per-TF factory overrides
# (M15 arm 0.70, M5 dist 0.85) are what subscribers see; only a
# legacy unstamped setup falls back to platform defaults.
is_pb = getattr(ts, "strategy", "GENERIC").endswith("_PB")
arm_r = sig_cfg.pb_trail_arm_r if is_pb else sig_cfg.trail_arm_r
dist_r = sig_cfg.pb_trail_dist_r if is_pb else sig_cfg.trail_dist_r
arm_r = getattr(ts, "trail_arm_r", None)
dist_r = getattr(ts, "trail_dist_r", None)
if arm_r is None or dist_r is None:
sig_cfg = PlatformConfig().signals
arm_r = sig_cfg.pb_trail_arm_r if is_pb else sig_cfg.trail_arm_r
dist_r = sig_cfg.pb_trail_dist_r if is_pb else sig_cfg.trail_dist_r
if arm_r > 0:
risk_dist = abs(ts.entry_price - ts.stop_loss)
if ts.direction == "BUY":
@@ -574,6 +579,7 @@ _alert_min_quality = _APP.alert_min_quality
# signal doesn't re-send an identical message every candle.
_last_alert_fp: Dict[str, tuple] = {}
_last_alert_ts: Dict[str, float] = {}
_last_alert_setup: Dict[str, tuple] = {} # key → (entry, risk) of last ALERTED setup
_ALERT_REARM_SECONDS = 3600 # identical signal re-alerts after 1h
@@ -582,40 +588,54 @@ def _passes_quality(quality: str) -> bool:
def _alert_fingerprint(report: SignalReport) -> tuple:
"""Alert identity for dedup. Includes a risk-relative entry bucket:
a persisting signal whose entry drifts a few ticks between candles
keeps the same fingerprint, while a setup that moved by half its risk
distance or more (new pullback, new swing anchor) re-alerts instead
of being swallowed for the full re-arm hour."""
"""Alert identity for dedup: signal, quality, setup direction.
Entry movement is checked separately in _should_alert against the
last ALERTED entry/risk (a stable reference). Deriving a price bucket
from the CURRENT SL distance here made the fingerprint churn on
normal ATR/anchor drift — near-duplicate alerts every candle."""
setup = report.trade_setup
entry_bucket = None
if setup is not None:
sl_dist = abs(setup.entry_price - setup.stop_loss)
entry_bucket = round(setup.entry_price / max(sl_dist * 0.5, 1e-9))
return (
report.final_signal.name,
report.signal_quality,
setup.direction if setup else None,
entry_bucket,
)
def _should_alert(key: str, report: SignalReport) -> bool:
"""True when this signal is new/changed for the key, or re-armed.
A persisting identical signal dedups for the re-arm hour, but a setup
whose entry moved >= half the last-alerted risk distance (new
pullback/swing anchor) re-alerts immediately.
Pure check — call _commit_alert only after the message actually went
out, so a failed Telegram send doesn't consume the alert for an hour.
"""
fp = _alert_fingerprint(report)
return not (
_last_alert_fp.get(key) == fp
and time.time() - _last_alert_ts.get(key, 0) < _ALERT_REARM_SECONDS
)
if _last_alert_fp.get(key) != fp:
return True
if time.time() - _last_alert_ts.get(key, 0) >= _ALERT_REARM_SECONDS:
return True
setup = report.trade_setup
last = _last_alert_setup.get(key)
if setup is not None and last is not None:
last_entry, last_risk = last
if abs(setup.entry_price - last_entry) >= 0.5 * max(last_risk, 1e-9):
return True
return False
def _commit_alert(key: str, report: SignalReport) -> None:
_last_alert_fp[key] = _alert_fingerprint(report)
_last_alert_ts[key] = time.time()
setup = report.trade_setup
if setup is not None:
_last_alert_setup[key] = (
setup.entry_price, abs(setup.entry_price - setup.stop_loss)
)
else:
_last_alert_setup.pop(key, None)
# ── Candle-boundary scheduler ────────────────────────────────────────
@@ -673,7 +693,8 @@ async def _scan_timeframe(
actionable = [
r for r in reports
if _passes_quality(r.signal_quality)
if r.final_signal != SignalStrength.NEUTRAL # same gate as breach path
and _passes_quality(r.signal_quality)
and _should_alert(f"{r.symbol}_{tf.value}", r)
]
if actionable: