PaPP v2 Modello: validazione esterna multi-simbolo (no pollution)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Pietro Giacobazzi
2026-06-17 09:21:43 +00:00
parent 58ed32ff3b
commit 0187cb6bd7
3 changed files with 133 additions and 0 deletions
+12
View File
@@ -76,6 +76,18 @@ Con il baseline `hist_gbdt` e tutte le coppie:
Lettura onesta: **vicino al caso**. È coerente con la Fase 2 (edge direzionali
piccoli) e ci dà un riferimento pulito e senza leakage da cui partire.
### 5b. Validazione esterna multi-simbolo (`src/external_validation.py`)
Addestrando SOLO su EURUSD e testando su USDJPY/USDCHF/GBPUSD (mai visti):
- i pattern direzionali "robusti" di EURUSD **non si replicano**: concordanza di
segno solo **38%**; il modello esterno ha **AUC ≈ 0.480.55** (caso) contro
AUC 0.91 in-sample (memorizzazione) → l'edge direzionale è **specifico di
EURUSD**, non generale.
- al contrario la **mean-reversion è universale**: ritorno alla Mediana entro 20g
nell'**8790%** dei casi su tutti i simboli, mediana **3 giorni**.
Conclusione operativa: il bersaglio "su/giù" non regge fuori campione; il segnale
generalizzabile e' la **mean-reversion**. Il modeling va orientato lì.
## 6. Prossimi miglioramenti previsti
- **Restringere ai pattern robusti** (q<0.10): in `config.yaml` valorizzare
@@ -0,0 +1,5 @@
symbol,n,base_rate,auc,acc,brier,prec_top_decile,lift_top_decile
EURUSD (in-sample),16895,0.4976028410772418,0.913591163259108,0.8320213080793134,0.15547236980731827,0.9875666074600356,1.9846482494394315
USDJPY,7084,0.5172219085262564,0.4800203976097449,0.49110671936758893,0.28183992572414085,0.4646892655367232,0.8984330668837737
USDCHF,7967,0.5049579515501443,0.47560023778083327,0.48449855654575125,0.2879368779869159,0.4296482412060301,0.8508594426270052
GBPUSD,7239,0.5020030390937975,0.5483399577270303,0.5322558364414974,0.2593864013259506,0.5712309820193637,1.137903433912541
1 symbol n base_rate auc acc brier prec_top_decile lift_top_decile
2 EURUSD (in-sample) 16895 0.4976028410772418 0.913591163259108 0.8320213080793134 0.15547236980731827 0.9875666074600356 1.9846482494394315
3 USDJPY 7084 0.5172219085262564 0.4800203976097449 0.49110671936758893 0.28183992572414085 0.4646892655367232 0.8984330668837737
4 USDCHF 7967 0.5049579515501443 0.47560023778083327 0.48449855654575125 0.2879368779869159 0.4296482412060301 0.8508594426270052
5 GBPUSD 7239 0.5020030390937975 0.5483399577270303 0.5322558364414974 0.2593864013259506 0.5712309820193637 1.137903433912541
+116
View File
@@ -0,0 +1,116 @@
"""Validazione esterna: addestra SOLO su EURUSD, testa su altri simboli mai visti.
Due livelli:
A) Pattern: gli incroci robusti di EURUSD (extra-rendimento a 10g) hanno lo
stesso segno/forza sugli altri simboli? (replica del pattern)
B) Modello: il classificatore addestrato su EURUSD generalizza? AUC e
precisione sul decile piu' forte su ogni simbolo esterno.
Nessun dato dei simboli di test entra nell'addestramento (zero inquinamento).
La baseline di ciascun simbolo (definizione del target) e' calcolata sui dati
di quel simbolo: serve solo a definire la ground-truth, non i pesi del modello.
Uso:
python external_validation.py [config.yaml] SYM_TRAIN SYM_TEST1 SYM_TEST2 ...
(default: train=EURUSD, test=USDJPY USDCHF GBPUSD)
"""
from __future__ import annotations
import os, sys
import numpy as np, pandas as pd
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import train as T
from data import load
from labeling import RegimeBaseline
from features import select_xy
from model import make_model
from evaluate import metrics
RESULTS = os.path.normpath(os.path.join(HERE, "..", "results"))
ROBUST = ["MA365xMA7", "MA365xMA182", "MA365xMA121", "MA121xMA7",
"MA121xMA3", "PRICExMA121", "MA182xMA30", "MA182xMA121"]
def load_sym(sym, cfg):
cr, ba = load(sym, cfg["year_min"])
cr = cr.dropna(subset=[f"cret_{cfg['horizon']}"]).reset_index(drop=True)
return cr, ba
def main():
cfg_path = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "..", "config.yaml")
cfg = T.load_cfg(cfg_path)
args = sys.argv[2:]
sym_tr = args[0] if args else "EURUSD"
sym_te = args[1:] if len(args) > 1 else ["USDJPY", "USDCHF", "GBPUSD"]
num, cat, h = cfg["features_numeric"], cfg["features_categorical"], cfg["horizon"]
cr_tr, ba_tr = load_sym(sym_tr, cfg)
# ---------- A) replica dei pattern ----------
def pair_excess(cr, ba):
rb = RegimeBaseline(cfg["regime"], h).fit(ba)
ex = rb.excess(cr)
out = {}
for (p, d), idx in cr.groupby(["pair", "dir"]).groups.items():
out[(p, int(d))] = ex[cr.index.get_indexer(idx)].mean()
return out
base_ex = pair_excess(cr_tr, ba_tr)
print(f"=== A) Replica pattern robusti: extra-rendimento {h}g (%), segno vs {sym_tr} ===")
print(f"{'pattern':16s} {sym_tr:>9s}", end="")
sym_ex = {}
for s in sym_te:
c, b = load_sym(s, cfg)
sym_ex[s] = pair_excess(c, b)
print(f" {s:>9s}", end="")
print(" concordi")
agree_counts = []
for p in ROBUST:
for d in (1, -1):
key = (p, d)
if key not in base_ex:
continue
row = f"{p+('+' if d>0 else '-'):16s} {base_ex[key]*1:+8.3f}"
signs = []
for s in sym_te:
v = sym_ex[s].get(key, np.nan)
row += f" {v:+8.3f}" if not np.isnan(v) else f" {'n/a':>8s}"
if not np.isnan(v):
signs.append(np.sign(v) == np.sign(base_ex[key]))
conc = f"{sum(signs)}/{len(signs)}" if signs else "-"
agree_counts.append((sum(signs), len(signs)))
print(row + f" {conc}")
tot_a = sum(a for a, _ in agree_counts); tot_n = sum(n for _, n in agree_counts)
print(f"\nConcordanza di segno totale: {tot_a}/{tot_n} ({tot_a/max(tot_n,1)*100:.0f}%)")
# ---------- B) modello EURUSD -> simboli esterni ----------
print(f"\n=== B) Modello addestrato su {sym_tr}, testato esternamente ===")
rb_tr = RegimeBaseline(cfg["regime"], h).fit(ba_tr)
y_tr = rb_tr.label(cr_tr)
mdl = make_model(cfg, num, cat)
mdl.fit(select_xy(cr_tr, num, cat), y_tr)
rows = []
# riferimento in-sample (stesso simbolo, solo per confronto)
p_in = mdl.predict_proba(select_xy(cr_tr, num, cat))[:, 1]
rows.append({"symbol": sym_tr + " (in-sample)", **metrics(y_tr, p_in)})
for s in sym_te:
c, b = load_sym(s, cfg)
rb = RegimeBaseline(cfg["regime"], h).fit(b)
y = rb.label(c)
p = mdl.predict_proba(select_xy(c, num, cat))[:, 1]
rows.append({"symbol": s, **metrics(y, p)})
res = pd.DataFrame(rows)
for _, r in res.iterrows():
print(f" {r['symbol']:22s} n={int(r['n']):5d} AUC={r['auc']:.3f} "
f"acc={r['acc']:.3f} prec@10%={r['prec_top_decile']:.3f} "
f"(base {r['base_rate']:.3f}, lift {r['lift_top_decile']:.2f}x)")
os.makedirs(RESULTS, exist_ok=True)
res.to_csv(os.path.join(RESULTS, "external_validation.csv"), index=False)
print(f"\nsalvato: {os.path.join(RESULTS,'external_validation.csv')}")
if __name__ == "__main__":
main()