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:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
0187cb6bd7
commit
7d329a0591
@@ -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
|
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)
|
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.
|
(skl2onnx) per la Fase 4.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from sklearn.ensemble import HistGradientBoostingClassifier
|
from sklearn.ensemble import (HistGradientBoostingClassifier,
|
||||||
|
HistGradientBoostingRegressor)
|
||||||
from sklearn.linear_model import LogisticRegression
|
from sklearn.linear_model import LogisticRegression
|
||||||
from sklearn.pipeline import Pipeline
|
from sklearn.pipeline import Pipeline
|
||||||
from features import build_preprocessor
|
from features import build_preprocessor
|
||||||
@@ -29,3 +30,18 @@ def make_model(cfg: dict, num_cols: list[str], cat_cols: list[str]) -> Pipeline:
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"model.kind sconosciuto: {kind}")
|
raise ValueError(f"model.kind sconosciuto: {kind}")
|
||||||
return Pipeline([("pre", pre), ("clf", clf)])
|
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