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
+156 -3
View File
@@ -814,6 +814,18 @@ def _():
assert dt.tzinfo is not None
@test("NewsEngine._parse_rss_date named zones carry their real offset")
def _():
# EST is UTC−5: 12:00 EST == 17:00 UTC. The old "%Z" path accepted the
# abbreviation but stamped UTC — a silent 5-hour shift.
dt = NewsEngine._parse_rss_date("Mon, 20 Jul 2026 12:00:00 EST")
assert dt is not None and dt.tzinfo is not None
assert dt.astimezone(timezone.utc).hour == 17
# GMT stays a no-op; without-seconds (MyFXBook) form still parses.
dt2 = NewsEngine._parse_rss_date("Mon, 20 Jul 2026 12:00 GMT")
assert dt2 is not None and dt2.astimezone(timezone.utc).hour == 12
@test("NewsEngine._parse_rss_date ISO format")
def _():
dt = NewsEngine._parse_rss_date("2025-05-12T14:30:00+00:00")
@@ -899,6 +911,27 @@ def _():
assert risk == "caution"
@test("NewsEngine.get_event_risk_level grades from the soonest event")
def _():
# Far event listed FIRST: an unsorted feed must still yield danger
# for the nearer event, not caution for the first match.
ne = NewsEngine(NewsConfig())
context = FundamentalContext()
context.events_upcoming_1h = [
EconomicEvent(
time=datetime.now(timezone.utc) + timedelta(minutes=55),
currency="USD", impact="high", event="GDP",
),
EconomicEvent(
time=datetime.now(timezone.utc) + timedelta(minutes=10),
currency="USD", impact="high", event="NFP",
),
]
risk, desc = ne.get_event_risk_level("XAU/USD", context)
assert risk == "danger"
assert "NFP" in desc
@test("NewsEngine.get_event_risk_level clear when no events")
def _():
ne = NewsEngine(NewsConfig())
@@ -1113,6 +1146,24 @@ def _():
assert ts.direction == "BUY"
assert ts.risk_reward == 2.0
assert ts.entry_price > ts.stop_loss
assert ts.trail_arm_r is None and ts.trail_dist_r is None # unstamped default
@test("trade setups carry trail params from the producing config")
def _():
config = PlatformConfig()
config.signals.trail_arm_r = 0.70
config.signals.trail_dist_r = 0.85
agg = SignalAggregator(config)
report = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp="now")
report.final_signal = SignalStrength.BUY
report.final_score = 40.0
report.signal_quality = "A+"
report.current_price = 3300.0
ts = agg._generate_xau_atr_trade_setup(report, 10.0)
assert ts is not None, "A+ setup with ATR 10 should generate"
assert ts.trail_arm_r == 0.70, "setup must carry the producing config's arm"
assert ts.trail_dist_r == 0.85, "setup must carry the producing config's dist"
@test("SignalAggregator._check_mtf_agreement with 2 bullish")
@@ -1569,6 +1620,30 @@ def _():
assert "Indicators" in msg
@test("format_signal_message renders stamped per-TF trail plan")
def _():
def _setup(**extra):
return TradeSetup(
pair="XAU/USD", display_name="XAU/USD", direction="BUY",
entry_price=3300.0, stop_loss=3290.0, take_profit_1=3315.0,
take_profit_2=3325.0, risk_reward=1.5, risk_pips=100,
reward_pips=150, pip_value=0.10, **extra,
)
report = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp="now")
report.final_signal = SignalStrength.BUY
report.signal_quality = "A"
# Stamped setup (M15-style overrides) must render the stamped values…
report.trade_setup = _setup(trail_arm_r=0.70, trail_dist_r=0.85)
msg = format_signal_message(report)
assert "+0.7R" in msg, "must render the stamped arm, not the 0.6 default"
assert "$8.50" in msg, "must render the stamped dist × risk, not the default"
# …while a legacy unstamped setup falls back to platform defaults.
report.trade_setup = _setup()
msg = format_signal_message(report)
assert "+0.6R" in msg
assert "$7.50" in msg
@test("_format_scan_summary produces valid text")
def _():
config = PlatformConfig()
@@ -2116,6 +2191,10 @@ def _():
src = open(_gb.__file__, encoding="utf-8").read()
assert "_apply_regime_filter_live(report, slice_dfs)" in src
assert "backtest.regime_filter" in src
# …and loads the regime-filter TF like live analyze_pair does, or the
# method silently no-ops for M5/M15 primaries (confirm maps lack H4).
assert "regime_filter_timeframe not in timeframes" in src, \
"replay must fetch/slice the regime-filter TF when the gate is on"
@test("disabled_indicators=['macd'] removes the pillar but keeps the histogram")
@@ -2181,6 +2260,40 @@ def _():
break
@test("Fresh fetch missing the just-closed bar retries once, then warns")
def _():
def bars_ending(last):
idx = pd.date_range(end=last, periods=300, freq="15min", tz="UTC")
v = np.linspace(3000.0, 3100.0, 300)
return pd.DataFrame({"Open": v, "High": v + 1, "Low": v - 1,
"Close": v, "Volume": 1000.0}, index=idx)
for _attempt in range(2):
provider = ForexDataProvider(PlatformConfig())
started = pd.Timestamp.now(tz="UTC").floor("15min")
last_closed = started - pd.Timedelta(minutes=15)
lagging = bars_ending(last_closed - pd.Timedelta(minutes=15))
complete = bars_ending(last_closed)
with patch("data_provider._is_forex_weekend", return_value=False), \
patch.object(ForexDataProvider, "FRESH_FETCH_RETRY_SECONDS", 0.0):
# (a) lagging first response → one retry returns the closed bar.
with patch.object(provider, "_fetch_twelve_data",
side_effect=[lagging.copy(), complete.copy()]) as mf:
served = provider.fetch_ohlcv("XAU/USD", Timeframe.M15)
# (b) complete first response → no retry at all.
provider._cache.clear()
provider._cache_timestamps.clear()
with patch.object(provider, "_fetch_twelve_data",
return_value=complete.copy()) as mf2:
provider.fetch_ohlcv("XAU/USD", Timeframe.M15)
if pd.Timestamp.now(tz="UTC").floor("15min") != started:
continue # boundary crossed mid-test — retry once
assert mf.call_count == 2, "lagging response must trigger exactly one retry"
assert served.index[-1] == last_closed, "retry result must be served"
assert mf2.call_count == 1, "boundary-complete response must not retry"
break
@test("Alert dedup: drift dedups, materially-moved setup re-alerts, no early commit")
def _():
import telegram_bot as tb
@@ -2201,17 +2314,23 @@ def _():
key = "TEST_ALERT_FP"
tb._last_alert_fp.pop(key, None)
tb._last_alert_ts.pop(key, None)
r1 = rpt(3400.0, 3392.0) # risk $8 → $4 buckets
tb._last_alert_setup.pop(key, None)
r1 = rpt(3400.0, 3392.0) # risk $8 → re-alert threshold $4
assert tb._should_alert(key, r1)
assert tb._should_alert(key, r1), "_should_alert must not commit state itself"
tb._commit_alert(key, r1)
assert not tb._should_alert(key, r1), "identical signal must dedup"
assert not tb._should_alert(key, rpt(3400.5, 3392.5)), \
"sub-bucket drift must dedup"
"sub-threshold drift must dedup"
assert not tb._should_alert(key, rpt(3401.0, 3392.6)), \
"SL-distance drift (ATR/anchor wobble) must not re-fingerprint"
assert tb._should_alert(key, rpt(3412.0, 3404.0)), \
"setup moved 1.5x risk — must re-alert"
assert tb._should_alert(key, rpt(3395.5, 3387.5)), \
"setup moved half risk AGAINST the entry — must re-alert too"
tb._last_alert_fp.pop(key, None)
tb._last_alert_ts.pop(key, None)
tb._last_alert_setup.pop(key, None)
@test("Backtester --confirm-tfs defaults to None (factory's live map)")
@@ -2250,6 +2369,31 @@ def _():
assert t2.entry_time == idx[2] and t2.entry_price == 110.0 and t2.sl == 102.0
@test("Max drawdown counts the decline from flat starting equity")
def _():
from gpu_backtest import Trade, compute_metrics
def mk(r):
t = Trade(pair="XAU/USD",
entry_time=pd.Timestamp("2026-01-05 10:00", tz="UTC"),
entry_price=3300.0, sl=3292.0, tp1=3312.0, tp2=3320.0,
direction="BUY", pip_value=0.10, risk_pips=80.0,
reward_pips=120.0, rr_planned=1.5, quality="B",
final_score=30.0, final_confidence=60.0,
market_regime="trending_up")
t.exit_reason = "SL" if r < 0 else "TP1"
t.r_multiple = r
return t
# Opening losing streak: equity 0 → −3R with no prior peak. The old
# high-water started at the first trade's cum (−1) and reported 2R.
m = compute_metrics([mk(-1.0), mk(-1.0), mk(-1.0)])
assert m.max_drawdown_r == 3.0, f"expected 3.0, got {m.max_drawdown_r}"
# Unchanged when a peak precedes the streak.
m2 = compute_metrics([mk(2.0), mk(-1.0), mk(-1.0), mk(-1.0)])
assert m2.max_drawdown_r == 3.0, f"expected 3.0, got {m2.max_drawdown_r}"
# ══════════════════════════════════════════════════════════════════════
# MODULE 12: Chart-pattern entry (XAU_CP)
# ══════════════════════════════════════════════════════════════════════
@@ -2997,7 +3141,10 @@ def _():
"w", suffix=".env", delete=False, encoding="utf-8")
fh.write("# comment\n\nPLAIN=abc\nexport EXPORTED=def\n"
'QUOTED="with spaces"\nSINGLE=\'sq\'\nEXISTING=from-dotenv\n'
"NOT A KV LINE\n")
"NOT A KV LINE\n"
"COMMENTED=abc123 # prod key\n"
'QUOTED_HASH="keep #this"\n'
"HASH_IN_VALUE=abc#123\n")
fh.close()
with patch.dict(os.environ, {"EXISTING": "from-shell"}, clear=True):
load_dotenv(fh.name)
@@ -3007,6 +3154,12 @@ def _():
assert os.environ["SINGLE"] == "sq"
assert os.environ["EXISTING"] == "from-shell", \
"a real shell export must beat .env"
assert os.environ["COMMENTED"] == "abc123", \
"inline comment must be stripped, not absorbed into the value"
assert os.environ["QUOTED_HASH"] == "keep #this", \
"quoted values keep their hashes"
assert os.environ["HASH_IN_VALUE"] == "abc#123", \
"a hash with no preceding whitespace is part of the value"
load_dotenv("/nonexistent/.env") # missing file is a silent no-op