PaPP v2 Modello: calibrazione rev (isotonic) + curva precisione/copertura + soglia operativa
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -99,12 +99,15 @@ Modello/
|
||||
├── config_mr.yaml parametri del modello di mean-reversion
|
||||
├── src/
|
||||
│ ├── mr_labeling.py costruzione dei 3 target + maschere
|
||||
│ └── mr_train.py pool + walk-forward + LOSO + modelli finali
|
||||
│ ├── mr_train.py pool + walk-forward + LOSO + modelli finali
|
||||
│ └── mr_calibrate.py calibrazione + curva precisione/copertura
|
||||
└── results/
|
||||
├── mr_wf_<task>.csv metriche walk-forward per anno
|
||||
├── mr_loso_<task>.csv metriche leave-one-symbol-out
|
||||
├── mr_summary.json riepilogo medio
|
||||
└── mr_model_<task>.joblib modelli finali (non versionati)
|
||||
├── mr_wf_<task>.csv metriche walk-forward per anno
|
||||
├── mr_loso_<task>.csv metriche leave-one-symbol-out
|
||||
├── mr_precision_coverage.csv soglia -> copertura/precisione/lift
|
||||
├── mr_calibration_metrics.json Brier prima/dopo + LOSO
|
||||
├── mr_*.png grafici (affidabilita', precisione/copertura)
|
||||
└── mr_*.joblib modelli (non versionati)
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -112,12 +115,48 @@ pip install -r requirements.txt
|
||||
# copia in ./data i CSV crosses+bars dei 4 simboli (vedi data/README.md), poi:
|
||||
cd src
|
||||
python mr_train.py # usa ../config_mr.yaml
|
||||
python mr_calibrate.py # calibrazione + soglia operativa (sez. 8)
|
||||
```
|
||||
|
||||
## 8. Prossimi passi
|
||||
## 8. Calibrazione e soglia operativa (`src/mr_calibrate.py`)
|
||||
|
||||
Il modello `rev` **ordina** bene gli eventi, ma la probabilità grezza non è
|
||||
"vera" (vedi curva rossa sotto: un 0.55 grezzo corrispondeva a ~46% di rientri
|
||||
reali). La **calibrazione isotonica** la rende affidabile nella zona operativa
|
||||
(0.7–0.9) e migliora il Brier score da **0.198 → 0.189** (più basso = meglio).
|
||||
|
||||
Split temporale onesto: modello base su anni `< 2016`, calibratore su `2016–2017`,
|
||||
valutazione su `2018+` (mai visto). LOSO calibrato: AUC 0.67–0.70 su simbolo mai
|
||||
visto → regge.
|
||||
|
||||

|
||||
|
||||
### Curva precisione/copertura (test 2018+, base rate 73%)
|
||||
|
||||
Per ogni soglia di probabilità: **copertura** = quanti segnali tieni,
|
||||
**precisione** = tasso di rientro effettivo tra quelli tenuti.
|
||||
|
||||
| soglia | copertura | precisione | lift |
|
||||
|---|---|---|---|
|
||||
| 0.50 | 99% | 73.1% | 1.00 |
|
||||
| 0.70 | 85% | 76.2% | 1.04 |
|
||||
| **0.75** | **51%** | **81.2%** | **1.11** |
|
||||
| 0.80 | 42% | 82.2% | 1.13 |
|
||||
| 0.85 | 28% | 83.3% | 1.14 |
|
||||
| 0.90 | 6% | 87.0% | 1.19 |
|
||||
|
||||

|
||||
|
||||
**Soglia consigliata: ~0.75** → l'EA opera su ~metà dei setup di mean-reversion
|
||||
ma il tasso di rientro sale dal 73% all'**81%**. Chi vuole essere più selettivo
|
||||
usa 0.80–0.85 (precisione ~82–83%, copertura 28–42%). La soglia è un parametro:
|
||||
la tabella completa è in `results/mr_precision_coverage.csv`.
|
||||
|
||||
Nota: tra 0.70 e 0.75 la copertura crolla (85%→51%) perché lì si concentra la
|
||||
massa delle probabilità; è il punto in cui il filtro inizia a "mordere".
|
||||
|
||||
## 9. Prossimi passi
|
||||
|
||||
- **Calibrazione** delle probabilità di `rev` e scelta soglia operativa
|
||||
(precisione vs copertura) per l'EA.
|
||||
- Aggiungere altri major FX al pool per irrobustire ancora il LOSO.
|
||||
- **Fase 4**: export del modello `rev` in **ONNX** (skl2onnx) e inferenza in MT5
|
||||
come filtro dei segnali di mean-reversion.
|
||||
- **Fase 4**: export del modello `rev` **calibrato** in **ONNX** (skl2onnx) e
|
||||
inferenza in MT5 come filtro dei segnali di mean-reversion (soglia ~0.75).
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"method": "isotonic",
|
||||
"brier_raw": 0.19825470393871938,
|
||||
"brier_cal": 0.18872294228809272,
|
||||
"auc": 0.6457651952507415,
|
||||
"test_base_rate": 0.7297476759628154,
|
||||
"loso": [
|
||||
{
|
||||
"symbol": "EURUSD",
|
||||
"brier": 0.1813803944436018,
|
||||
"auc": 0.6746989606877932,
|
||||
"base": 0.7324227355699628
|
||||
},
|
||||
{
|
||||
"symbol": "USDJPY",
|
||||
"brier": 0.19381118796660657,
|
||||
"auc": 0.6742950427540226,
|
||||
"base": 0.7028252001812962
|
||||
},
|
||||
{
|
||||
"symbol": "USDCHF",
|
||||
"brier": 0.16726747719373602,
|
||||
"auc": 0.699568173561329,
|
||||
"base": 0.7603328331741918
|
||||
},
|
||||
{
|
||||
"symbol": "GBPUSD",
|
||||
"brier": 0.17639890281257165,
|
||||
"auc": 0.6957124361813346,
|
||||
"base": 0.7374227714033539
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1,11 @@
|
||||
soglia,copertura,precisione,n_tenuti,lift_vs_base
|
||||
0.5,0.9932577382776586,0.7307929651342179,19446,1.0014323980820128
|
||||
0.55,0.9445295740116457,0.744646333549643,18492,1.02041617682053
|
||||
0.6,0.9440698743487588,0.7448466158091219,18483,1.0206906309449841
|
||||
0.65,0.8972826642149351,0.7543689873057438,17567,1.0337394857893085
|
||||
0.7,0.8463581571151292,0.7623415811707905,16570,1.0446646235152053
|
||||
0.75,0.509704770660946,0.8117045796171961,9979,1.1123085504126453
|
||||
0.8,0.42312800081724383,0.8221873491067118,8284,1.1266734738441382
|
||||
0.85,0.2760241086934314,0.8330866025166543,5404,1.1416091204641323
|
||||
0.9,0.0641025641025641,0.8701195219123506,1255,1.1923566878980891
|
||||
0.95,0.0,,0,
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,166 @@
|
||||
"""Calibrazione delle probabilita' del modello `rev` + scelta soglia operativa.
|
||||
|
||||
Perche': il modello `rev` ordina bene gli eventi (AUC ~0.68) ma la probabilita'
|
||||
grezza puo' non essere "vera" (un 0.80 deve voler dire ~80% di rientri reali).
|
||||
La calibrazione rende le probabilita' affidabili; la curva precisione/copertura
|
||||
serve a scegliere la soglia per l'EA.
|
||||
|
||||
Split temporale onesto (nessun leakage):
|
||||
train_core : anni < calib_start -> addestra il modello base
|
||||
calib : [calib_start, test_start) -> stima il calibratore
|
||||
test : >= test_start -> valutazione (mai vista)
|
||||
|
||||
Output (in ../results):
|
||||
mr_calibration_reliability.png curva di affidabilita' (prima/dopo)
|
||||
mr_precision_coverage.png precisione e copertura vs soglia
|
||||
mr_precision_coverage.csv tabella soglia -> copertura/precisione/lift
|
||||
mr_calibration_metrics.json Brier prima/dopo + LOSO
|
||||
mr_rev_calibrated.joblib modello calibrato finale (non versionato)
|
||||
|
||||
Uso:
|
||||
python mr_calibrate.py [config_mr.yaml] [method] # method: isotonic|sigmoid
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import os, sys, json
|
||||
import numpy as np, pandas as pd
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
from mr_train import load_cfg, load_pool
|
||||
from mr_labeling import make_targets
|
||||
from features import select_xy
|
||||
from model import make_model
|
||||
from sklearn.calibration import CalibratedClassifierCV
|
||||
from sklearn.frozen import FrozenEstimator
|
||||
from sklearn.metrics import brier_score_loss, roc_auc_score
|
||||
|
||||
RESULTS = os.path.normpath(os.path.join(HERE, "..", "results"))
|
||||
CALIB_START = 2016
|
||||
TEST_START = 2018
|
||||
|
||||
|
||||
def reliability(y, p, bins=10):
|
||||
edges = np.linspace(0, 1, bins + 1)
|
||||
idx = np.clip(np.digitize(p, edges) - 1, 0, bins - 1)
|
||||
xs, ys, ns = [], [], []
|
||||
for b in range(bins):
|
||||
m = idx == b
|
||||
if m.sum() > 0:
|
||||
xs.append(p[m].mean()); ys.append(y[m].mean()); ns.append(int(m.sum()))
|
||||
return np.array(xs), np.array(ys), np.array(ns)
|
||||
|
||||
|
||||
def prec_cov(y, p, taus):
|
||||
rows = []
|
||||
base = y.mean()
|
||||
for t in taus:
|
||||
keep = p >= t
|
||||
cov = keep.mean()
|
||||
prec = y[keep].mean() if keep.sum() > 0 else np.nan
|
||||
rows.append({"soglia": round(float(t), 3), "copertura": float(cov),
|
||||
"precisione": float(prec) if not np.isnan(prec) else np.nan,
|
||||
"n_tenuti": int(keep.sum()),
|
||||
"lift_vs_base": float(prec / base) if keep.sum() > 0 else np.nan})
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def run(cfg_path, method="isotonic"):
|
||||
cfg = load_cfg(cfg_path)
|
||||
num, cat = cfg["features_numeric"], cfg["features_categorical"]
|
||||
df = load_pool(cfg)
|
||||
y_all, mask = make_targets(df, cfg["horizon"], cfg["drop_trivial"], cfg["trivial_eps"])["rev"]
|
||||
d = df[mask].reset_index(drop=True)
|
||||
y = y_all[mask]
|
||||
yr = d.time.dt.year.values
|
||||
|
||||
tr = yr < CALIB_START
|
||||
ca = (yr >= CALIB_START) & (yr < TEST_START)
|
||||
te = yr >= TEST_START
|
||||
print(f"train_core<{CALIB_START}: {tr.sum()} calib[{CALIB_START}-{TEST_START}): "
|
||||
f"{ca.sum()} test>={TEST_START}: {te.sum()} base_rate test={y[te].mean():.3f}")
|
||||
|
||||
base = make_model(cfg, num, cat)
|
||||
base.fit(select_xy(d[tr], num, cat), y[tr])
|
||||
|
||||
Xte = select_xy(d[te], num, cat); yte = y[te]
|
||||
p_raw = base.predict_proba(Xte)[:, 1]
|
||||
|
||||
cal = CalibratedClassifierCV(FrozenEstimator(base), method=method)
|
||||
cal.fit(select_xy(d[ca], num, cat), y[ca])
|
||||
p_cal = cal.predict_proba(Xte)[:, 1]
|
||||
|
||||
b_raw, b_cal = brier_score_loss(yte, p_raw), brier_score_loss(yte, p_cal)
|
||||
auc = roc_auc_score(yte, p_cal)
|
||||
print(f"Brier grezzo={b_raw:.4f} calibrato={b_cal:.4f} (piu' basso = meglio) AUC={auc:.3f}")
|
||||
|
||||
# ---- reliability plot ----
|
||||
os.makedirs(RESULTS, exist_ok=True)
|
||||
xr, yr_, _ = reliability(yte, p_raw)
|
||||
xc, yc, _ = reliability(yte, p_cal)
|
||||
plt.figure(figsize=(5, 5))
|
||||
plt.plot([0, 1], [0, 1], "k--", lw=1, label="ideale")
|
||||
plt.plot(xr, yr_, "o-", color="#c0392b", label=f"grezzo (Brier {b_raw:.3f})")
|
||||
plt.plot(xc, yc, "o-", color="#27ae60", label=f"{method} (Brier {b_cal:.3f})")
|
||||
plt.xlabel("probabilita' prevista"); plt.ylabel("frequenza reale di rientro")
|
||||
plt.title(f"Affidabilita' rev (test >= {TEST_START})"); plt.legend(); plt.tight_layout()
|
||||
plt.savefig(os.path.join(RESULTS, "mr_calibration_reliability.png"), dpi=110); plt.close()
|
||||
|
||||
# ---- precisione/copertura ----
|
||||
taus = np.round(np.arange(0.50, 0.951, 0.05), 2)
|
||||
pc = prec_cov(yte, p_cal, taus)
|
||||
pc.to_csv(os.path.join(RESULTS, "mr_precision_coverage.csv"), index=False)
|
||||
print("\nsoglia copertura precisione lift n")
|
||||
for _, r in pc.iterrows():
|
||||
print(f" {r.soglia:.2f} {r.copertura*100:5.1f}% "
|
||||
f"{r.precisione*100:5.1f}% {r.lift_vs_base:.2f} {int(r.n_tenuti)}")
|
||||
|
||||
fig, ax1 = plt.subplots(figsize=(6, 4))
|
||||
ax1.plot(pc.soglia, pc.precisione * 100, "o-", color="#27ae60")
|
||||
ax1.set_xlabel("soglia di probabilita'"); ax1.set_ylabel("precisione (% rientri)", color="#27ae60")
|
||||
ax1.axhline(yte.mean() * 100, color="#7f8c8d", ls=":", label=f"base rate {yte.mean()*100:.0f}%")
|
||||
ax2 = ax1.twinx()
|
||||
ax2.plot(pc.soglia, pc.copertura * 100, "s--", color="#2980b9")
|
||||
ax2.set_ylabel("copertura (% segnali tenuti)", color="#2980b9")
|
||||
ax1.set_title("Precisione vs Copertura (rev calibrato)"); ax1.legend(loc="lower left")
|
||||
fig.tight_layout(); fig.savefig(os.path.join(RESULTS, "mr_precision_coverage.png"), dpi=110)
|
||||
plt.close()
|
||||
|
||||
# ---- LOSO: Brier calibrato su simbolo mai visto ----
|
||||
loso = []
|
||||
for s in cfg["symbols"]:
|
||||
m_tr = d.symbol.values != s; m_te = d.symbol.values == s
|
||||
b = make_model(cfg, num, cat)
|
||||
cv = CalibratedClassifierCV(b, method=method, cv=5)
|
||||
cv.fit(select_xy(d[m_tr], num, cat), y[m_tr])
|
||||
pp = cv.predict_proba(select_xy(d[m_te], num, cat))[:, 1]
|
||||
loso.append({"symbol": s, "brier": float(brier_score_loss(y[m_te], pp)),
|
||||
"auc": float(roc_auc_score(y[m_te], pp)), "base": float(y[m_te].mean())})
|
||||
print("\nLOSO (calibrato, simbolo mai visto):")
|
||||
for r in loso:
|
||||
print(f" {r['symbol']:7s} Brier={r['brier']:.4f} AUC={r['auc']:.3f} base={r['base']:.3f}")
|
||||
|
||||
with open(os.path.join(RESULTS, "mr_calibration_metrics.json"), "w") as f:
|
||||
json.dump({"method": method, "brier_raw": b_raw, "brier_cal": b_cal,
|
||||
"auc": auc, "test_base_rate": float(yte.mean()), "loso": loso}, f, indent=2)
|
||||
|
||||
# ---- modello finale: base su (train+calib) + calibratore sull'ultimo blocco ----
|
||||
fit_mask = yr < TEST_START
|
||||
base_f = make_model(cfg, num, cat); base_f.fit(select_xy(d[fit_mask], num, cat), y[fit_mask])
|
||||
final = CalibratedClassifierCV(base_f, method=method, cv=5)
|
||||
final.fit(select_xy(d[fit_mask], num, cat), y[fit_mask])
|
||||
try:
|
||||
import joblib
|
||||
joblib.dump(final, os.path.join(RESULTS, "mr_rev_calibrated.joblib"))
|
||||
except Exception as e:
|
||||
print("modello non serializzato:", e)
|
||||
print(f"\nsalvati grafici, tabella e modello in {RESULTS}")
|
||||
return pc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cfg_path = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "..", "config_mr.yaml")
|
||||
method = sys.argv[2] if len(sys.argv) > 2 else "isotonic"
|
||||
run(cfg_path, method)
|
||||
Reference in New Issue
Block a user