audit: alert delivery, provider robustness, backtester NaN/cache guards (Batches C+E+F)
2026-08-04 Fable 5 QA audit. Three batches sharing one test file, all verified by runnable repros before fixing, all gated below. Batch C — telegram alert delivery (verified by h4 harness): - Failed breach rescan shrinks the cooldown to 30 s instead of silencing the symbol for 5 min; breach during cooldown is PARKED and fired on the first tick after expiry (was dropped forever). - Level cache keyed symbol→TF (M5 scans no longer wipe H1/H4 swing levels within minutes); breach labels TF-qualified. - Markdown alert sends get a plain-text fallback (mirroring _safe_edit); fingerprint commits only when a message actually landed. - Per-key asyncio.Lock closes the dedup TOCTOU between auto-scan and breach sends (forced-interleave repro produced duplicate alerts). - _start_scheduler respawns dead per-TF loops (was all-or-nothing). - Clean WS close now backs off (was a zero-delay reconnect hot loop); backoff resets only after a connection that received ticks; TD iter_ticks fires on_connect on the first RECEIVED frame (invalid key can no longer pin the backoff at minimum). - Scan-failure logs carry the real traceback (was "NoneType: None"). Batch E — providers (verified by h5 harness): - fetch_range: 3 consecutive chunk ERRORS raise instead of returning silently truncated history (verified 731-day silent gap repro; also covers 429 storms); pagination no-progress guard; head-truncation ERROR log. - Single-flight per (symbol,TF): 4 concurrent scans = 1 API call (was 4); cache records requested depth so a longer-lookback call refetches. - Multi-bar forming/future tail fully dropped (was iloc[:-1] once). - Oanda: malformed candles skipped, never 0.0-defaulted; cleaning drops non-positive-price rows BEFORE OHLC repair; inf returns neutralized so one corrupt bar can no longer disarm the whole 10-sigma filter. Batch F — backtester internals (verified by h6 harness): - NaN bar in a trailing window exits at the last valid bar (was NaN R poisoning every metric); static-path TIMEOUT NaN guard; compute_metrics excludes non-finite R loudly. Zero real-data exposure (0 NaN rows in all 10 Oanda parquets) — defensive. - fetch_history ERROR-logs pre-2026-07-08 cache files (cleaned-bars era) when loaded for raw-path resolution. Gates: suite 333/333 green; pinned M15 12mo book reproduced identically (397 trades, +78.7111R) after E and after F; M5 HEAD-config baseline reproduced identically (556 trades, +97.5604R) after F. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UnZkzeFcTu9agFg8BDh2tn
This commit is contained in:
@@ -210,6 +210,12 @@ class TwelveDataProvider(BaseDataProvider):
|
||||
The TD WS speaks display symbols ("XAU/USD"); ticks are mapped back
|
||||
to the canonical pair symbols passed in. The caller owns the
|
||||
reconnect/backoff loop around this generator.
|
||||
|
||||
`on_connect` fires on the FIRST RECEIVED frame, not after the
|
||||
subscribe send (2026-08-04 audit H38b, mirroring the Tiingo
|
||||
provider) — firing pre-receive let a rejected API key reset the
|
||||
caller's reconnect backoff on every attempt, pinning it at the
|
||||
minimum forever.
|
||||
"""
|
||||
import websockets
|
||||
|
||||
@@ -222,11 +228,14 @@ class TwelveDataProvider(BaseDataProvider):
|
||||
}
|
||||
await ws.send(json.dumps(subscribe))
|
||||
logger.info(f"WebSocket connected, subscribed to {len(ws_to_pair)} symbols")
|
||||
if on_connect is not None:
|
||||
on_connect()
|
||||
hb_task = asyncio.create_task(self._ws_heartbeat(ws))
|
||||
connected = False
|
||||
try:
|
||||
async for raw in ws:
|
||||
if not connected:
|
||||
connected = True
|
||||
if on_connect is not None:
|
||||
on_connect()
|
||||
parsed = self._parse_ws_price(raw)
|
||||
if parsed is None:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user