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