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>
This commit is contained in:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
d441454767
commit
fe10be5ecc
@@ -0,0 +1,108 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user