"""Download XAUUSD M5 history to Parquet (doc 02 §3, doc 07 §6). Pulls the full M5 window from the running MT5 terminal and saves it as a Parquet file in ``data/``. Re-runs of the engine then load from Parquet (fast, compact, no MT5 connection needed). M5 is the EA's signal timeframe — higher timeframes are resampled in Python from this M1/M5 base. Connects to the already-running, manually-logged-in terminal (initialize() with no path → reuse). Keep the terminal UI open while this runs. """ from __future__ import annotations import sys import time from datetime import datetime, timedelta from pathlib import Path PROJECT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(PROJECT)) from shared.config import get_secret, load_env load_env(PROJECT) import MetaTrader5 as mt5 # type: ignore import pandas as pd SYMBOL = "XAUUSD" TIMEFRAME = "M5" # Full window: 2 years back to today (matches the history depth probe). START = (datetime.now() - timedelta(days=730)).strftime("%Y-%m-%d") END = datetime.now().strftime("%Y-%m-%d") OUT = PROJECT / "data" / f"{SYMBOL}_{TIMEFRAME}_{START}_{END}.parquet" def connect(retries: int = 5, sleep_s: float = 5.0) -> bool: """Connect to the running terminal (initialize() with no path → reuse).""" for attempt in range(1, retries + 1): if not mt5.initialize(): print(f" initialize() attempt {attempt}/{retries} failed: {mt5.last_error()}") time.sleep(sleep_s) continue login = int(get_secret("MT5_DEMO_LOGIN") or 0) password = get_secret("MT5_DEMO_PASSWORD") server = get_secret("MT5_DEMO_SERVER") if not mt5.login(login, password=password, server=server): print(f" login() attempt {attempt}/{retries} failed: {mt5.last_error()}") mt5.shutdown() time.sleep(sleep_s) continue print(f" connected: login={login} server={server}") return True return False def main() -> int: if not connect(): print("could not connect — is the terminal running and logged in?") return 1 try: tf = getattr(mt5, f"TIMEFRAME_{TIMEFRAME}") print(f"downloading {SYMBOL} {TIMEFRAME} {START} → {END} ...") rates = mt5.copy_rates_range( SYMBOL, tf, pd.Timestamp(START), pd.Timestamp(END) ) if rates is None or len(rates) == 0: print(f" no bars returned: {mt5.last_error()}") return 2 df = pd.DataFrame(rates) df["timestamp"] = pd.to_datetime(df["time"], unit="s", utc=False) df = df[["timestamp", "open", "high", "low", "close", "spread"]] df = df.sort_values("timestamp").reset_index(drop=True) # Deduplicate (MT5 occasionally returns overlapping bars near boundaries). df = df.drop_duplicates(subset=["timestamp"]).reset_index(drop=True) OUT.parent.mkdir(parents=True, exist_ok=True) df.to_parquet(OUT, index=False) print(f"saved {len(df):,} bars → {OUT.relative_to(PROJECT)}") print(f"range: {df['timestamp'].iloc[0]} → {df['timestamp'].iloc[-1]}") print(f"spread stats: min={df['spread'].min()} max={df['spread'].max()} " f"median={df['spread'].median():.1f}") return 0 finally: mt5.shutdown() if __name__ == "__main__": raise SystemExit(main())