PaPP v2 Modello: modello mean-reversion (rev/speed/disc) pooled 4 FX + LOSO + doc
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,14 @@
|
||||
# PaPP v2 — Fase 3: classificatore extra-rendimento (architettura)
|
||||
# PaPP v2 — Fase 3: modelli
|
||||
|
||||
> **Modello principale → [`README_mean_reversion.md`](README_mean_reversion.md).**
|
||||
> La validazione esterna (sez. 5b) ha mostrato che l'edge **direzionale** non
|
||||
> generalizza fuori da EURUSD, mentre la **mean-reversion** sì. Il modeling è
|
||||
> quindi orientato sul **rientro alla Mediana**. Questo documento descrive il
|
||||
> primo modello (direzionale), tenuto come riferimento metodologico.
|
||||
|
||||
---
|
||||
|
||||
# Fase 3 (riferimento): classificatore extra-rendimento direzionale
|
||||
|
||||
Architettura del modello che prova a prevedere, al momento di un incrocio, se
|
||||
l'**extra-rendimento a 10 giorni** (rispetto alla baseline dello stesso regime)
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# Fase 3 — Modello di MEAN-REVERSION (rientro alla Mediana)
|
||||
|
||||
> Questo è il **modello principale** della Fase 3. Nasce dopo aver dimostrato
|
||||
> (vedi `README.md` sez. 5b, validazione esterna) che l'edge **direzionale**
|
||||
> "su/giù" **non generalizza** fuori da EURUSD, mentre la **mean-reversion** è
|
||||
> universale (rientro alla Mediana entro 20g nell'87–90% dei casi su tutti i
|
||||
> major FX). Quindi modelliamo ciò che è davvero robusto: il **rientro alla
|
||||
> Mediana**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Idea
|
||||
|
||||
Dopo un incrocio il prezzo tende a tornare sulla Mediana. Non vogliamo prevedere
|
||||
la direzione, ma **caratterizzare il rientro**. Tre target, costruiti da esiti
|
||||
già presenti nei CSV (nessuna baseline di regime: sono osservazioni dirette):
|
||||
|
||||
| target | tipo | colonna | domanda |
|
||||
|---|---|---|---|
|
||||
| `rev` | classificazione | `rev_10` | rientra alla Mediana **entro 10 giorni**? (sì/no) |
|
||||
| `speed` | regressione | `bars_to_revert` | in **quanti giorni** rientra (solo eventi rientrati) |
|
||||
| `disc` | regressione | `disc_max_pct` | **quanto si allontana** (max %) prima di rientrare |
|
||||
|
||||
Esclusione del banale (`drop_trivial`): gli eventi già praticamente sulla
|
||||
Mediana (`|dist_med_pct| < 0.05%`) rientrano per costruzione; vengono esclusi per
|
||||
studiare il rientro "vero".
|
||||
|
||||
## 2. Feature (note al momento dell'incrocio)
|
||||
|
||||
`abs_dist` (=|dist_med_pct|), `dist_med_pct`, `cluster_pct`, `cluster_exp`,
|
||||
`slope_a`, `slope_b`, `vel_med`, `acc_med`, `vol_med` (numeriche) +
|
||||
`pair`, `dir`, `trend`, `dow`, `month` (categoriche). Nessuna colonna di esito
|
||||
futuro è tra le feature.
|
||||
|
||||
## 3. Pooling pulito + validazione (anti-inquinamento)
|
||||
|
||||
I 4 major FX (EURUSD, USDJPY, USDCHF, GBPUSD) hanno mean-reversion omogenea →
|
||||
si possono **mettere in pool**. Le feature sono già in percentuale/relative,
|
||||
quindi confrontabili tra simboli. Due validazioni, entrambe oneste:
|
||||
|
||||
1. **Walk-forward pooled**: train tutti i simboli fino all'anno T, test anno T+1
|
||||
→ generalizzazione **nel tempo**.
|
||||
2. **Leave-one-symbol-out (LOSO)**: train su 3 simboli, test sul 4° **mai visto**
|
||||
→ generalizzazione **tra strumenti** (la prova più severa anti-inquinamento).
|
||||
|
||||
Ogni metrica è confrontata con una **baseline banale**:
|
||||
- classificazione → accuratezza della classe maggioritaria;
|
||||
- regressione → MAE ottenuto prevedendo sempre la mediana del train
|
||||
(`skill_% = 1 − MAE/MAE_baseline`).
|
||||
|
||||
## 4. Risultati (pool 4 major FX, ~39k incroci)
|
||||
|
||||
**Walk-forward (nel tempo):**
|
||||
- `rev` : **AUC ≈ 0.65** (la baseline è 0.5) → il modello sa **ordinare** quali
|
||||
incroci rientreranno entro 10g e quali no.
|
||||
- `speed` : skill ≈ **+2%** → marginale (la mediana è ~3g e cambia poco).
|
||||
- `disc` : skill ≈ **−3%** → non meglio della mediana.
|
||||
|
||||
**Leave-one-symbol-out (su simbolo mai visto):**
|
||||
|
||||
| target | EURUSD | USDJPY | USDCHF | GBPUSD |
|
||||
|---|---|---|---|---|
|
||||
| `rev` AUC | 0.677 | 0.687 | 0.682 | 0.688 |
|
||||
| `speed` skill | +1.3% | +4.8% | −2.9% | +2.8% |
|
||||
| `disc` skill | +2.0% | +5.8% | −1.1% | −0.1% |
|
||||
|
||||
**Lettura:** il classificatore di rientro (`rev`) ha un edge **reale e
|
||||
generalizzabile** — AUC ~0.68 anche su un simbolo completamente fuori dal
|
||||
training. I due regressori (`speed`, `disc`) aggiungono poco: la velocità e
|
||||
l'ampiezza dell'allontanamento sono in pratica vicine alla mediana storica.
|
||||
|
||||
## 5. Cosa guida il rientro (importanza, permutation su holdout 2018+)
|
||||
|
||||
```
|
||||
abs_dist +0.104 <- distanza dalla Mediana: di gran lunga il fattore #1
|
||||
dist_med_pct +0.025 <- il segno della distanza (sopra/sotto)
|
||||
trend +0.021
|
||||
cluster_exp +0.017 <- cluster in espansione/contrazione
|
||||
vel_med +0.010
|
||||
... (vol_med, cluster_pct ~0)
|
||||
pair ~0 <- QUALI linee si incrociano e' irrilevante
|
||||
```
|
||||
|
||||
Conferma economica: il rientro dipende da **quanto sei lontano dalla Mediana** e
|
||||
dal **regime**, non dallo specifico tipo di incrocio. È coerente con la Fase 2 e
|
||||
con la validazione esterna (il "pattern" di linee era rumore).
|
||||
|
||||
## 6. Uso operativo
|
||||
|
||||
Il modello `rev` non dice "compra/vendi": stima la **probabilità di rientro
|
||||
rapido**. Serve da **filtro**: tra i setup di mean-reversion (prezzo lontano
|
||||
dalla Mediana), privilegiare quelli con alta probabilità ed evitare il ~25% che
|
||||
tende a NON rientrare in fretta (i casi rischiosi, spesso inizio di trend).
|
||||
|
||||
## 7. File e come eseguire
|
||||
|
||||
```
|
||||
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
|
||||
└── 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)
|
||||
```
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
## 8. 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.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Configurazione Fase 3 — modello di MEAN-REVERSION (rientro alla Mediana)
|
||||
|
||||
# pooling pulito sui major FX (dinamica di mean-reversion omogenea)
|
||||
symbols: [EURUSD, USDJPY, USDCHF, GBPUSD]
|
||||
year_min: 1999
|
||||
|
||||
horizon: 10 # orizzonte per il target di classificazione rev_N
|
||||
drop_trivial: true # esclude gli eventi gia' praticamente sulla Mediana
|
||||
trivial_eps: 0.05 # soglia |dist_med_pct| (%) sotto cui l'evento e' "banale"
|
||||
|
||||
# feature note al momento dell'incrocio
|
||||
features_numeric:
|
||||
- abs_dist # |dist_med_pct| (derivata): quanto e' lontano dalla Mediana
|
||||
- dist_med_pct
|
||||
- cluster_pct
|
||||
- cluster_exp
|
||||
- slope_a
|
||||
- slope_b
|
||||
- vel_med
|
||||
- acc_med
|
||||
- vol_med
|
||||
features_categorical:
|
||||
- pair
|
||||
- dir
|
||||
- trend
|
||||
- dow
|
||||
- month
|
||||
|
||||
# validazione
|
||||
oos_start_year: 2020
|
||||
walkforward:
|
||||
min_train_years: 8
|
||||
step_years: 1
|
||||
|
||||
model:
|
||||
kind: hist_gbdt
|
||||
hist_gbdt:
|
||||
max_depth: 4
|
||||
learning_rate: 0.05
|
||||
max_iter: 400
|
||||
l2_regularization: 1.0
|
||||
early_stopping: true
|
||||
|
||||
seed: 42
|
||||
@@ -0,0 +1,5 @@
|
||||
n,mae,mae_baseline,skill_%,r2,test_symbol
|
||||
15898,0.7016688523942158,0.7156681469367216,1.956115359112609,0.056705406202969044,EURUSD
|
||||
6635,0.766700972158277,0.8138184204973624,5.789675823544238,0.09699286957940556,USDJPY
|
||||
7349,0.7110993410001212,0.703173893046673,-1.1270964453912402,0.13820883251935367,USDCHF
|
||||
6807,0.7712259995571173,0.7706658777728809,-0.07268023671362922,0.10481685322904755,GBPUSD
|
||||
|
@@ -0,0 +1,5 @@
|
||||
n,base_rate,acc_baseline,auc,acc,test_symbol
|
||||
15887,0.7324227355699628,0.7324227355699628,0.6765454819075187,0.7357587965002832,EURUSD
|
||||
6619,0.7028252001812962,0.7028252001812962,0.6869979773747487,0.719292944553558,USDJPY
|
||||
7331,0.7603328331741918,0.7603328331741918,0.6816590320250597,0.7442368026190151,USDCHF
|
||||
6798,0.7374227714033539,0.7374227714033539,0.687844098341511,0.7375698734922036,GBPUSD
|
||||
|
@@ -0,0 +1,5 @@
|
||||
n,mae,mae_baseline,skill_%,r2,test_symbol
|
||||
13909,3.610173858972498,3.65878208354303,1.3285356564187945,0.09908303164055288,EURUSD
|
||||
5729,3.615529034826519,3.7987432361668705,4.823021456044085,0.1281263482188003,USDJPY
|
||||
6570,3.627862458240022,3.5245053272450533,-2.9325287210094197,0.07345132592032444,USDCHF
|
||||
6090,3.638542831980693,3.744006568144499,2.8168683533072225,0.11825074333860008,GBPUSD
|
||||
|
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"wf": {
|
||||
"rev": {
|
||||
"n": 1594.0,
|
||||
"base_rate": 0.7183856164543461,
|
||||
"acc_baseline": 0.7183856164543461,
|
||||
"auc": 0.6478491068895667,
|
||||
"acc": 0.7130101340748866,
|
||||
"test_year": 2016.5
|
||||
},
|
||||
"speed": {
|
||||
"n": 1406.55,
|
||||
"mae": 3.6537111122909542,
|
||||
"mae_baseline": 3.7337467740160712,
|
||||
"skill_%": 1.7998203587207346,
|
||||
"r2": 0.07833366023387726,
|
||||
"test_year": 2016.5
|
||||
},
|
||||
"disc": {
|
||||
"n": 1596.7,
|
||||
"mae": 0.7847562209218487,
|
||||
"mae_baseline": 0.7657217825432683,
|
||||
"skill_%": -3.381481774117468,
|
||||
"r2": -0.36203395606382444,
|
||||
"test_year": 2016.5
|
||||
}
|
||||
},
|
||||
"loso": {
|
||||
"rev": {
|
||||
"n": 9158.75,
|
||||
"base_rate": 0.7332508850822013,
|
||||
"acc_baseline": 0.7332508850822013,
|
||||
"auc": 0.6832616474122095,
|
||||
"acc": 0.734214604291265
|
||||
},
|
||||
"speed": {
|
||||
"n": 8074.5,
|
||||
"mae": 3.623027046004933,
|
||||
"mae_baseline": 3.681509303774863,
|
||||
"skill_%": 1.5089741861901707,
|
||||
"r2": 0.10472786227956943
|
||||
},
|
||||
"disc": {
|
||||
"n": 9172.25,
|
||||
"mae": 0.7376737912774329,
|
||||
"mae_baseline": 0.7508315845634095,
|
||||
"skill_%": 1.6365036251379943,
|
||||
"r2": 0.09918099038269396
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
n,mae,mae_baseline,skill_%,r2,test_year
|
||||
468,0.5210450337997958,0.4877705555555555,-6.82174802584008,-0.440278374819538,2007
|
||||
511,1.4979005200832076,1.46627927592955,-2.1565635327970734,-0.4428980601648529,2008
|
||||
590,0.5372706093503566,0.624245779661017,13.932840740691965,0.09814855360679986,2009
|
||||
548,0.8435515547481117,0.9210056204379563,8.40972779873087,-0.20129888622520697,2010
|
||||
649,0.6264895295465607,0.7349227426810478,14.754368974746313,-0.23270335993701874,2011
|
||||
563,0.8444189035765441,0.7120972291296623,-18.581967326092208,-0.6572098948790617,2012
|
||||
574,0.6570009471217217,0.5640415679442509,-16.480944749564785,-1.9530900867332583,2013
|
||||
1089,0.8585576108637076,0.8849660055096419,2.984114020371431,-0.0058690268732339845,2014
|
||||
2465,0.86287949087831,0.8346033630831645,-3.387971945223023,0.1741985535476418,2015
|
||||
2440,0.9893521953097881,0.9588108852459016,-3.1853320121677298,0.06164644338816894,2016
|
||||
2405,0.6483027913003414,0.606651659043659,-6.865741094707012,-0.3827607195275795,2017
|
||||
2208,0.716304832228868,0.6826646195652174,-4.927780303756735,-0.30219708772441267,2018
|
||||
2520,0.7276522650483845,0.693340365079365,-4.948781536048585,-0.9380918084727767,2019
|
||||
2351,1.0262956818683626,0.9582834198213527,-7.097301345325269,-0.054525480586665065,2020
|
||||
2378,0.5921425747388034,0.566631362489487,-4.502259129680586,-0.6404632411590949,2021
|
||||
2074,0.9417510265311481,1.0620946142719383,11.330778456426438,-0.41893129794772066,2022
|
||||
2185,0.6089553673717031,0.5467215652173913,-11.383088964044408,-0.23632995343602725,2023
|
||||
2317,0.7212702214350022,0.6373334527406128,-13.169992620574188,-0.24062608198432445,2024
|
||||
2335,0.7021795509288201,0.7111189250535332,1.2570856729822166,0.1140083899835217,2025
|
||||
1264,0.7718037117074369,0.6608526424050633,-16.78907856047691,-0.5414077013318492,2026
|
||||
|
@@ -0,0 +1,21 @@
|
||||
n,base_rate,acc_baseline,auc,acc,test_year
|
||||
468,0.6175213675213675,0.6175213675213675,0.641878177495119,0.6260683760683761,2007
|
||||
511,0.6516634050880626,0.6516634050880626,0.5756149407834801,0.6594911937377691,2008
|
||||
590,0.7440677966101695,0.7440677966101695,0.5982138816394877,0.7372881355932204,2009
|
||||
548,0.7408759124087592,0.7408759124087592,0.6361617983764658,0.7299270072992701,2010
|
||||
649,0.5855161787365177,0.5855161787365177,0.6710135002934846,0.6718027734976888,2011
|
||||
563,0.8206039076376554,0.8206039076376554,0.6997342591401998,0.7708703374777975,2012
|
||||
574,0.6933797909407665,0.6933797909407665,0.6654865235267245,0.6428571428571429,2013
|
||||
1089,0.6464646464646465,0.6464646464646465,0.6203659976387249,0.6721763085399449,2014
|
||||
2465,0.7829614604462475,0.7829614604462475,0.6354597840298292,0.7517241379310344,2015
|
||||
2440,0.7487704918032787,0.7487704918032787,0.6421575586789066,0.6905737704918032,2016
|
||||
2405,0.7600831600831601,0.7600831600831601,0.6408060252797805,0.755925155925156,2017
|
||||
2208,0.7336956521739131,0.7336956521739131,0.6192271352985639,0.7133152173913043,2018
|
||||
2520,0.8067460317460318,0.8067460317460318,0.6465879719737271,0.7746031746031746,2019
|
||||
2351,0.7243726074011059,0.7243726074011059,0.6688414780017834,0.7209698000850702,2020
|
||||
2378,0.7863751051303617,0.7863751051303617,0.6841361741547013,0.7901597981497056,2021
|
||||
2074,0.6769527483124397,0.6769527483124397,0.5842326827401454,0.6557377049180327,2022
|
||||
2185,0.6668192219679634,0.6668192219679634,0.6764596076538425,0.6906178489702517,2023
|
||||
2317,0.6551575312904618,0.6551575312904618,0.6896672553471812,0.6870953819594303,2024
|
||||
2335,0.7430406852248393,0.7430406852248393,0.7314169068203651,0.7644539614561028,2025
|
||||
1210,0.7826446280991736,0.7826446280991736,0.6295204789188191,0.7545454545454545,2026
|
||||
|
@@ -0,0 +1,21 @@
|
||||
n,mae,mae_baseline,skill_%,r2,test_year
|
||||
338,3.465379140810698,3.4142011834319526,-1.498973101734502,0.12927353007997744,2007
|
||||
400,4.090858376700547,3.9425,-3.7630533088280904,0.04579965148367826,2008
|
||||
540,4.0205317754813965,3.8740740740740742,-3.780456919691866,-0.12200728544542727,2009
|
||||
470,3.1693714434220257,3.421276595744681,7.362899352714425,0.2246464741787182,2010
|
||||
495,4.314538334266083,4.3232323232323235,0.20109927749014656,-0.055325713812715005,2011
|
||||
515,3.2237999945625653,2.9029126213592233,-11.053979745800735,-0.06562655938335582,2012
|
||||
485,3.733393114617742,4.072164948453608,8.319207058754174,0.19434702822584105,2013
|
||||
879,3.924266870235131,4.180887372013652,6.137943430294424,0.022748161878883844,2014
|
||||
2243,3.398709049328624,3.3410610789121713,-1.7254389864438613,0.07268045675088919,2015
|
||||
2243,3.6650255510608725,3.7588051716451183,2.494931668490852,0.06864404910302002,2016
|
||||
2206,3.65145803728882,3.8028105167724386,3.9800163278205125,0.10526816265720629,2017
|
||||
1923,3.577364530993464,3.4607384295371815,-3.3699773568810043,0.07894792932730887,2018
|
||||
2354,3.356457496216363,3.373406966864911,0.502443696092203,0.015425103761181336,2019
|
||||
2116,3.8333614130177756,3.972117202268431,3.4932450928540915,0.049572458729656765,2020
|
||||
2190,3.3981138450501156,3.4990867579908675,2.8856933229837733,0.12873115565641913,2021
|
||||
1715,3.799187552337051,3.816909620991254,0.4643041207142984,0.0433313417575899,2022
|
||||
1810,4.043300720188006,3.8784530386740332,-4.250346204277644,0.06762624567274822,2023
|
||||
1997,3.6962952238115996,4.476214321482224,17.423631704309617,0.2427214165484335,2024
|
||||
2094,3.369937232661497,3.7597898758357213,10.369000823152874,0.2265055214709174,2025
|
||||
1118,3.3428725437687103,3.4042933810375673,1.804216922401003,0.09336407603657426,2026
|
||||
|
@@ -5,7 +5,8 @@ come riferimento lineare interpretabile. Entrambi esportabili in ONNX
|
||||
(skl2onnx) per la Fase 4.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from sklearn.ensemble import HistGradientBoostingClassifier
|
||||
from sklearn.ensemble import (HistGradientBoostingClassifier,
|
||||
HistGradientBoostingRegressor)
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
from sklearn.pipeline import Pipeline
|
||||
from features import build_preprocessor
|
||||
@@ -29,3 +30,18 @@ def make_model(cfg: dict, num_cols: list[str], cat_cols: list[str]) -> Pipeline:
|
||||
else:
|
||||
raise ValueError(f"model.kind sconosciuto: {kind}")
|
||||
return Pipeline([("pre", pre), ("clf", clf)])
|
||||
|
||||
|
||||
def make_regressor(cfg: dict, num_cols: list[str], cat_cols: list[str]) -> Pipeline:
|
||||
"""Regressore HistGradientBoosting per i target continui di mean-reversion."""
|
||||
pre = build_preprocessor(num_cols, cat_cols)
|
||||
p = cfg["model"]["hist_gbdt"]
|
||||
reg = HistGradientBoostingRegressor(
|
||||
max_depth=p["max_depth"],
|
||||
learning_rate=p["learning_rate"],
|
||||
max_iter=p["max_iter"],
|
||||
l2_regularization=p["l2_regularization"],
|
||||
early_stopping=p["early_stopping"],
|
||||
random_state=cfg["seed"],
|
||||
)
|
||||
return Pipeline([("pre", pre), ("reg", reg)])
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Target di mean-reversion (rientro alla Mediana dopo un incrocio).
|
||||
|
||||
A differenza del target direzionale, questi esiti sono diretti e NON richiedono
|
||||
una baseline di regime: sono proprieta' osservate dell'evento.
|
||||
|
||||
Tre target (vedi README sez. Mean-reversion):
|
||||
- REV_N (classificazione): rientra alla Mediana entro N giorni? (rev_N)
|
||||
- SPEED (regressione) : in quanti giorni rientra (bars_to_revert);
|
||||
definito solo per gli eventi che rientrano entro 20g
|
||||
- DISC (regressione) : massimo allontanamento dalla Mediana prima del
|
||||
rientro, in % (disc_max_pct)
|
||||
|
||||
Nota sulla "banalita'": l'incrocio PRICExMED ha per costruzione dist~0 e rientro
|
||||
immediato. Con `drop_trivial` si possono escludere gli eventi gia' praticamente
|
||||
sulla Mediana (|dist_med_pct| < trivial_eps) per studiare il rientro "vero".
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def make_targets(df: pd.DataFrame, horizon: int = 10,
|
||||
drop_trivial: bool = False, trivial_eps: float = 0.05):
|
||||
"""Ritorna un dict di colonne target + una maschera 'valid' per ciascun task.
|
||||
|
||||
Output:
|
||||
{
|
||||
"rev": (y, mask), # classificazione 0/1
|
||||
"speed": (y, mask), # regressione (giorni), solo eventi rientrati
|
||||
"disc": (y, mask), # regressione (% allontanamento)
|
||||
}
|
||||
"""
|
||||
n = len(df)
|
||||
base_mask = np.ones(n, bool)
|
||||
if drop_trivial:
|
||||
base_mask &= df["dist_med_pct"].abs().values >= trivial_eps
|
||||
|
||||
out = {}
|
||||
|
||||
# REV_N: classificazione
|
||||
yrev = df[f"rev_{horizon}"].astype(float).values
|
||||
mrev = base_mask & ~np.isnan(yrev)
|
||||
out["rev"] = (np.nan_to_num(yrev).astype(int), mrev)
|
||||
|
||||
# SPEED: giorni al rientro, solo per chi rientra entro 20g (bars_to_revert valorizzato)
|
||||
btr = pd.to_numeric(df["bars_to_revert"], errors="coerce").values
|
||||
mspeed = base_mask & ~np.isnan(btr)
|
||||
out["speed"] = (btr, mspeed)
|
||||
|
||||
# DISC: massimo allontanamento (in valore assoluto, %)
|
||||
disc = pd.to_numeric(df["disc_max_pct"], errors="coerce").abs().values
|
||||
mdisc = base_mask & ~np.isnan(disc)
|
||||
out["disc"] = (disc, mdisc)
|
||||
|
||||
return out
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Self-test del modello di mean-reversion.
|
||||
|
||||
M1 feature senza esiti futuri
|
||||
M2 reproducibilita' (LOSO rev AUC identico tra due run)
|
||||
M3 controllo negativo: con label rev mescolate l'AUC deve ~0.5
|
||||
"""
|
||||
import os, sys
|
||||
import numpy as np
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
from mr_train import load_cfg, load_pool, leave_one_symbol_out
|
||||
from mr_labeling import make_targets
|
||||
from features import select_xy
|
||||
from model import make_model
|
||||
from sklearn.metrics import roc_auc_score
|
||||
|
||||
CFG = os.path.join(HERE, "..", "config_mr.yaml")
|
||||
ok = True
|
||||
|
||||
def check(name, cond):
|
||||
global ok
|
||||
print(("PASS" if cond else "FAIL"), "-", name); ok = ok and cond
|
||||
|
||||
cfg = load_cfg(CFG)
|
||||
num, cat = cfg["features_numeric"], cfg["features_categorical"]
|
||||
|
||||
# M1
|
||||
future = ("ret_", "mfe_", "mae_", "dir_", "rev_", "cret_", "bars_to_revert", "disc_max")
|
||||
feats = num + cat
|
||||
check("M1 feature senza esiti futuri",
|
||||
not any(f.startswith(future) for f in feats))
|
||||
|
||||
df = load_pool(cfg)
|
||||
|
||||
# M2 reproducibilita'
|
||||
a = leave_one_symbol_out(cfg, df, num, cat)["rev"].auc.mean()
|
||||
b = leave_one_symbol_out(cfg, df, num, cat)["rev"].auc.mean()
|
||||
check("M2 reproducibilita' LOSO rev AUC", abs(a - b) < 1e-9)
|
||||
|
||||
# M3 controllo negativo
|
||||
y, mask = make_targets(df, cfg["horizon"], cfg["drop_trivial"], cfg["trivial_eps"])["rev"]
|
||||
d = df[mask].reset_index(drop=True); y = y[mask]
|
||||
tr = d.time.dt.year.values < 2018
|
||||
m = make_model(cfg, num, cat); m.fit(select_xy(d[tr], num, cat), y[tr])
|
||||
p = m.predict_proba(select_xy(d[~tr], num, cat))[:, 1]
|
||||
yte = y[~tr].copy(); np.random.default_rng(0).shuffle(yte)
|
||||
auc_shuf = roc_auc_score(yte, p)
|
||||
check("M3 controllo negativo AUC~0.5 (0.45-0.55)", 0.45 <= auc_shuf <= 0.55)
|
||||
|
||||
print(f"\nLOSO rev AUC medio reale = {a:.3f}")
|
||||
print("=== SELF-TEST MR:", "TUTTO OK" if ok else "CI SONO FAIL", "===")
|
||||
sys.exit(0 if ok else 1)
|
||||
@@ -0,0 +1,192 @@
|
||||
"""Fase 3 — modello di MEAN-REVERSION (rientro alla Mediana).
|
||||
|
||||
Pooling pulito sui major FX. Tre target (vedi mr_labeling):
|
||||
- rev : classificatore "rientra entro `horizon` giorni si/no"
|
||||
- speed : regressione su quanti giorni impiega a rientrare (eventi rientrati)
|
||||
- disc : regressione sul massimo allontanamento (% ) prima del rientro
|
||||
|
||||
Due schemi di validazione, entrambi onesti:
|
||||
1) WALK-FORWARD pooled: train tutti i simboli fino all'anno T, test anno T+1.
|
||||
(generalizzazione nel tempo)
|
||||
2) LEAVE-ONE-SYMBOL-OUT: train su 3 simboli, test sul 4o mai visto.
|
||||
(generalizzazione tra strumenti — la prova piu' severa)
|
||||
|
||||
Confronto sempre contro una baseline banale:
|
||||
- clf : prevedere sempre la classe maggioritaria / la base rate
|
||||
- reg : prevedere la mediana del train (MAE di riferimento)
|
||||
|
||||
Uso:
|
||||
python mr_train.py [config_mr.yaml]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import os, sys, json
|
||||
import numpy as np, pandas as pd
|
||||
import yaml
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
from data import load
|
||||
from features import select_xy
|
||||
from model import make_model, make_regressor
|
||||
from mr_labeling import make_targets
|
||||
from sklearn.metrics import roc_auc_score, accuracy_score, mean_absolute_error, r2_score
|
||||
|
||||
RESULTS = os.path.normpath(os.path.join(HERE, "..", "results"))
|
||||
|
||||
|
||||
def load_cfg(path):
|
||||
with open(path) as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def load_pool(cfg):
|
||||
parts = []
|
||||
for s in cfg["symbols"]:
|
||||
cr, _ = load(s, cfg["year_min"])
|
||||
cr = cr.copy()
|
||||
cr["abs_dist"] = cr["dist_med_pct"].abs()
|
||||
parts.append(cr)
|
||||
df = pd.concat(parts, ignore_index=True).sort_values("time").reset_index(drop=True)
|
||||
return df
|
||||
|
||||
|
||||
# ---------------- metriche ----------------
|
||||
def clf_metrics(ytr, yte, ptr, pte):
|
||||
base = max(yte.mean(), 1 - yte.mean()) # accuratezza della classe maggioritaria
|
||||
out = {"n": int(len(yte)), "base_rate": float(yte.mean()),
|
||||
"acc_baseline": float(base)}
|
||||
if len(np.unique(yte)) < 2:
|
||||
out.update(auc=np.nan, acc=np.nan); return out
|
||||
out["auc"] = float(roc_auc_score(yte, pte))
|
||||
out["acc"] = float(accuracy_score(yte, (pte >= 0.5).astype(int)))
|
||||
return out
|
||||
|
||||
|
||||
def reg_metrics(ytr, yte, pred):
|
||||
base_pred = np.median(ytr)
|
||||
mae_base = mean_absolute_error(yte, np.full_like(yte, base_pred, dtype=float))
|
||||
mae = mean_absolute_error(yte, pred)
|
||||
return {"n": int(len(yte)), "mae": float(mae), "mae_baseline": float(mae_base),
|
||||
"skill_%": float((1 - mae / mae_base) * 100) if mae_base > 0 else np.nan,
|
||||
"r2": float(r2_score(yte, pred)) if len(np.unique(yte)) > 1 else np.nan}
|
||||
|
||||
|
||||
def fit_predict(cfg, task, tr, te, num, cat):
|
||||
"""Addestra il modello giusto per il task e ritorna (ytr,yte,pred_or_proba)."""
|
||||
horizon = cfg["horizon"]
|
||||
t_tr = make_targets(tr, horizon, cfg["drop_trivial"], cfg["trivial_eps"])[task]
|
||||
t_te = make_targets(te, horizon, cfg["drop_trivial"], cfg["trivial_eps"])[task]
|
||||
ytr, mtr = t_tr; yte, mte = t_te
|
||||
Xtr, Xte = tr[mtr], te[mte]
|
||||
ytr, yte = ytr[mtr], yte[mte]
|
||||
if len(Xtr) < 50 or len(Xte) < 20:
|
||||
return None
|
||||
if task == "rev":
|
||||
m = make_model(cfg, num, cat)
|
||||
m.fit(select_xy(Xtr, num, cat), ytr)
|
||||
return ("clf", ytr, yte,
|
||||
m.predict_proba(select_xy(Xtr, num, cat))[:, 1],
|
||||
m.predict_proba(select_xy(Xte, num, cat))[:, 1])
|
||||
else:
|
||||
m = make_regressor(cfg, num, cat)
|
||||
m.fit(select_xy(Xtr, num, cat), ytr)
|
||||
return ("reg", ytr, yte, None, m.predict(select_xy(Xte, num, cat)))
|
||||
|
||||
|
||||
def walk_forward(cfg, df, num, cat):
|
||||
years = sorted(df.time.dt.year.unique())
|
||||
t = years[0] + cfg["walkforward"]["min_train_years"] - 1
|
||||
rows = {k: [] for k in ("rev", "speed", "disc")}
|
||||
while t < years[-1]:
|
||||
tr = df[df.time.dt.year <= t]
|
||||
te = df[df.time.dt.year == t + 1]
|
||||
if len(te):
|
||||
for task in rows:
|
||||
r = fit_predict(cfg, task, tr, te, num, cat)
|
||||
if r is None:
|
||||
continue
|
||||
kind, ytr, yte, ptr, pte = r
|
||||
m = (clf_metrics(ytr, yte, ptr, pte) if kind == "clf"
|
||||
else reg_metrics(ytr, yte, pte))
|
||||
m["test_year"] = t + 1
|
||||
rows[task].append(m)
|
||||
t += cfg["walkforward"]["step_years"]
|
||||
return {k: pd.DataFrame(v) for k, v in rows.items()}
|
||||
|
||||
|
||||
def leave_one_symbol_out(cfg, df, num, cat):
|
||||
rows = {k: [] for k in ("rev", "speed", "disc")}
|
||||
for s in cfg["symbols"]:
|
||||
tr = df[df.symbol != s]
|
||||
te = df[df.symbol == s]
|
||||
for task in rows:
|
||||
r = fit_predict(cfg, task, tr, te, num, cat)
|
||||
if r is None:
|
||||
continue
|
||||
kind, ytr, yte, ptr, pte = r
|
||||
m = (clf_metrics(ytr, yte, ptr, pte) if kind == "clf"
|
||||
else reg_metrics(ytr, yte, pte))
|
||||
m["test_symbol"] = s
|
||||
rows[task].append(m)
|
||||
return {k: pd.DataFrame(v) for k, v in rows.items()}
|
||||
|
||||
|
||||
def run(cfg_path):
|
||||
cfg = load_cfg(cfg_path)
|
||||
np.random.seed(cfg["seed"])
|
||||
num, cat = cfg["features_numeric"], cfg["features_categorical"]
|
||||
df = load_pool(cfg)
|
||||
print(f"pool: {len(df)} incroci su {cfg['symbols']} "
|
||||
f"(drop_trivial={cfg['drop_trivial']})\n")
|
||||
|
||||
print("=== 1) WALK-FORWARD pooled (generalizzazione nel tempo) ===")
|
||||
wf = walk_forward(cfg, df, num, cat)
|
||||
print(f"[rev] AUC medio={wf['rev'].auc.mean():.3f} acc={wf['rev'].acc.mean():.3f}"
|
||||
f" (acc baseline maggioritaria={wf['rev'].acc_baseline.mean():.3f})")
|
||||
print(f"[speed] MAE={wf['speed'].mae.mean():.2f}g baseline={wf['speed'].mae_baseline.mean():.2f}g"
|
||||
f" skill={wf['speed']['skill_%'].mean():+.1f}%")
|
||||
print(f"[disc] MAE={wf['disc'].mae.mean():.3f}% baseline={wf['disc'].mae_baseline.mean():.3f}%"
|
||||
f" skill={wf['disc']['skill_%'].mean():+.1f}%")
|
||||
|
||||
print("\n=== 2) LEAVE-ONE-SYMBOL-OUT (generalizzazione tra strumenti) ===")
|
||||
loso = leave_one_symbol_out(cfg, df, num, cat)
|
||||
for _, r in loso["rev"].iterrows():
|
||||
print(f"[rev] test {r['test_symbol']:7s} AUC={r['auc']:.3f} acc={r['acc']:.3f}"
|
||||
f" (base {r['base_rate']:.3f})")
|
||||
for _, r in loso["speed"].iterrows():
|
||||
print(f"[speed] test {r['test_symbol']:7s} MAE={r['mae']:.2f}g base={r['mae_baseline']:.2f}g"
|
||||
f" skill={r['skill_%']:+.1f}%")
|
||||
for _, r in loso["disc"].iterrows():
|
||||
print(f"[disc] test {r['test_symbol']:7s} MAE={r['mae']:.3f}% base={r['mae_baseline']:.3f}%"
|
||||
f" skill={r['skill_%']:+.1f}%")
|
||||
|
||||
# ---- modelli finali su tutto il pool ----
|
||||
os.makedirs(RESULTS, exist_ok=True)
|
||||
saved = {}
|
||||
for task in ("rev", "speed", "disc"):
|
||||
y, mask = make_targets(df, cfg["horizon"], cfg["drop_trivial"], cfg["trivial_eps"])[task]
|
||||
X = select_xy(df[mask], num, cat)
|
||||
mdl = make_model(cfg, num, cat) if task == "rev" else make_regressor(cfg, num, cat)
|
||||
mdl.fit(X, y[mask])
|
||||
try:
|
||||
import joblib
|
||||
path = os.path.join(RESULTS, f"mr_model_{task}.joblib")
|
||||
joblib.dump(mdl, path); saved[task] = path
|
||||
except Exception as e:
|
||||
print("non serializzato", task, e)
|
||||
|
||||
for task in ("rev", "speed", "disc"):
|
||||
wf[task].to_csv(os.path.join(RESULTS, f"mr_wf_{task}.csv"), index=False)
|
||||
loso[task].to_csv(os.path.join(RESULTS, f"mr_loso_{task}.csv"), index=False)
|
||||
with open(os.path.join(RESULTS, "mr_summary.json"), "w") as f:
|
||||
json.dump({
|
||||
"wf": {k: v.mean(numeric_only=True).to_dict() for k, v in wf.items()},
|
||||
"loso": {k: v.mean(numeric_only=True).to_dict() for k, v in loso.items()},
|
||||
}, f, indent=2)
|
||||
print(f"\nsalvati risultati e modelli in {RESULTS}")
|
||||
return wf, loso
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cfg_path = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "..", "config_mr.yaml")
|
||||
run(cfg_path)
|
||||
Reference in New Issue
Block a user