#!/usr/bin/env python3 """ ================================================================ Auteur : Simon-Pierre Boucher Contact : contact@spboucher.ai Projet : Prévision de volatilité réalisée multi-actifs (HAR-RV vs GARCH vs Machine Learning) Fichier : 03_run_models.py Description : Étape 03 — Prévisions out-of-sample roulantes pour toutes les familles de modèles (HAR, GARCH, ML, deep, pooled), parallélisées par actif. Usage : python scripts/03_run_models.py --family econ|ml|deep|pooled|all [--tickers SPY,BTC] [--workers 8] ================================================================ """ from __future__ import annotations import argparse import logging import sys from concurrent.futures import ProcessPoolExecutor, as_completed from pathlib import Path import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from wp12 import config # noqa: E402 logger = logging.getLogger("03_run_models") FC_DIR = config.path("processed") / "forecasts" HORIZONS = tuple(config.load_config()["evaluation"]["horizons"]) WINDOW = int(config.load_config()["evaluation"]["estimation_window_days"]) REFIT = int(config.load_config()["evaluation"]["reestimation_freq_days"]) def _load_rv(ticker: str) -> pd.DataFrame: return pd.read_parquet(config.path("processed") / f"rv_{ticker}.parquet") def run_econ(ticker: str) -> str: """All HAR-family and GARCH-family forecasts for one ticker.""" from wp12 import models_econ as me df = _load_rv(ticker) parts = [] for spec in ["HAR", "HAR-J", "HAR-CJ", "SHAR", "HARQ", "LogHAR"]: parts.append(me.rolling_har_forecasts( df, spec, horizons=HORIZONS, window=WINDOW, refit_every=REFIT)) for kind in ["GARCH", "GJR", "EGARCH"]: parts.append(me.rolling_garch_forecasts( df, kind, horizons=HORIZONS, window=WINDOW, refit_every=REFIT)) parts.append(me.rolling_realgarch_forecasts( df, horizons=HORIZONS, window=WINDOW, refit_every=REFIT)) out = pd.concat(parts, ignore_index=True) out["ticker"] = ticker f = FC_DIR / f"econ_{ticker}.parquet" out.to_parquet(f, index=False) return f"{ticker}: {len(out)} econ forecasts" def run_ml(ticker: str) -> str: """Tabular ML forecasts (2 feature sets x 6 estimators) for one ticker.""" from wp12 import models_ml as ml panel = pd.read_parquet(config.path("processed") / "panel.parquet") frames = ml.ml_features(panel) frame = frames[ticker] parts = [] for name in ["LASSO", "Ridge", "ElasticNet", "RF", "XGBoost", "LightGBM"]: for fs in ["har", "extended"]: parts.append(ml.rolling_ml_forecasts( frame, name, feature_set=fs, horizons=HORIZONS, window=WINDOW, refit_every=REFIT)) out = pd.concat(parts, ignore_index=True) out["ticker"] = ticker f = FC_DIR / f"ml_{ticker}.parquet" out.to_parquet(f, index=False) return f"{ticker}: {len(out)} ML forecasts" def run_deep(ticker: str) -> str: """LSTM and Transformer forecasts for one ticker.""" from wp12 import models_ml as ml panel = pd.read_parquet(config.path("processed") / "panel.parquet") frames = ml.ml_features(panel) frame = frames[ticker] parts = [ ml.rolling_deep_forecasts(frame, arch, horizons=HORIZONS, window=WINDOW) for arch in ["LSTM", "Transformer"] ] out = pd.concat(parts, ignore_index=True) out["ticker"] = ticker f = FC_DIR / f"deep_{ticker}.parquet" out.to_parquet(f, index=False) return f"{ticker}: {len(out)} deep forecasts" def run_interpret() -> str: """SHAP values and permutation importance of the pooled LightGBM.""" from wp12 import models_ml as ml panel = pd.read_parquet(config.path("processed") / "panel.parquet") frames = ml.ml_features(panel) shap_tab, perm_tab = ml.shap_and_permutation(frames, h=1, window=WINDOW) out = config.path("reproduced") shap_tab.to_csv(out / "shap_h1.csv", index=False) perm_tab.to_csv(out / "permutation_h1.csv", index=False) return f"interpret: {len(shap_tab)} features" def run_pooled() -> str: """Pooled multi-asset LightGBM (single job, all tickers jointly).""" from wp12 import models_ml as ml panel = pd.read_parquet(config.path("processed") / "panel.parquet") frames = ml.ml_features(panel) out = ml.rolling_pooled_lgbm( frames, horizons=HORIZONS, window=WINDOW, refit_every=REFIT) f = FC_DIR / "pooled.parquet" out.to_parquet(f, index=False) return f"pooled: {len(out)} forecasts" def main() -> None: """Dispatch model runs across tickers with a process pool.""" ap = argparse.ArgumentParser() ap.add_argument("--family", default="all", choices=["econ", "ml", "deep", "pooled", "interpret", "all"]) ap.add_argument("--tickers", default=None, help="comma-separated subset") ap.add_argument("--workers", type=int, default=8) args = ap.parse_args() config.setup_logging() FC_DIR.mkdir(parents=True, exist_ok=True) tickers = ( args.tickers.split(",") if args.tickers else [i["ticker"] for i in config.universe()] ) jobs: list[tuple] = [] if args.family in ("econ", "all"): jobs += [(run_econ, tk) for tk in tickers] if args.family in ("ml", "all"): jobs += [(run_ml, tk) for tk in tickers] if args.family in ("deep", "all"): jobs += [(run_deep, tk) for tk in tickers] if jobs: with ProcessPoolExecutor(max_workers=args.workers) as pool: futs = {pool.submit(fn, tk): (fn.__name__, tk) for fn, tk in jobs} for fut in as_completed(futs): name, tk = futs[fut] try: logger.info("done %s %s -> %s", name, tk, fut.result()) except Exception as exc: # noqa: BLE001 logger.error("FAILED %s %s: %s", name, tk, exc) if args.family in ("pooled", "all"): logger.info(run_pooled()) if args.family in ("interpret", "all"): logger.info(run_interpret()) if __name__ == "__main__": main()