6d235061b9
- data_provider: new fetch_historical() that paginates /time_series in 5000-bar chunks (1 credit/chunk) for arbitrary date ranges. Tolerates the API's "no data on specified dates" boundary error gracefully when prior chunks succeeded — that error is just the marker that we walked past the symbol's earliest available bar. - seed_history.py: one-shot CLI that downloads OHLCV for the configured pairs/TFs into history/<PAIR>_<TF>.csv. Skips existing files unless --force. - backtest.py: walk-forward replay through SignalAggregator with strict no-look-ahead semantics. Each bar is only visible to the engine after it has fully closed (`df.index + interval <= as_of`). Trade simulation is first-touch (SL vs TP1) with worst-case (SL first if both touched same bar), spread cost deducted from R, max-hold timeout, no-overlap. Reports per-grade win-rate / avg-R / profit-factor / max-DD, plus optional matplotlib equity + by-grade plots. - requirements: add matplotlib (lazy-imported, only needed for --plot). - .gitignore: history/ and backtest_results/ regenerable, kept untracked. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
134 lines
4.4 KiB
Python
134 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
One-shot historical OHLCV seeder for backtesting.
|
||
|
||
Downloads bars from Twelve Data for the configured pairs/timeframes,
|
||
paginated to stay within the 5000-bar per-call limit, and writes each
|
||
(symbol, timeframe) to history/<PAIR>_<TF>.csv.
|
||
|
||
Usage:
|
||
python seed_history.py --years 1
|
||
python seed_history.py --years 2 --pairs EURUSD GBPUSD USDJPY
|
||
python seed_history.py --tfs M15 H1 D1 --years 1 --force
|
||
|
||
Cost (free tier, 800 credits/day):
|
||
1 year M15 + H1 + D1 × 7 majors ≈ 56 credits
|
||
5 years same ≈ 231 credits
|
||
"""
|
||
|
||
import argparse
|
||
import logging
|
||
import sys
|
||
from datetime import datetime, timedelta, timezone
|
||
from pathlib import Path
|
||
|
||
try:
|
||
from dotenv import load_dotenv
|
||
load_dotenv()
|
||
except ImportError:
|
||
pass
|
||
|
||
from config import MAJOR_PAIRS, PlatformConfig, Timeframe
|
||
from data_provider import DataUnavailableError, ForexDataProvider
|
||
|
||
DEFAULT_TFS = ["M15", "H1", "D1"]
|
||
HISTORY_DIR = Path("history")
|
||
|
||
_TF_MAP = {
|
||
"M1": Timeframe.M1, "M5": Timeframe.M5,
|
||
"M15": Timeframe.M15, "M30": Timeframe.M30,
|
||
"H1": Timeframe.H1, "H4": Timeframe.H4,
|
||
"D1": Timeframe.D1, "W1": Timeframe.W1,
|
||
}
|
||
|
||
|
||
def csv_path(out_dir: Path, symbol: str, tf: Timeframe) -> Path:
|
||
return out_dir / f"{symbol.replace('=X', '')}_{tf.value}.csv"
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(
|
||
description="Seed historical OHLCV for backtesting (Twelve Data, free tier).",
|
||
)
|
||
parser.add_argument("--years", type=float, default=1.0,
|
||
help="Years of history to download (default 1.0)")
|
||
parser.add_argument("--pairs", nargs="+", default=None,
|
||
help="Pair symbols, e.g. EURUSD GBPUSD (default all 7 majors)")
|
||
parser.add_argument("--tfs", nargs="+", default=DEFAULT_TFS,
|
||
help=f"Timeframes (default {' '.join(DEFAULT_TFS)})")
|
||
parser.add_argument("--out", default=str(HISTORY_DIR),
|
||
help="Output directory (default history/)")
|
||
parser.add_argument("--force", action="store_true",
|
||
help="Re-download even if a CSV already exists")
|
||
args = parser.parse_args()
|
||
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||
datefmt="%H:%M:%S",
|
||
)
|
||
|
||
out_dir = Path(args.out)
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
pairs = args.pairs if args.pairs else MAJOR_PAIRS
|
||
pairs = [p if p.endswith("=X") else f"{p}=X" for p in pairs]
|
||
|
||
try:
|
||
tfs = [_TF_MAP[t.upper()] for t in args.tfs]
|
||
except KeyError as e:
|
||
print(f"❌ Unknown timeframe: {e}. Valid: {list(_TF_MAP.keys())}")
|
||
return 2
|
||
|
||
end = datetime.now(timezone.utc)
|
||
start = end - timedelta(days=int(args.years * 365))
|
||
|
||
provider = ForexDataProvider(PlatformConfig())
|
||
|
||
total = len(pairs) * len(tfs)
|
||
print(f"📥 Seeding {total} pair × tf combinations from "
|
||
f"{start.date()} to {end.date()} ({args.years} year(s))")
|
||
print(f" Pairs: {[p.replace('=X', '') for p in pairs]}")
|
||
print(f" TFs: {[tf.value for tf in tfs]}")
|
||
print(f" Output: {out_dir}/\n")
|
||
|
||
done = 0
|
||
failed: list = []
|
||
for symbol in pairs:
|
||
for tf in tfs:
|
||
done += 1
|
||
out_path = csv_path(out_dir, symbol, tf)
|
||
|
||
if out_path.exists() and not args.force:
|
||
print(f"[{done}/{total}] ⏭ {symbol} {tf.value} — exists, "
|
||
f"skipping (--force to overwrite)")
|
||
continue
|
||
|
||
print(f"[{done}/{total}] 📥 {symbol} {tf.value}…", flush=True)
|
||
try:
|
||
df = provider.fetch_historical(symbol, tf, start, end)
|
||
df.to_csv(out_path)
|
||
print(f" ✅ {len(df):,} bars "
|
||
f"({df.index.min().date()} → {df.index.max().date()}) "
|
||
f"→ {out_path}")
|
||
except DataUnavailableError as e:
|
||
print(f" ❌ {e}")
|
||
failed.append((symbol, tf.value, str(e)))
|
||
except Exception as e:
|
||
print(f" ❌ unexpected: {e}")
|
||
failed.append((symbol, tf.value, str(e)))
|
||
|
||
print()
|
||
if failed:
|
||
print(f"⚠️ {len(failed)} fetch(es) failed:")
|
||
for sym, tfn, msg in failed:
|
||
print(f" {sym} {tfn}: {msg}")
|
||
return 1
|
||
|
||
print("✅ Seeding complete. Run: python backtest.py --primary M15")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|