From 637c39e2014ca7c902ef0076ee78b7fe4e4f6844 Mon Sep 17 00:00:00 2001 From: Harwinder Sekhon Date: Tue, 28 Jul 2026 20:12:59 -0400 Subject: [PATCH] Switch active data source to Oanda v20 (practice feed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit config.json.example: data_providers.active = "oanda" (credentials stay in env/.env or the top-level oanda block; TD and Tiingo remain registered fallbacks). Backtest parquet cache is now keyed per provider — Oanda and TD bars diverge (2026-07-24 study), so a feed switch must never serve another feed's cached bars; twelve_data keeps the legacy un-prefixed filename so pinned-book caches stay valid. 254 tests green; live smoke-tested against the practice API (66 contract checks: all scanned TFs, boundary/freshness guarantees, real spread $0.71, fetch_range pagination, typed error paths, CLI + backtest end-to-end). Claude-Session: https://claude.ai/code/session_01Mc7XKYyPySVXiUpcN82QEK --- CLAUDE.md | 4 ++-- backtest/gpu_backtest.py | 14 +++++++++++--- config.json.example | 3 ++- test_all.py | 18 ++++++++++++++++++ 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6349e12..26a3a15 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # ForexBot2 — Codebase Reference -Signal-intelligence platform for **XAU/USD (gold — live, validated)**. It fetches OHLCV from Twelve Data, scores 9 technical indicators per timeframe with regime-adaptive weights, checks multi-timeframe agreement and news/calendar risk, grades the signal (A+…F), and generates a full trade setup (entry, SL, TP1/TP2, trailing plan). Delivery: Telegram bot (auto-scanner + subscriber channel) and a CLI. A realism-focused backtester replays history through the *exact same* signal code. +Signal-intelligence platform for **XAU/USD (gold — live, validated)**. It fetches OHLCV from the active data provider (**Oanda v20 since 2026-07-28**; Twelve Data and Tiingo stay registered as fallbacks — config.json `data_providers.active` switches), scores 9 technical indicators per timeframe with regime-adaptive weights, checks multi-timeframe agreement and news/calendar risk, grades the signal (A+…F), and generates a full trade setup (entry, SL, TP1/TP2, trailing plan). Delivery: Telegram bot (auto-scanner + subscriber channel) and a CLI. A realism-focused backtester replays history through the *exact same* signal code. This file is the authoritative developer/agent reference. The human-facing docs live in `docs/` (`user_guide.html` + `beginner_guide.html`, and their PDF builds) — keep them in sync when signal behavior changes. `backtest/instructions_for_backtest.txt` lists what every backtest report must record. @@ -329,7 +329,7 @@ Report: summary metrics (total R, PF, Sharpe on R, max DD, MFE/MAE), breakdowns **Comparability note (2026-07-16):** the parity/realism fixes of that date (EMA-ribbon port to the GPU engine, exact-pandas Bollinger precompute, raw-path entry re-anchoring, per-TF D1 warmup, live confirm-map CLI default, outlier snap-back retention) all shift books slightly vs earlier runs. Books produced before these fixes are not directly comparable — re-run the baseline before any before/after tuning comparison. -Data: paginated fetches via `provider.fetch_range` (5000-bar chunks; works with any registered provider) cached as parquet in `backtest/cache/` keyed by symbol/TF/window; warmup pad ≥60 days before the window for EMA200, extended per-TF so slow confirmation TFs get ≥210 bars at window start (D1 gets ~299 days — 62 days gave only ~61 bars: no EMA200, unlike live's 300-bar outputsize floor). `--confirm-tfs` defaults to the pair factory's live confirmation map (pass it explicitly to override); a hardcoded default here previously made the standard invocation backtest a different MTF setup than the bot scans. +Data: paginated fetches via `provider.fetch_range` (5000-bar chunks; works with any registered provider) cached as parquet in `backtest/cache/` keyed by **provider**/symbol/TF/window (bars differ between feeds — Oanda/TD divergence study; `twelve_data` keeps the legacy un-prefixed filename so pinned-book caches stay valid); warmup pad ≥60 days before the window for EMA200, extended per-TF so slow confirmation TFs get ≥210 bars at window start (D1 gets ~299 days — 62 days gave only ~61 bars: no EMA200, unlike live's 300-bar outputsize floor). `--confirm-tfs` defaults to the pair factory's live confirmation map (pass it explicitly to override); a hardcoded default here previously made the standard invocation backtest a different MTF setup than the bot scans. --- diff --git a/backtest/gpu_backtest.py b/backtest/gpu_backtest.py index 68d634c..7d5d6a7 100644 --- a/backtest/gpu_backtest.py +++ b/backtest/gpu_backtest.py @@ -935,9 +935,17 @@ class GPUTechnicalEngine(TechnicalEngine): CACHE_DIR = Path(__file__).resolve().parent / "cache" -def _cache_path(symbol: str, tf: Timeframe, start: datetime, end: datetime) -> Path: +def _cache_path( + symbol: str, tf: Timeframe, start: datetime, end: datetime, + provider_name: str = "twelve_data", +) -> Path: safe_sym = symbol.replace("/", "_").replace("=X", "") - return CACHE_DIR / f"{safe_sym}_{tf.value}_{start:%Y%m%d}_{end:%Y%m%d}.parquet" + # Bars differ between feeds (Oanda vs TD divergence study 2026-07-24), + # so the cache is keyed per provider. twelve_data keeps the legacy + # un-prefixed filename so pinned-book caches stay valid. + prefix = "" if provider_name == "twelve_data" else f"{provider_name}_" + return CACHE_DIR / ( + f"{prefix}{safe_sym}_{tf.value}_{start:%Y%m%d}_{end:%Y%m%d}.parquet") def fetch_history( @@ -962,7 +970,7 @@ def fetch_history( cleaned data; a refetch is needed to restore dropped spike bars.) """ CACHE_DIR.mkdir(parents=True, exist_ok=True) - path = _cache_path(symbol, timeframe, start, end) + path = _cache_path(symbol, timeframe, start, end, provider.name) if use_cache and path.exists(): logger.info(f"Loading cached {symbol} {timeframe.value}: {path.name}") df = pd.read_parquet(path) diff --git a/config.json.example b/config.json.example index 441ae22..09e4e8d 100644 --- a/config.json.example +++ b/config.json.example @@ -6,10 +6,11 @@ "alert_min_quality": "C" }, "data_providers": { - "active": "twelve_data", + "active": "oanda", "twelve_data": { "api_key": "REPLACE_WITH_YOUR_TWELVE_DATA_API_KEY" }, + "oanda": {}, "tiingo": { "api_key": "REPLACE_WITH_YOUR_TIINGO_API_KEY_OR_EMPTY" } diff --git a/test_all.py b/test_all.py index 87d4f1a..87078d8 100644 --- a/test_all.py +++ b/test_all.py @@ -4291,6 +4291,24 @@ def _(): "aggregator must not depend on a concrete provider class" +@test("providers: backtest parquet cache is keyed per provider") +def _(): + # Bars differ between feeds (Oanda/TD divergence study 2026-07-24) — + # a provider switch must never serve another feed's cached bars. + # twelve_data keeps the legacy un-prefixed name so pinned-book caches + # stay valid. + from datetime import datetime + from backtest.gpu_backtest import _cache_path + s, e = datetime(2026, 1, 1), datetime(2026, 2, 1) + td = _cache_path(XAUUSD, Timeframe.M15, s, e, "twelve_data") + oa = _cache_path(XAUUSD, Timeframe.M15, s, e, "oanda") + legacy = _cache_path(XAUUSD, Timeframe.M15, s, e) + assert td.name == "XAU_USD_15min_20260101_20260201.parquet", td.name + assert oa.name == "oanda_XAU_USD_15min_20260101_20260201.parquet", oa.name + assert legacy == td, "default must stay the legacy twelve_data key" + assert td != oa, "providers must not share cache files" + + # ══════════════════════════════════════════════════════════════════════ # SUMMARY # ══════════════════════════════════════════════════════════════════════