alerts: require a trade setup, log every send, drop stale spread-gate vetoes, daily log rotation
Delivery-side fixes from the 2026-09-08 three-day signal-log + telegram-log review. Nothing in the signal core changes: the 3-year H1 and M15 books at $0.65 were regenerated before/after and are identical on every trade column (H1 253 / +70.02R / PF 1.98, M15 1092 / +151.90R / PF 1.38); only the exported `warnings` text differs, on 50/253 H1 and 358/1092 M15 trades. - signal_aggregator: step 7 moves into `_attach_core_setup`; a final pass after the PB/CP archetypes (`_drop_stale_spread_gate_vetoes`) moves a core generator's "SPREAD GATE: ... costs would eat the edge" veto to `cleaned_warnings` whenever a later generator shipped a setup. The 2026-09-08 07:45 UTC A-grade alert carried that line beside a $26.49 TP1; a fifth of H1 and a third of M15 A-grade alerts had been doing the same. The prefix is now the `_SPREAD_GATE_PREFIX` constant shared by both emitters and both cleanups. - telegram_bot: alerts require a trade setup (`_is_alert_candidate`, shared by the scan and breach paths; no-setup candidates log `alert skipped: ... (gate reason)`). The 07:50 UTC B-grade SELL with no setup had gone out under the same warning. - telegram_bot: every delivered alert / breach alert / violent-move heads-up logs an `... sent:` INFO line with the signal-log row id (only failures were logged before, so the review had to reconstruct sends from panel rows). Send fan-out is one `_broadcast` helper for both paths. - telegram_bot: `telegram.log` rotates daily x 30 instead of 3-hourly x 240 (old `_HH` files on the live box need one manual delete). - signal_log / CLAUDE.md: `cleaned_warnings_json` is no longer a CP-only marker; docs say so. - test_all: 468 tests (+2): stale-veto cleanup incl. the PB-shaped case and the both-vetoed case, and the alert-candidate rule with its skip line; the rotation test asserts MIDNIGHT / interval 86400 / 30 backups. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xY4NPPrx6NJoG6QWHzwpd
This commit is contained in:
+62
-15
@@ -18,6 +18,11 @@ import pandas as pd
|
||||
|
||||
_NY_TZ = ZoneInfo("America/New_York")
|
||||
|
||||
# Prefix of the cost-floor veto both core setup generators emit; the XAU_CP
|
||||
# cleanup and the post-archetype stale-veto pass match on it, so it is
|
||||
# spelled once.
|
||||
_SPREAD_GATE_PREFIX = "SPREAD GATE:"
|
||||
|
||||
from chart_patterns import detect_chart_pattern
|
||||
from config import PlatformConfig, SignalStrength, Timeframe
|
||||
from providers import BaseDataProvider, make_data_provider
|
||||
@@ -129,8 +134,11 @@ class SignalReport:
|
||||
bullish_reasons: List[str] = field(default_factory=list)
|
||||
bearish_reasons: List[str] = field(default_factory=list)
|
||||
warnings: List[str] = field(default_factory=list)
|
||||
# Warnings the XAU_CP acceptance removed from `warnings` (metadata
|
||||
# only — logged so the gates a CP trade bypassed stay auditable).
|
||||
# Warnings a shipped setup superseded, removed from `warnings` but
|
||||
# logged so they stay auditable (metadata only): the vetoes the
|
||||
# XAU_CP acceptance stripped and, since 2026-09-08, a core
|
||||
# generator's stale spread-gate veto when a later generator produced
|
||||
# the setup (`_drop_stale_spread_gate_vetoes`).
|
||||
cleaned_warnings: List[str] = field(default_factory=list)
|
||||
|
||||
current_price: float = 0.0
|
||||
@@ -889,7 +897,7 @@ class SignalAggregator:
|
||||
final when this runs.
|
||||
"""
|
||||
stale_prefixes = (
|
||||
"SPREAD GATE:",
|
||||
_SPREAD_GATE_PREFIX,
|
||||
"EXHAUSTION BLOCK",
|
||||
"EXHAUSTED TREND FILTER:",
|
||||
"MTF OPPOSITION:",
|
||||
@@ -1225,16 +1233,7 @@ class SignalAggregator:
|
||||
self._apply_m5_filters(report)
|
||||
|
||||
# ── 7. Trade setup for actionable signals ──────────────────
|
||||
# ATR generator is XAU-only: it hardcodes gold pip value and
|
||||
# 2-decimal price rounding.
|
||||
if (report.trade_setup is None and report.signal_quality in ("A+", "A")
|
||||
and report.display_name == "XAU/USD"):
|
||||
atr = (report.primary_analysis.key_levels.get("atr", 0)
|
||||
if report.primary_analysis else 0)
|
||||
if atr > 0:
|
||||
report.trade_setup = self._generate_xau_atr_trade_setup(report, atr)
|
||||
if report.trade_setup is None and report.signal_quality in ("A+", "A", "B", "C"):
|
||||
report.trade_setup = self._generate_trade_setup(report, pip_value)
|
||||
self._attach_core_setup(report, pip_value)
|
||||
|
||||
# ── 8. Reasons ─────────────────────────────────────────────
|
||||
self._collect_reasons(report)
|
||||
@@ -1271,6 +1270,11 @@ class SignalAggregator:
|
||||
for w in report.warnings)):
|
||||
self._try_chart_pattern(report, primary_df, pip_value)
|
||||
|
||||
# ── 11. Stale spread-gate vetoes ─────────────────────────────
|
||||
# Whichever archetype shipped the setup, a core generator's
|
||||
# cost-floor veto no longer describes it.
|
||||
self._drop_stale_spread_gate_vetoes(report)
|
||||
|
||||
return report
|
||||
|
||||
# ── Internals ──────────────────────────────────────────────────
|
||||
@@ -1278,6 +1282,49 @@ class SignalAggregator:
|
||||
# Only A+/A reach the XAU ATR generator (gate at the call site).
|
||||
_TP_QUALITY_SCALE = {"A+": 2.0, "A": 1.5}
|
||||
|
||||
def _attach_core_setup(self, report: SignalReport, pip_value: float) -> None:
|
||||
"""Step 7: the XAU ATR generator (A+/A) first, the swing generator
|
||||
(GENERIC) for anything it declined. A veto either one leaves
|
||||
behind is reconciled after every archetype has run
|
||||
(`_drop_stale_spread_gate_vetoes`)."""
|
||||
# ATR generator is XAU-only: it hardcodes gold pip value and
|
||||
# 2-decimal price rounding.
|
||||
if (report.trade_setup is None and report.signal_quality in ("A+", "A")
|
||||
and report.display_name == "XAU/USD"):
|
||||
atr = (report.primary_analysis.key_levels.get("atr", 0)
|
||||
if report.primary_analysis else 0)
|
||||
if atr > 0:
|
||||
report.trade_setup = self._generate_xau_atr_trade_setup(report, atr)
|
||||
if report.trade_setup is None and report.signal_quality in ("A+", "A", "B", "C"):
|
||||
report.trade_setup = self._generate_trade_setup(report, pip_value)
|
||||
|
||||
@staticmethod
|
||||
def _drop_stale_spread_gate_vetoes(report: SignalReport) -> None:
|
||||
"""Step 11: runs once after every setup archetype (core ATR/swing,
|
||||
XAU_PB, XAU_CP) has had its turn.
|
||||
|
||||
Both core generators veto a target under the cost floor with a
|
||||
`_SPREAD_GATE_PREFIX` warning. When a later generator then ships
|
||||
a setup with a bigger target, that veto is stale — the 2026-09-08
|
||||
07:45 UTC A-grade alert said "costs would eat the edge" beside a
|
||||
$26.49 TP1. A stale veto moves to `cleaned_warnings` (metadata
|
||||
only, the XAU_CP cleanup's contract; that path has already
|
||||
stripped its own copy by the time this runs, and the PB path is
|
||||
covered here). Nothing downstream reads warnings while a setup
|
||||
exists, so trade books are unchanged — verified 2026-09-08 on the
|
||||
3-yr H1/M15 books; only the exported `warnings` text column
|
||||
differs.
|
||||
"""
|
||||
if report.trade_setup is None:
|
||||
return
|
||||
kept: List[str] = []
|
||||
stale: List[str] = []
|
||||
for warning in report.warnings:
|
||||
(stale if warning.startswith(_SPREAD_GATE_PREFIX) else kept).append(warning)
|
||||
if stale:
|
||||
report.warnings[:] = kept
|
||||
report.cleaned_warnings.extend(stale)
|
||||
|
||||
def _generate_xau_atr_trade_setup(
|
||||
self, report: SignalReport, atr: float
|
||||
) -> Optional[TradeSetup]:
|
||||
@@ -1325,7 +1372,7 @@ class SignalAggregator:
|
||||
min_tp = self.config.signals.round_trip_cost * self.config.signals.min_tp_cost_multiple
|
||||
if tp_distance < min_tp:
|
||||
report.warnings.append(
|
||||
f"SPREAD GATE: target ${tp_distance:.2f} < ${min_tp:.2f} minimum — "
|
||||
f"{_SPREAD_GATE_PREFIX} target ${tp_distance:.2f} < ${min_tp:.2f} minimum — "
|
||||
f"costs would eat the edge"
|
||||
)
|
||||
return None
|
||||
@@ -1744,7 +1791,7 @@ class SignalAggregator:
|
||||
min_tp = self.config.signals.round_trip_cost * self.config.signals.min_tp_cost_multiple
|
||||
if tp1_distance < min_tp:
|
||||
report.warnings.append(
|
||||
f"SPREAD GATE: target ${tp1_distance:.2f} < ${min_tp:.2f} minimum — "
|
||||
f"{_SPREAD_GATE_PREFIX} target ${tp1_distance:.2f} < ${min_tp:.2f} minimum — "
|
||||
f"costs would eat the edge"
|
||||
)
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user