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:
+41
-20
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user