Compare commits

...

8 Commits

Author SHA1 Message Date
pietro1991-dot f4ec58440a Merge pull request #19 from pietro1991-dot/devin/1781688049-validazione-esterna
* PaPP v2 Modello: validazione esterna multi-simbolo (no pollution)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* 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>

---------

Co-authored-by: Pietro Giacobazzi <giacobazzipietro@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-17 11:57:13 +02:00
Pietro Giacobazzi 7d329a0591 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>
2026-06-17 09:28:15 +00:00
Pietro Giacobazzi 0187cb6bd7 PaPP v2 Modello: validazione esterna multi-simbolo (no pollution)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-17 09:21:43 +00:00
pietro1991-dot 58ed32ff3b Merge pull request #18 from pietro1991-dot/devin/1781682226-modello-fase3
* PaPP v2: Fase 3 - architettura classificatore extra-rendimento (walk-forward + OOS)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* PaPP v2 Modello: self-test (reproducibilita', anti-leakage, varianti)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* PaPP v2 Modello: documenta selftest nel README

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Pietro Giacobazzi <giacobazzipietro@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-17 11:15:06 +02:00
Pietro Giacobazzi 375dbf1dc3 PaPP v2 Modello: documenta selftest nel README
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-17 07:48:33 +00:00
Pietro Giacobazzi 4084008642 PaPP v2 Modello: self-test (reproducibilita', anti-leakage, varianti)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-17 07:48:26 +00:00
Pietro Giacobazzi fe10be5ecc PaPP v2: Fase 3 - architettura classificatore extra-rendimento (walk-forward + OOS)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-17 07:45:46 +00:00
pietro1991-dot d441454767 Merge pull request #17 from pietro1991-dot/devin/1781681941-analisi-docs
Co-authored-by: Pietro Giacobazzi <giacobazzipietro@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-17 09:42:22 +02:00
29 changed files with 1338 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
data/*.csv
results/*.joblib
src/__pycache__/
*.pyc
+111
View File
@@ -0,0 +1,111 @@
# 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)
sarà **positivo o no**. È lo scheletro modulare della Fase 3, già pronto per
l'export ONNX della Fase 4.
> Stato: **scaffolding funzionante end-to-end**. Il modello di riferimento è un
> baseline volutamente semplice; i risultati attuali confermano che l'edge è
> debole (vedi sotto) e indicano dove migliorare. L'obiettivo di questa cartella
> è la **struttura**, non ancora un modello ottimizzato.
---
## 1. Idea e target
- **Target** (binario): `1` se `extra-rendimento a 10g > 0`, altrimenti `0`, dove
`extra = cret_10(incrocio) media cret_10 della baseline nello stesso regime`.
- **Regime** = `trend (sopra/sotto MA365) × tercile(cluster) × tercile(velocità)`.
- **Feature** (solo informazioni note all'incrocio, nessun esito futuro):
`dist_med_pct, cluster_pct, cluster_exp, slope_a, slope_b, vel_med, acc_med,
vol_med` (numeriche) + `pair, dir, trend, dow, month` (categoriche).
### Niente leakage (punto chiave)
Terzili del regime e medie di baseline sono stimati **solo sul training**
(`labeling.RegimeBaseline.fit`) e applicati a validazione/OOS. Così la label di
test non usa mai informazione futura.
---
## 2. Validazione
- **Walk-forward espansivo**: train = tutti gli anni fino a T, test = anno T+1,
scorrendo T (configurabile `min_train_years`, `step_years`).
- **Hold-out OOS**: tutti gli anni `>= oos_start_year` (default **2020**) sono
tenuti fuori dal training e usati solo alla fine.
Metriche (`evaluate.py`): AUC, accuracy, Brier, e **precisione/lift sul decile a
probabilità più alta** (uso pratico: si opera solo sui segnali più forti).
---
## 3. Struttura
```
Modello/
├── config.yaml tutti i parametri (target, regime, feature, split, modello)
├── requirements.txt
├── src/
│ ├── data.py caricamento CSV + etichetta di regime
│ ├── labeling.py RegimeBaseline: baseline per regime e target (fit su train)
│ ├── features.py ColumnTransformer (one-hot + impute), selezione X
│ ├── splits.py split OOS + walk-forward espansivo
│ ├── model.py factory modello (hist_gbdt | logistic)
│ ├── evaluate.py metriche
│ └── train.py pipeline end-to-end
├── results/ metriche walk-forward + OOS (modello .joblib non versionato)
└── data/ i 2 CSV di input (non versionati)
```
## 4. Come eseguire
```bash
pip install -r requirements.txt
# copia i 2 CSV in ./data (vedi data/README.md), poi:
cd src
python train.py # usa ../config.yaml
python selftest.py # verifiche di correttezza (reproducibilita', anti-leakage, varianti)
```
## 5. Risultati attuali (EURUSD, hold-out 2020+)
Con il baseline `hist_gbdt` e tutte le coppie:
- walk-forward medio **AUC ≈ 0.51**, OOS 2020+ **AUC ≈ 0.52**, lift sul decile ≈ 1.0×.
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
`restrict_pairs` con gli incroci MA121/MA182/MA365 emersi in Fase 2.
- **Target alternativi**: classificare solo le code (es. extra nel top/bottom 30%)
invece del semplice segno; oppure regressione sull'extra-rendimento.
- **Feature aggiuntive**: percentile storico delle metriche (come nel pannello),
interazioni coppia×regime, stato di mean-reversion in corso.
- **Calibrazione** delle probabilità e scelta soglia per massimizzare la
precisione sui segnali operativi.
- **Fase 4**: export del `Pipeline` in **ONNX** (skl2onnx) e inferenza in MT5.
+123
View File
@@ -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'8790% 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.
+53
View File
@@ -0,0 +1,53 @@
# Configurazione Fase 3 — classificatore extra-rendimento positivo
symbol: EURUSD
year_min: 1999 # esclude i dati EURUSD sintetici pre-1999
# --- target ---
horizon: 10 # orizzonte (giorni D1) per l'extra-rendimento
target: excess_positive # 1 se extra-rendimento a `horizon` > 0, altrimenti 0
# --- definizione del regime per la baseline (terzili stimati SOLO sul train) ---
regime:
use_trend: true # sopra/sotto MA365
cluster_terciles: true
velocity_terciles: true
# --- feature usate dal modello (note al momento dell'incrocio) ---
features_numeric:
- 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 # hold-out finale mai usato in training
walkforward:
scheme: expanding # finestra espansiva
min_train_years: 8 # anni minimi nel primo fold
step_years: 1
# --- modello di riferimento ---
model:
kind: hist_gbdt # hist_gbdt | logistic
hist_gbdt:
max_depth: 4
learning_rate: 0.05
max_iter: 400
l2_regularization: 1.0
early_stopping: true
# opzionale: limita ai pattern robusti (q<0.10). Vuoto = usa tutti gli incroci.
restrict_pairs: []
seed: 42
+44
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
# Dati di input (non versionati)
Copia qui i due CSV prodotti da `PaPP v2/PaPP_CrossExport.mq5`:
- `PaPP_crosses_<SYMBOL>_D1.csv`
- `PaPP_bars_<SYMBOL>_D1.csv`
Sono gli stessi file usati in `PaPP v2/Analisi/`. Poi lancia `src/train.py`.
+8
View File
@@ -0,0 +1,8 @@
pandas
numpy
scikit-learn
pyyaml
joblib
# Fase 4 (export ONNX), opzionali finche' non si esporta:
# skl2onnx
# onnxruntime
@@ -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
+5
View File
@@ -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
1 n mae mae_baseline skill_% r2 test_symbol
2 15898 0.7016688523942158 0.7156681469367216 1.956115359112609 0.056705406202969044 EURUSD
3 6635 0.766700972158277 0.8138184204973624 5.789675823544238 0.09699286957940556 USDJPY
4 7349 0.7110993410001212 0.703173893046673 -1.1270964453912402 0.13820883251935367 USDCHF
5 6807 0.7712259995571173 0.7706658777728809 -0.07268023671362922 0.10481685322904755 GBPUSD
+5
View File
@@ -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
1 n base_rate acc_baseline auc acc test_symbol
2 15887 0.7324227355699628 0.7324227355699628 0.6765454819075187 0.7357587965002832 EURUSD
3 6619 0.7028252001812962 0.7028252001812962 0.6869979773747487 0.719292944553558 USDJPY
4 7331 0.7603328331741918 0.7603328331741918 0.6816590320250597 0.7442368026190151 USDCHF
5 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
1 n mae mae_baseline skill_% r2 test_symbol
2 13909 3.610173858972498 3.65878208354303 1.3285356564187945 0.09908303164055288 EURUSD
3 5729 3.615529034826519 3.7987432361668705 4.823021456044085 0.1281263482188003 USDJPY
4 6570 3.627862458240022 3.5245053272450533 -2.9325287210094197 0.07345132592032444 USDCHF
5 6090 3.638542831980693 3.744006568144499 2.8168683533072225 0.11825074333860008 GBPUSD
+51
View File
@@ -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
}
}
}
+21
View File
@@ -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
1 n mae mae_baseline skill_% r2 test_year
2 468 0.5210450337997958 0.4877705555555555 -6.82174802584008 -0.440278374819538 2007
3 511 1.4979005200832076 1.46627927592955 -2.1565635327970734 -0.4428980601648529 2008
4 590 0.5372706093503566 0.624245779661017 13.932840740691965 0.09814855360679986 2009
5 548 0.8435515547481117 0.9210056204379563 8.40972779873087 -0.20129888622520697 2010
6 649 0.6264895295465607 0.7349227426810478 14.754368974746313 -0.23270335993701874 2011
7 563 0.8444189035765441 0.7120972291296623 -18.581967326092208 -0.6572098948790617 2012
8 574 0.6570009471217217 0.5640415679442509 -16.480944749564785 -1.9530900867332583 2013
9 1089 0.8585576108637076 0.8849660055096419 2.984114020371431 -0.0058690268732339845 2014
10 2465 0.86287949087831 0.8346033630831645 -3.387971945223023 0.1741985535476418 2015
11 2440 0.9893521953097881 0.9588108852459016 -3.1853320121677298 0.06164644338816894 2016
12 2405 0.6483027913003414 0.606651659043659 -6.865741094707012 -0.3827607195275795 2017
13 2208 0.716304832228868 0.6826646195652174 -4.927780303756735 -0.30219708772441267 2018
14 2520 0.7276522650483845 0.693340365079365 -4.948781536048585 -0.9380918084727767 2019
15 2351 1.0262956818683626 0.9582834198213527 -7.097301345325269 -0.054525480586665065 2020
16 2378 0.5921425747388034 0.566631362489487 -4.502259129680586 -0.6404632411590949 2021
17 2074 0.9417510265311481 1.0620946142719383 11.330778456426438 -0.41893129794772066 2022
18 2185 0.6089553673717031 0.5467215652173913 -11.383088964044408 -0.23632995343602725 2023
19 2317 0.7212702214350022 0.6373334527406128 -13.169992620574188 -0.24062608198432445 2024
20 2335 0.7021795509288201 0.7111189250535332 1.2570856729822166 0.1140083899835217 2025
21 1264 0.7718037117074369 0.6608526424050633 -16.78907856047691 -0.5414077013318492 2026
+21
View File
@@ -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
1 n base_rate acc_baseline auc acc test_year
2 468 0.6175213675213675 0.6175213675213675 0.641878177495119 0.6260683760683761 2007
3 511 0.6516634050880626 0.6516634050880626 0.5756149407834801 0.6594911937377691 2008
4 590 0.7440677966101695 0.7440677966101695 0.5982138816394877 0.7372881355932204 2009
5 548 0.7408759124087592 0.7408759124087592 0.6361617983764658 0.7299270072992701 2010
6 649 0.5855161787365177 0.5855161787365177 0.6710135002934846 0.6718027734976888 2011
7 563 0.8206039076376554 0.8206039076376554 0.6997342591401998 0.7708703374777975 2012
8 574 0.6933797909407665 0.6933797909407665 0.6654865235267245 0.6428571428571429 2013
9 1089 0.6464646464646465 0.6464646464646465 0.6203659976387249 0.6721763085399449 2014
10 2465 0.7829614604462475 0.7829614604462475 0.6354597840298292 0.7517241379310344 2015
11 2440 0.7487704918032787 0.7487704918032787 0.6421575586789066 0.6905737704918032 2016
12 2405 0.7600831600831601 0.7600831600831601 0.6408060252797805 0.755925155925156 2017
13 2208 0.7336956521739131 0.7336956521739131 0.6192271352985639 0.7133152173913043 2018
14 2520 0.8067460317460318 0.8067460317460318 0.6465879719737271 0.7746031746031746 2019
15 2351 0.7243726074011059 0.7243726074011059 0.6688414780017834 0.7209698000850702 2020
16 2378 0.7863751051303617 0.7863751051303617 0.6841361741547013 0.7901597981497056 2021
17 2074 0.6769527483124397 0.6769527483124397 0.5842326827401454 0.6557377049180327 2022
18 2185 0.6668192219679634 0.6668192219679634 0.6764596076538425 0.6906178489702517 2023
19 2317 0.6551575312904618 0.6551575312904618 0.6896672553471812 0.6870953819594303 2024
20 2335 0.7430406852248393 0.7430406852248393 0.7314169068203651 0.7644539614561028 2025
21 1210 0.7826446280991736 0.7826446280991736 0.6295204789188191 0.7545454545454545 2026
+21
View File
@@ -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
1 n mae mae_baseline skill_% r2 test_year
2 338 3.465379140810698 3.4142011834319526 -1.498973101734502 0.12927353007997744 2007
3 400 4.090858376700547 3.9425 -3.7630533088280904 0.04579965148367826 2008
4 540 4.0205317754813965 3.8740740740740742 -3.780456919691866 -0.12200728544542727 2009
5 470 3.1693714434220257 3.421276595744681 7.362899352714425 0.2246464741787182 2010
6 495 4.314538334266083 4.3232323232323235 0.20109927749014656 -0.055325713812715005 2011
7 515 3.2237999945625653 2.9029126213592233 -11.053979745800735 -0.06562655938335582 2012
8 485 3.733393114617742 4.072164948453608 8.319207058754174 0.19434702822584105 2013
9 879 3.924266870235131 4.180887372013652 6.137943430294424 0.022748161878883844 2014
10 2243 3.398709049328624 3.3410610789121713 -1.7254389864438613 0.07268045675088919 2015
11 2243 3.6650255510608725 3.7588051716451183 2.494931668490852 0.06864404910302002 2016
12 2206 3.65145803728882 3.8028105167724386 3.9800163278205125 0.10526816265720629 2017
13 1923 3.577364530993464 3.4607384295371815 -3.3699773568810043 0.07894792932730887 2018
14 2354 3.356457496216363 3.373406966864911 0.502443696092203 0.015425103761181336 2019
15 2116 3.8333614130177756 3.972117202268431 3.4932450928540915 0.049572458729656765 2020
16 2190 3.3981138450501156 3.4990867579908675 2.8856933229837733 0.12873115565641913 2021
17 1715 3.799187552337051 3.816909620991254 0.4643041207142984 0.0433313417575899 2022
18 1810 4.043300720188006 3.8784530386740332 -4.250346204277644 0.06762624567274822 2023
19 1997 3.6962952238115996 4.476214321482224 17.423631704309617 0.2427214165484335 2024
20 2094 3.369937232661497 3.7597898758357213 10.369000823152874 0.2265055214709174 2025
21 1118 3.3428725437687103 3.4042933810375673 1.804216922401003 0.09336407603657426 2026
+9
View File
@@ -0,0 +1,9 @@
{
"n": 3951,
"base_rate": 0.49253353581371806,
"auc": 0.5241849128463528,
"acc": 0.5206276891926095,
"brier": 0.2758629924455611,
"prec_top_decile": 0.48860759493670886,
"lift_top_decile": 0.9920290892060312
}
@@ -0,0 +1,14 @@
n,base_rate,auc,acc,brier,prec_top_decile,lift_top_decile,fold,test_years
506,0.6027667984189723,0.5552565043634288,0.5533596837944664,0.2929861795083319,0.66,1.0949508196721314,0,2007-2007
528,0.4431818181818182,0.4868160939589511,0.48674242424242425,0.3025718576380564,0.19230769230769232,0.43392504930966475,1,2008-2008
618,0.5857605177993528,0.48292904005524856,0.46116504854368934,0.32693476077811057,0.5081967213114754,0.8675844579295353,2,2009-2009
570,0.44912280701754387,0.5532568670382165,0.531578947368421,0.28012422532136644,0.631578947368421,1.40625,3,2010-2010
669,0.554559043348281,0.3955570831599703,0.4200298953662182,0.3276694334072587,0.3484848484848485,0.6283999019848078,4,2011-2011
616,0.5227272727272727,0.5127920733510796,0.5032467532467533,0.2825509892233342,0.5901639344262295,1.129009265858874,5,2012-2012
583,0.5265866209262435,0.5670466883821933,0.5214408233276158,0.2796922887853283,0.7068965517241379,1.342412669886555,6,2013-2013
565,0.2884955752212389,0.5871104599700881,0.4424778761061947,0.3559192608214337,0.17857142857142858,0.6189745836985101,7,2014-2014
627,0.45614035087719296,0.46299448352234274,0.49920255183413076,0.31880860582951115,0.3709677419354839,0.8132754342431763,8,2015-2015
749,0.4512683578104139,0.5659165838840179,0.5046728971962616,0.2895300011880442,0.6756756756756757,1.4972813049736124,9,2016-2016
628,0.6496815286624203,0.4584336007130124,0.49044585987261147,0.2895065709863026,0.3870967741935484,0.5958254269449715,10,2017-2017
622,0.5,0.41347794170862584,0.45980707395498394,0.29186125547058084,0.2903225806451613,0.5806451612903226,11,2018-2018
707,0.36633663366336633,0.5378947186982901,0.5487977369165488,0.27594238285206885,0.5142857142857142,1.4038610038610038,12,2019-2019
1 n base_rate auc acc brier prec_top_decile lift_top_decile fold test_years
2 506 0.6027667984189723 0.5552565043634288 0.5533596837944664 0.2929861795083319 0.66 1.0949508196721314 0 2007-2007
3 528 0.4431818181818182 0.4868160939589511 0.48674242424242425 0.3025718576380564 0.19230769230769232 0.43392504930966475 1 2008-2008
4 618 0.5857605177993528 0.48292904005524856 0.46116504854368934 0.32693476077811057 0.5081967213114754 0.8675844579295353 2 2009-2009
5 570 0.44912280701754387 0.5532568670382165 0.531578947368421 0.28012422532136644 0.631578947368421 1.40625 3 2010-2010
6 669 0.554559043348281 0.3955570831599703 0.4200298953662182 0.3276694334072587 0.3484848484848485 0.6283999019848078 4 2011-2011
7 616 0.5227272727272727 0.5127920733510796 0.5032467532467533 0.2825509892233342 0.5901639344262295 1.129009265858874 5 2012-2012
8 583 0.5265866209262435 0.5670466883821933 0.5214408233276158 0.2796922887853283 0.7068965517241379 1.342412669886555 6 2013-2013
9 565 0.2884955752212389 0.5871104599700881 0.4424778761061947 0.3559192608214337 0.17857142857142858 0.6189745836985101 7 2014-2014
10 627 0.45614035087719296 0.46299448352234274 0.49920255183413076 0.31880860582951115 0.3709677419354839 0.8132754342431763 8 2015-2015
11 749 0.4512683578104139 0.5659165838840179 0.5046728971962616 0.2895300011880442 0.6756756756756757 1.4972813049736124 9 2016-2016
12 628 0.6496815286624203 0.4584336007130124 0.49044585987261147 0.2895065709863026 0.3870967741935484 0.5958254269449715 10 2017-2017
13 622 0.5 0.41347794170862584 0.45980707395498394 0.29186125547058084 0.2903225806451613 0.5806451612903226 11 2018-2018
14 707 0.36633663366336633 0.5378947186982901 0.5487977369165488 0.27594238285206885 0.5142857142857142 1.4038610038610038 12 2019-2019
+54
View File
@@ -0,0 +1,54 @@
"""Caricamento dei dati e definizione del regime.
I dati sono i due CSV prodotti da `PaPP v2/PaPP_CrossExport.mq5`:
- PaPP_crosses_<SYM>_D1.csv (un record per incrocio)
- PaPP_bars_<SYM>_D1.csv (un record per giorno D1 = baseline)
Per default cercati in ../data; sovrascrivibili via argomento.
NB sulla correttezza temporale: i terzili del regime e le medie di baseline
NON vengono calcolati qui su tutti i dati, ma stimati sul SOLO training
(vedi labeling.RegimeBaseline.fit) e applicati al test. Qui carichiamo solo.
"""
from __future__ import annotations
import os
import numpy as np
import pandas as pd
HERE = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.normpath(os.path.join(HERE, "..", "data"))
def _read(path: str) -> pd.DataFrame:
df = pd.read_csv(path)
df["time"] = pd.to_datetime(df["time"], format="%Y.%m.%d", errors="coerce")
if df["time"].isna().all():
df["time"] = pd.to_datetime(df["time"], errors="coerce")
return df.dropna(subset=["time"]).sort_values("time").reset_index(drop=True)
def load(symbol: str = "EURUSD", year_min: int = 1999, data_dir: str | None = None):
d = data_dir or DATA_DIR
crosses = _read(os.path.join(d, f"PaPP_crosses_{symbol}_D1.csv"))
bars = _read(os.path.join(d, f"PaPP_bars_{symbol}_D1.csv"))
if year_min:
crosses = crosses[crosses.time.dt.year >= year_min].reset_index(drop=True)
bars = bars[bars.time.dt.year >= year_min].reset_index(drop=True)
return crosses, bars
def regime_labels(df: pd.DataFrame, edges: dict, cfg_regime: dict) -> pd.Series:
"""Etichetta di regime coerente con `edges` (terzili stimati sul train)."""
parts = []
if cfg_regime.get("use_trend", True):
parts.append((df["trend"] > 0).astype(int).astype(str))
if cfg_regime.get("cluster_terciles", True):
parts.append(np.digitize(df["cluster_pct"], edges["cl"]).astype(str))
if cfg_regime.get("velocity_terciles", True):
parts.append(np.digitize(df["vel_med"], edges["ve"]).astype(str))
if not parts:
return pd.Series(["all"] * len(df), index=df.index)
out = parts[0]
for p in parts[1:]:
out = out + "_" + p
return out
+23
View File
@@ -0,0 +1,23 @@
"""Metriche di valutazione del classificatore."""
from __future__ import annotations
import numpy as np
from sklearn.metrics import roc_auc_score, accuracy_score, brier_score_loss
def metrics(y_true, p_pred) -> dict:
y_true = np.asarray(y_true)
p = np.asarray(p_pred)
out = {"n": int(len(y_true)), "base_rate": float(y_true.mean())}
if len(np.unique(y_true)) < 2:
out.update({"auc": np.nan, "acc": np.nan, "brier": np.nan,
"prec_top_decile": np.nan, "lift_top_decile": np.nan})
return out
out["auc"] = float(roc_auc_score(y_true, p))
out["acc"] = float(accuracy_score(y_true, (p >= 0.5).astype(int)))
out["brier"] = float(brier_score_loss(y_true, p))
# precisione sul decile a piu' alta probabilita' (uso pratico: prendo solo i segnali piu' forti)
k = max(1, int(0.1 * len(p)))
top = np.argsort(p)[-k:]
out["prec_top_decile"] = float(y_true[top].mean())
out["lift_top_decile"] = float(y_true[top].mean() / max(y_true.mean(), 1e-9))
return out
+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()
+33
View File
@@ -0,0 +1,33 @@
"""Feature engineering: costruisce la matrice X dalle colonne di contesto.
Le feature sono SOLO informazioni note al momento dell'incrocio (nessun esito
futuro). Categoriche e numeriche sono gestite da un ColumnTransformer
(one-hot per le categoriche, passthrough per le numeriche) per restare
compatibili con l'export ONNX della Fase 4.
"""
from __future__ import annotations
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
def build_preprocessor(num_cols: list[str], cat_cols: list[str]) -> ColumnTransformer:
num = Pipeline([("impute", SimpleImputer(strategy="median"))])
cat = Pipeline([
("impute", SimpleImputer(strategy="most_frequent")),
("oh", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
])
return ColumnTransformer([
("num", num, num_cols),
("cat", cat, cat_cols),
])
def select_xy(df: pd.DataFrame, num_cols: list[str], cat_cols: list[str]):
cols = num_cols + cat_cols
X = df[cols].copy()
for c in cat_cols:
X[c] = X[c].astype(str)
return X
+40
View File
@@ -0,0 +1,40 @@
"""Costruzione del target: extra-rendimento positivo a `horizon` giorni.
extra = cret_h(incrocio) - baseline_mean_h(regime)
`RegimeBaseline` impara i terzili del regime e la media di baseline per regime
SOLO sui dati di training (fit), e li applica a qualunque set (transform).
Cosi' la label di test non usa informazione futura.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from data import regime_labels
class RegimeBaseline:
def __init__(self, cfg_regime: dict, horizon: int):
self.cfg_regime = cfg_regime
self.h = horizon
self.edges_: dict = {}
self.base_mean_: pd.Series | None = None
self.base_glob_: float = 0.0
def fit(self, bars_train: pd.DataFrame) -> "RegimeBaseline":
self.edges_ = {
"cl": np.quantile(bars_train["cluster_pct"], [1/3, 2/3]),
"ve": np.quantile(bars_train["vel_med"], [1/3, 2/3]),
}
reg = regime_labels(bars_train, self.edges_, self.cfg_regime)
col = f"cret_{self.h}"
self.base_mean_ = bars_train.assign(_r=reg).groupby("_r")[col].mean()
self.base_glob_ = float(bars_train[col].mean())
return self
def excess(self, crosses: pd.DataFrame) -> np.ndarray:
reg = regime_labels(crosses, self.edges_, self.cfg_regime)
bexp = reg.map(self.base_mean_).fillna(self.base_glob_).values
return crosses[f"cret_{self.h}"].values - bexp
def label(self, crosses: pd.DataFrame) -> np.ndarray:
return (self.excess(crosses) > 0).astype(int)
+47
View File
@@ -0,0 +1,47 @@
"""Factory del modello di riferimento.
`hist_gbdt` (HistGradientBoosting) come baseline forte e veloce; `logistic`
come riferimento lineare interpretabile. Entrambi esportabili in ONNX
(skl2onnx) per la Fase 4.
"""
from __future__ import annotations
from sklearn.ensemble import (HistGradientBoostingClassifier,
HistGradientBoostingRegressor)
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from features import build_preprocessor
def make_model(cfg: dict, num_cols: list[str], cat_cols: list[str]) -> Pipeline:
kind = cfg["model"]["kind"]
pre = build_preprocessor(num_cols, cat_cols)
if kind == "hist_gbdt":
p = cfg["model"]["hist_gbdt"]
clf = HistGradientBoostingClassifier(
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"],
)
elif kind == "logistic":
clf = LogisticRegression(max_iter=1000, C=1.0, random_state=cfg["seed"])
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)])
+55
View File
@@ -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
+52
View File
@@ -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)
+192
View File
@@ -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)
+80
View File
@@ -0,0 +1,80 @@
"""Self-test della pipeline Fase 3 (controlli di correttezza, non un training serio).
Esegue:
T1 reproducibilita': due run identiche danno stesse metriche OOS
T2 controllo negativo anti-leakage: con label OOS mescolate l'AUC deve ~0.5
T3 no temporal leakage: la baseline OOS dipende SOLO dallo sviluppo
T4 variante logistic gira senza errori
T5 restrict_pairs (solo pattern robusti) gira senza errori
T6 le feature non contengono colonne di esito futuro
"""
import os, sys, copy
import numpy as np
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 splits import split_oos
CFG = os.path.join(HERE, "..", "config.yaml")
ok = True
def check(name, cond):
global ok
print(("PASS" if cond else "FAIL"), "-", name)
ok = ok and cond
# T6: nessuna feature e' un esito futuro
cfg = T.load_cfg(CFG)
future_like = ("ret_", "mfe_", "mae_", "dir_", "rev_", "cret_", "bars_to_revert", "disc_max")
feats = cfg["features_numeric"] + cfg["features_categorical"]
check("T6 feature senza esiti futuri",
not any(f.startswith(future_like) or f in ("bars_to_revert",) for f in feats))
# T1: reproducibilita'
folds1, oos1 = T.run(CFG)
folds2, oos2 = T.run(CFG)
check("T1 reproducibilita' OOS AUC", abs(oos1["auc"] - oos2["auc"]) < 1e-9)
# T3: baseline OOS non dipende dai dati OOS (no temporal leakage)
crosses, bars = load(cfg["symbol"], cfg["year_min"])
crosses = crosses.dropna(subset=[f"cret_{cfg['horizon']}"]).reset_index(drop=True)
dev_cr, oos_cr = split_oos(crosses, cfg["oos_start_year"])
dev_ba, oos_ba = split_oos(bars, cfg["oos_start_year"])
rb_dev = RegimeBaseline(cfg["regime"], cfg["horizon"]).fit(dev_ba)
lbl_a = rb_dev.label(oos_cr)
# se cambio i dati OOS, l'edges/baseline (fittati su dev) NON cambiano
rb_dev2 = RegimeBaseline(cfg["regime"], cfg["horizon"]).fit(dev_ba)
lbl_b = rb_dev2.label(oos_cr)
check("T3 baseline OOS deterministica e da solo-sviluppo", np.array_equal(lbl_a, lbl_b)
and np.allclose(rb_dev.edges_["cl"], rb_dev2.edges_["cl"]))
# T2: controllo negativo (label mescolate -> AUC ~0.5)
from evaluate import metrics
from features import select_xy
from model import make_model
rng = np.random.default_rng(0)
y_dev = rb_dev.label(dev_cr)
final = make_model(cfg, cfg["features_numeric"], cfg["features_categorical"])
final.fit(select_xy(dev_cr, cfg["features_numeric"], cfg["features_categorical"]), y_dev)
p_oos = final.predict_proba(select_xy(oos_cr, cfg["features_numeric"], cfg["features_categorical"]))[:, 1]
y_oos = rb_dev.label(oos_cr)
y_shuf = y_oos.copy(); rng.shuffle(y_shuf)
m_shuf = metrics(y_shuf, p_oos)
check("T2 controllo negativo AUC~0.5 (0.45-0.55)", 0.45 <= m_shuf["auc"] <= 0.55)
# T4: variante logistic
cfg_log = copy.deepcopy(cfg); cfg_log["model"]["kind"] = "logistic"
ml = make_model(cfg_log, cfg["features_numeric"], cfg["features_categorical"])
ml.fit(select_xy(dev_cr, cfg["features_numeric"], cfg["features_categorical"]), y_dev)
pl = ml.predict_proba(select_xy(oos_cr, cfg["features_numeric"], cfg["features_categorical"]))[:, 1]
check("T4 logistic produce probabilita' valide", np.all((pl >= 0) & (pl <= 1)))
# T5: restrict_pairs
robust = ["MA365xMA7", "MA121xMA7", "MA121xMA3", "PRICExMA121", "MA182xMA30"]
sub = dev_cr[dev_cr.pair.isin(robust)]
check("T5 restrict_pairs ha campioni", len(sub) > 100)
print("\n=== RISULTATO SELF-TEST:", "TUTTO OK" if ok else "CI SONO FAIL", "===")
sys.exit(0 if ok else 1)
+30
View File
@@ -0,0 +1,30 @@
"""Split temporali: walk-forward espansivo + hold-out finale out-of-sample.
- `walk_forward_folds`: per la validazione interna sui dati pre-OOS. Finestra
espansiva: train = [inizio .. anno T], test = anno T+1, scorrendo T.
- L'hold-out OOS (>= oos_start_year) e' tenuto separato e usato solo alla fine.
"""
from __future__ import annotations
import pandas as pd
def split_oos(df: pd.DataFrame, oos_start_year: int):
dev = df[df.time.dt.year < oos_start_year].reset_index(drop=True)
oos = df[df.time.dt.year >= oos_start_year].reset_index(drop=True)
return dev, oos
def walk_forward_folds(df: pd.DataFrame, min_train_years: int, step_years: int = 1):
"""Genera (train_idx, test_idx) su `df` (gia' solo periodo di sviluppo)."""
years = sorted(df.time.dt.year.unique())
if len(years) <= min_train_years:
return
start = years[0]
t = start + min_train_years - 1
while t < years[-1]:
test_years = list(range(t + 1, min(t + 1 + step_years, years[-1] + 1)))
train_idx = df.index[df.time.dt.year <= t].to_numpy()
test_idx = df.index[df.time.dt.year.isin(test_years)].to_numpy()
if len(test_idx) > 0 and len(train_idx) > 0:
yield train_idx, test_idx
t += step_years
+108
View File
@@ -0,0 +1,108 @@
"""Pipeline Fase 3 end-to-end.
1. carica incroci + baseline
2. tiene da parte l'hold-out OOS (>= oos_start_year)
3. walk-forward espansivo sul periodo di sviluppo:
- per ogni fold: fit baseline+terzili sul SOLO train, costruisce le label,
addestra il modello, valuta sul fold di test
4. riaddestra sul periodo di sviluppo completo e valuta sull'hold-out OOS
5. salva metriche e modello finale
Uso:
python train.py [config.yaml]
"""
from __future__ import annotations
import os, sys, json
import numpy as np
import pandas as pd
import yaml
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from data import load
from labeling import RegimeBaseline
from features import select_xy
from splits import split_oos, walk_forward_folds
from model import make_model
from evaluate import metrics
RESULTS = os.path.normpath(os.path.join(HERE, "..", "results"))
def load_cfg(path: str) -> dict:
with open(path) as f:
return yaml.safe_load(f)
def run(cfg_path: str):
cfg = load_cfg(cfg_path)
np.random.seed(cfg["seed"])
num_cols = cfg["features_numeric"]
cat_cols = cfg["features_categorical"]
h = cfg["horizon"]
crosses, bars = load(cfg["symbol"], cfg["year_min"])
if cfg.get("restrict_pairs"):
crosses = crosses[crosses.pair.isin(cfg["restrict_pairs"])].reset_index(drop=True)
# serve cret_h presente (le ultime barre non hanno il futuro completo)
crosses = crosses.dropna(subset=[f"cret_{h}"]).reset_index(drop=True)
dev_cr, oos_cr = split_oos(crosses, cfg["oos_start_year"])
dev_ba, oos_ba = split_oos(bars, cfg["oos_start_year"])
print(f"sviluppo: {len(dev_cr)} incroci | OOS({cfg['oos_start_year']}+): {len(oos_cr)} incroci")
# ---------- walk-forward ----------
fold_rows = []
for i, (tr_idx, te_idx) in enumerate(walk_forward_folds(
dev_cr, cfg["walkforward"]["min_train_years"], cfg["walkforward"]["step_years"])):
tr, te = dev_cr.loc[tr_idx], dev_cr.loc[te_idx]
# baseline/terzili stimati SOLO sul train (barre fino all'ultimo anno di train)
max_train_year = tr.time.dt.year.max()
ba_tr = dev_ba[dev_ba.time.dt.year <= max_train_year]
rb = RegimeBaseline(cfg["regime"], h).fit(ba_tr)
ytr, yte = rb.label(tr), rb.label(te)
mdl = make_model(cfg, num_cols, cat_cols)
mdl.fit(select_xy(tr, num_cols, cat_cols), ytr)
pte = mdl.predict_proba(select_xy(te, num_cols, cat_cols))[:, 1]
m = metrics(yte, pte)
m["fold"] = i
m["test_years"] = f"{te.time.dt.year.min()}-{te.time.dt.year.max()}"
fold_rows.append(m)
print(f" fold {i} test {m['test_years']:>9} n={m['n']:5d} "
f"AUC={m['auc']:.3f} acc={m['acc']:.3f} "
f"prec@10%={m['prec_top_decile']:.3f} (base {m['base_rate']:.3f})")
folds = pd.DataFrame(fold_rows)
if len(folds):
print(f"\nWalk-forward medio: AUC={folds.auc.mean():.3f} "
f"acc={folds.acc.mean():.3f} prec@10%={folds.prec_top_decile.mean():.3f}")
# ---------- modello finale + OOS ----------
rb_full = RegimeBaseline(cfg["regime"], h).fit(dev_ba)
y_dev = rb_full.label(dev_cr)
final = make_model(cfg, num_cols, cat_cols)
final.fit(select_xy(dev_cr, num_cols, cat_cols), y_dev)
y_oos = rb_full.label(oos_cr) # baseline fittata sullo sviluppo: no leakage
p_oos = final.predict_proba(select_xy(oos_cr, num_cols, cat_cols))[:, 1]
oos_m = metrics(y_oos, p_oos)
print(f"\nHOLD-OUT OOS {cfg['oos_start_year']}+ : n={oos_m['n']} AUC={oos_m['auc']:.3f} "
f"acc={oos_m['acc']:.3f} prec@10%={oos_m['prec_top_decile']:.3f} "
f"(base {oos_m['base_rate']:.3f}, lift {oos_m['lift_top_decile']:.2f}x)")
os.makedirs(RESULTS, exist_ok=True)
folds.to_csv(os.path.join(RESULTS, "walkforward_folds.csv"), index=False)
with open(os.path.join(RESULTS, "oos_metrics.json"), "w") as f:
json.dump(oos_m, f, indent=2)
try:
import joblib
joblib.dump(final, os.path.join(RESULTS, "model_final.joblib"))
except Exception as e:
print("modello non serializzato:", e)
print(f"\nsalvati risultati in {RESULTS}")
return folds, oos_m
if __name__ == "__main__":
cfg_path = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "..", "config.yaml")
run(cfg_path)