Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
109 lines
4.3 KiB
Python
109 lines
4.3 KiB
Python
"""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)
|