Switch active data source to Oanda v20 (practice feed)

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).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mc7XKYyPySVXiUpcN82QEK
This commit is contained in:
Harwinder Sekhon
2026-07-28 20:12:59 -04:00
parent c8beb7e3ae
commit d2d4fb4ac6
4 changed files with 33 additions and 6 deletions
+2 -2
View File
@@ -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.
---
+11 -3
View File
@@ -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)
+2 -1
View File
@@ -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"
}
+18
View File
@@ -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
# ══════════════════════════════════════════════════════════════════════