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:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
58ed32ff3b
commit
0187cb6bd7
@@ -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.48–0.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'**87–90%** 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
|
||||
|
@@ -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()
|
||||
Reference in New Issue
Block a user