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). Claude-Session: https://claude.ai/code/session_01Mc7XKYyPySVXiUpcN82QEK
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# ForexBot2 — Codebase Reference
|
# 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.
|
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.
|
**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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -935,9 +935,17 @@ class GPUTechnicalEngine(TechnicalEngine):
|
|||||||
CACHE_DIR = Path(__file__).resolve().parent / "cache"
|
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", "")
|
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(
|
def fetch_history(
|
||||||
@@ -962,7 +970,7 @@ def fetch_history(
|
|||||||
cleaned data; a refetch is needed to restore dropped spike bars.)
|
cleaned data; a refetch is needed to restore dropped spike bars.)
|
||||||
"""
|
"""
|
||||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
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():
|
if use_cache and path.exists():
|
||||||
logger.info(f"Loading cached {symbol} {timeframe.value}: {path.name}")
|
logger.info(f"Loading cached {symbol} {timeframe.value}: {path.name}")
|
||||||
df = pd.read_parquet(path)
|
df = pd.read_parquet(path)
|
||||||
|
|||||||
+2
-1
@@ -6,10 +6,11 @@
|
|||||||
"alert_min_quality": "C"
|
"alert_min_quality": "C"
|
||||||
},
|
},
|
||||||
"data_providers": {
|
"data_providers": {
|
||||||
"active": "twelve_data",
|
"active": "oanda",
|
||||||
"twelve_data": {
|
"twelve_data": {
|
||||||
"api_key": "REPLACE_WITH_YOUR_TWELVE_DATA_API_KEY"
|
"api_key": "REPLACE_WITH_YOUR_TWELVE_DATA_API_KEY"
|
||||||
},
|
},
|
||||||
|
"oanda": {},
|
||||||
"tiingo": {
|
"tiingo": {
|
||||||
"api_key": "REPLACE_WITH_YOUR_TIINGO_API_KEY_OR_EMPTY"
|
"api_key": "REPLACE_WITH_YOUR_TIINGO_API_KEY_OR_EMPTY"
|
||||||
}
|
}
|
||||||
|
|||||||
+18
@@ -4291,6 +4291,24 @@ def _():
|
|||||||
"aggregator must not depend on a concrete provider class"
|
"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
|
# SUMMARY
|
||||||
# ══════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user