"""Pipeline Fase 3 end-to-end. 1. carica incroci + baseline 2. tiene da parte l'hold-out OOS (>= oos_start_year) 3. walk-forward espansivo sul periodo di sviluppo: - per ogni fold: fit baseline+terzili sul SOLO train, costruisce le label, addestra il modello, valuta sul fold di test 4. riaddestra sul periodo di sviluppo completo e valuta sull'hold-out OOS 5. salva metriche e modello finale Uso: python train.py [config.yaml] """ from __future__ import annotations import os, sys, json import numpy as np import pandas as pd import yaml HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) from data import load from labeling import RegimeBaseline from features import select_xy from splits import split_oos, walk_forward_folds from model import make_model from evaluate import metrics RESULTS = os.path.normpath(os.path.join(HERE, "..", "results")) def load_cfg(path: str) -> dict: with open(path) as f: return yaml.safe_load(f) def run(cfg_path: str): cfg = load_cfg(cfg_path) np.random.seed(cfg["seed"]) num_cols = cfg["features_numeric"] cat_cols = cfg["features_categorical"] h = cfg["horizon"] crosses, bars = load(cfg["symbol"], cfg["year_min"]) if cfg.get("restrict_pairs"): crosses = crosses[crosses.pair.isin(cfg["restrict_pairs"])].reset_index(drop=True) # serve cret_h presente (le ultime barre non hanno il futuro completo) crosses = crosses.dropna(subset=[f"cret_{h}"]).reset_index(drop=True) dev_cr, oos_cr = split_oos(crosses, cfg["oos_start_year"]) dev_ba, oos_ba = split_oos(bars, cfg["oos_start_year"]) print(f"sviluppo: {len(dev_cr)} incroci | OOS({cfg['oos_start_year']}+): {len(oos_cr)} incroci") # ---------- walk-forward ---------- fold_rows = [] for i, (tr_idx, te_idx) in enumerate(walk_forward_folds( dev_cr, cfg["walkforward"]["min_train_years"], cfg["walkforward"]["step_years"])): tr, te = dev_cr.loc[tr_idx], dev_cr.loc[te_idx] # baseline/terzili stimati SOLO sul train (barre fino all'ultimo anno di train) max_train_year = tr.time.dt.year.max() ba_tr = dev_ba[dev_ba.time.dt.year <= max_train_year] rb = RegimeBaseline(cfg["regime"], h).fit(ba_tr) ytr, yte = rb.label(tr), rb.label(te) mdl = make_model(cfg, num_cols, cat_cols) mdl.fit(select_xy(tr, num_cols, cat_cols), ytr) pte = mdl.predict_proba(select_xy(te, num_cols, cat_cols))[:, 1] m = metrics(yte, pte) m["fold"] = i m["test_years"] = f"{te.time.dt.year.min()}-{te.time.dt.year.max()}" fold_rows.append(m) print(f" fold {i} test {m['test_years']:>9} n={m['n']:5d} " f"AUC={m['auc']:.3f} acc={m['acc']:.3f} " f"prec@10%={m['prec_top_decile']:.3f} (base {m['base_rate']:.3f})") folds = pd.DataFrame(fold_rows) if len(folds): print(f"\nWalk-forward medio: AUC={folds.auc.mean():.3f} " f"acc={folds.acc.mean():.3f} prec@10%={folds.prec_top_decile.mean():.3f}") # ---------- modello finale + OOS ---------- rb_full = RegimeBaseline(cfg["regime"], h).fit(dev_ba) y_dev = rb_full.label(dev_cr) final = make_model(cfg, num_cols, cat_cols) final.fit(select_xy(dev_cr, num_cols, cat_cols), y_dev) y_oos = rb_full.label(oos_cr) # baseline fittata sullo sviluppo: no leakage p_oos = final.predict_proba(select_xy(oos_cr, num_cols, cat_cols))[:, 1] oos_m = metrics(y_oos, p_oos) print(f"\nHOLD-OUT OOS {cfg['oos_start_year']}+ : n={oos_m['n']} AUC={oos_m['auc']:.3f} " f"acc={oos_m['acc']:.3f} prec@10%={oos_m['prec_top_decile']:.3f} " f"(base {oos_m['base_rate']:.3f}, lift {oos_m['lift_top_decile']:.2f}x)") os.makedirs(RESULTS, exist_ok=True) folds.to_csv(os.path.join(RESULTS, "walkforward_folds.csv"), index=False) with open(os.path.join(RESULTS, "oos_metrics.json"), "w") as f: json.dump(oos_m, f, indent=2) try: import joblib joblib.dump(final, os.path.join(RESULTS, "model_final.joblib")) except Exception as e: print("modello non serializzato:", e) print(f"\nsalvati risultati in {RESULTS}") return folds, oos_m if __name__ == "__main__": cfg_path = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "..", "config.yaml") run(cfg_path)