Files
TR_Agent/PaPP v2/Modello/src/train.py
T
Pietro GiacobazziandDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> fe10be5ecc PaPP v2: Fase 3 - architettura classificatore extra-rendimento (walk-forward + OOS)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-17 07:45:46 +00:00

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)