Files
Encelado/Encelado/docs/ML_AND_LEARNING.md
T
Alby96andClaude Fable 5.1 9c1bab5224 5.0 Fase 9: documenti, screenshot, risposte D-28/29/36/37, piano concluso
README con gli screenshot e l'avvio rapido Docker; CLAUDE.md, architettura, runbook,
problemi noti, glossario, fonti dei dati e apprendimento riscritti per il container e
l'interfaccia web; il post-mortem spiega gli accrediti del demo (uno per ordine
ridotto, dal ledger ricevuto); le skill seguono la catena nuova. Lo stato dice che il
piano 5.0 è completo e che il rilascio 5.0.0 resta una decisione dell'utente.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 13:24:30 +02:00

118 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Apprendimento: livelli 0-3
Aggiornato: 2026-09-23 (ADR-0006, 5.0). Tutto è costruito da zero nel Core (`src/Encelado.Core/Baskets/Learning/`), senza pacchetti: regressione logistica online, un MLP a 16 unità ReLU con Adam, un bandit di Thompson, due previsori di volatilità. Il codice del motore che li usa a runtime è `src/Encelado.Engine/Baskets/LearningState.cs`; lo stesso ciclo gira offline con `backtest learn`.
**Regola che governa tutto**: nessun livello cambia un parametro live da solo. Il meta-modello può soltanto *rifiutare* un ingresso quando è attivo; il bandit *propone* un preset e **non lo applica** (dalla 5.0; fino alla 4.0.0 lo applicava in Paper e Demo); tutto il resto finisce in `knowledge/proposals.csv` e passa dal forward test pre-registrato.
## Stato dalla 5.0 (ADR-0006): quasi tutto in ombra
`strategy.json``learning` è `{enabled: false, weeklyCycle: false, challenger: false}` di fabbrica. Con `enabled = false`:
| Componente | Runtime |
|---|---|
| Ledger, `orders.jsonl`, `baskets.csv` | scritti sempre: sono il dato |
| Livello 0 (calibrazione) | generato dal ciclo, quindi fermo finché il ciclo non gira (`backtest learn`, o «Esegui ciclo di apprendimento» in Impostazioni ▸ Ricerca) |
| Previsione di volatilità | attiva, deterministica, usata da `z_in` effettivo |
| Logistica (livello 1) | **in ombra**: `p_ML` a ogni chiusura di barra nel ledger, apprendimento a ogni chiusura di basket; mai un cancello (`mlActive = false`) |
| MLP challenger (livello 2) | non addestrato |
| Bandit (livello 3) | riceve i premi e **propone** nel log; non applica |
| Ciclo settimanale | non gira nel bot; passa a `backtest learn` o al pulsante di Impostazioni ▸ Ricerca |
**Criterio di riattivazione** (`learning.enabled = true`, scritto dall'operatore, mai dal bot): almeno **300 basket chiusi in Demo** *e* **P&L netto forward ≥ 0** sulla metrica pre-registrata in `knowledge/preregistrazione.csv`. Prima di allora qualunque «adattamento automatico» è rumore: il meta-modello può solo ridurre le perdite di una strategia che il backtest dice non reggere i costi, e a poche entrate al giorno i 300 basket sono mesi. Motivazione completa in `docs/adr/ADR-0006-apprendimento-in-ombra.md`.
## Il dataset
Una riga per basket **aperto**: le 28 feature scritte nel ledger nel momento della decisione (`decisions.jsonl`, evento `ingresso`), unite per `basket_id` all'esito scritto alla chiusura (`baskets.csv`: `label` = 1 se il P&L netto è positivo, `pnl_net`). Le feature non vengono mai ricostruite a posteriori: il dataset è il ledger (`LearningState.BuildDataset`).
| # | Feature | Origine |
|---|---|---|
| 0-3 | `z`, `abs_z`, `z_in_eff`, `d_pips` | z-score del cross sintetico, soglia effettiva dopo la scala di volatilità, divergenza in pip |
| 4-6 | `rho_w`, `rho_20`, `half_life` | correlazione rolling (finestra W e 20 barre), semiperiodo OLS |
| 7-10 | `atr_a`, `atr_b`, `sigma_x`, `ewma_vol_x` | volatilità delle gambe e del cross |
| 11 | `trend_strength` | forza di trend (ADX-like) del cross |
| 12-15 | `spread_a`, `spread_b`, `cost_pips`, `break_even_win_rate` | costi del momento |
| 16-18 | `hour_sin`, `hour_cos`, `day_of_week` | ora UTC ciclica, giorno |
| 19-20 | `minutes_to_high`, `minutes_since_high` | calendario (troncati a 24 h) |
| 21-23 | `sent_1h`, `sent_4h`, `hawkish_diff` | sentiment (valuta lunga valuta corta del cross) |
| 24 | `risk_off` | sentiment risk-off |
| 25 | `vol_ratio` | σ prevista / σ media 30 giorni |
| 26 | `last_outcomes` | media degli ultimi 10 esiti |
| 27 | `buy_cross` | direzione |
I nomi sono in `LearningFeatures.Names`; il test `LeakTests` verifica che nessun nome contenga l'esito e che un'etichetta presa dal futuro non sia apprendibile (AUC ≈ 0,5).
## Livello 0 — Calibrazione
`CalibrationTables.Build` raggruppa i basket chiusi per basket, preset, terzile di volatilità, ora del giorno, bucket di |z| e di costo, e scrive win rate e P&L medio per bucket in `knowledge/calibration.csv` (colonna `motivazione` con il conteggio). Serve a leggere dove la strategia paga e dove no, e a niente altro: non cambia soglie.
## Livello 1 — Logistica online (il campione)
`OnlineLogistic`: pesi su 28 feature standardizzate con statistiche rolling (`RollingStandardizer`, emivita 200 righe), SGD con L2 = 10⁻³ e tasso 0,01/√(1+n/100). Predice a ogni chiusura di barra (`p_ML` nella dashboard, "in ombra") e impara a ogni chiusura di basket. Stato in `data/models/logreg_current.json`; versioni datate `logreg_vN.json` con `trained_on_until` e hash del dataset.
**Valutazione walk-forward** (`ModelEvaluator.EvaluateLogistic`): sequenziale, predici-poi-aggiorna, con i primi 30 basket di burn-in esclusi dalle metriche. Metriche: AUC con intervallo bootstrap (1000 ricampionamenti), Brier, log-loss, curva di calibrazione in 10 bin, P&L di tutti i basket contro P&L dei soli basket con p ≥ `mlMinProbability`, Sharpe e DSR del filtrato.
**Attivazione** (§8.3 della specifica), tutte insieme:
1. almeno **300** basket chiusi;
2. AUC walk-forward ≥ **0,55** con l'intervallo bootstrap che esclude 0,50;
3. P&L filtrato migliore del P&L non filtrato **e** DSR del filtrato ≥ **0,95**.
Quando è attivo, un ingresso con p < `mlMinProbability` (0,55) viene rifiutato (`ml_gate` nel ledger). **Disattivazione**: se l'AUC mobile sugli ultimi 100 basket scende sotto **0,52** il modello torna in ombra e lo scrive in `models_registry.csv`.
Stato del 2026-09-16: **0 basket chiusi nel ledger** → il modello è in ombra e non è valutabile. Nessuna cifra qui è un risultato.
## Livello 2 — MLP challenger
`SmallMlp`: 28 → 16 ReLU → 1 sigmoide, inizializzazione Glorot con seme fisso, Adam (β 0,9/0,999), L2 = 10⁻⁴, mini-batch 8-64. Addestrato dal ciclo settimanale in **5 fold cronologici con purga ed embargo di 24 ore** attorno al fold di test, **5 semi** mediati, early stopping sull'ultimo 20 % (cronologico) dei dati di addestramento con pazienza 20 epoche. Lo standardizzatore viene adattato all'intero insieme di addestramento prima del fit (le statistiche rolling partono da zero e distorcono le prime righe: scoperto e corretto con il test sul cerchio, vedi `ModelTests`).
Il **gradient check** (`SmallMlp.GradientCheck`, test `TheMlpGradientMatchesTheNumericalOne`) confronta il gradiente analitico di ogni peso vivo del primo strato con la differenza centrale numerica: scarto relativo < 10⁻⁴.
Promozione a campione: solo se batte la logistica di almeno 0,01 di AUC walk-forward **e** supera gli stessi cancelli di attivazione, e comunque solo dopo il forward test. Fino ad allora è registrato come `challenger` in `models_registry.csv`.
## Livello 3 — Bandit sui preset
`ThompsonBandit`: una Beta(α, β) per braccio = preset × terzile di volatilità prevista (3 × 3). A ogni chiusura il braccio usato riceve 1 se il basket è positivo. La proposta campiona dalle posteriori con un **tetto del 10 %** alle scelte esplorative (`ExplorationCap`; test `TheBanditKeepsExplorationUnderTheCap`). Dalla 5.0 la proposta viene scritta nel log e in `knowledge/proposals.csv` e **non viene applicata** in nessuna modalità (D-30); fino alla 4.0.0 in Paper e Demo cambiava il preset a caldo.
## Previsione della volatilità
`VolForecaster`: sui rendimenti a 15 minuti del cross calcola la varianza realizzata giornaliera e mantiene due previsori a 1-4 ore, **EWMA** (span 100) e **HAR-RV** (OLS sulle medie a 1, 5 e 22 giorni, rifittato ogni giorno). Ogni giorno confronta l'errore quadratico delle due previsioni sulla finestra mobile e usa quello migliore (`ActiveModel`). Il rapporto σ prevista / σ media 30 giorni scala la soglia `z_in` (`volScaleZIn`) e la size, ed è la feature `vol_ratio`.
## Deriva
L'indice di stabilità (PSI) previsto dalla specifica non è collegato a niente: il codice è stato rimosso nella pulizia della 5.0 (era una classe mai chiamata). Quando il meta-modello sarà attivabile, il controllo della deriva va reintrodotto insieme al criterio di ritorno in ombra, non prima.
## Ciclo settimanale e comando `learn`
`LearningState.RunCycle` — nel bot solo con `learning.weeklyCycle = true` (la domenica dopo le 10 UTC o al primo avvio dopo sette giorni); altrimenti a mano, dallo strumento o dall'interfaccia:
```powershell
dotnet run --project tools/Encelado.Backtest -- learn --data <cartella data del bot> [--knowledge <cartella>] [--strategy config/strategy.json]
dotnet msbuild build/Release.proj -t:Backtest -p:Dati="%USERPROFILE%\Documents\Encelado\data" -p:Comando=learn
```
`--data` è la cartella che contiene `ledger/` (una copia di `/data/data` del container va benissimo); `--knowledge` è dove scrivere (default: `knowledge` accanto ai dati). Lo strumento stampa quante righe ha il dataset e lo stato del modello dopo il ciclo. Il pulsante «Esegui ciclo di apprendimento» in Impostazioni ▸ Ricerca fa la stessa cosa sui dati del container. Il ciclo:
1. ricostruisce il dataset dal ledger;
2. valuta e riaddestra logistica (walk-forward) e MLP (fold purgati);
3. scrive `knowledge/calibration.csv`, `knowledge/insights_YYYYWW.md` (cosa ha funzionato, calibrazione, meta-modello, bandit, parametri suggeriti), `knowledge/models_registry.csv`, `knowledge/proposals.csv` (una riga per proposta, con evidenza e stato `proposta`);
4. salva i modelli con versione.
**Le proposte non toccano niente.** Il percorso per cambiare un parametro live è: proposta → `knowledge/preregistrazione.csv` (metrica, soglia, periodo, N minimo, scritti prima) → forward test in Paper/Demo → `forward_registry.csv` → decisione umana.
## Cosa è stato escluso, e perché
- **LSTM / Transformer / RL profondo**: con qualche centinaio di basket l'anno per cinque coppie, un modello con migliaia di parametri impara il rumore del campione; la regressione logistica e un MLP minuscolo sono già al limite di ciò che il dataset può sostenere. Il costo (settimane di lavoro e di calcolo) non è giustificato da nessun indizio che un modello più ricco troverebbe struttura dove i test di falsificazione non ne trovano.
- **Feature ricostruite a posteriori**: il ledger scrive ciò che il bot sapeva; ricostruire feature dopo è il modo più facile di introdurre look-ahead.
- **Ottimizzazione automatica dei parametri**: la griglia del backtest serve a *sapere*, non a *scegliere*; ogni prova conta nel DSR.
## File
| File | Contenuto |
|---|---|
| `data/models/logreg_current.json`, `mlp_current.json`, `bandit.json` | stato corrente (ripreso all'avvio) |
| `data/models/logreg_vN.json`, `mlp_vN.json` | versioni del ciclo settimanale con `trained_on_until`, righe, hash del dataset, nomi delle feature |
| `data/models/learning_state.json` | attivo/ombra, campione, versione, ultimi 200 (p, esito) per l'AUC mobile, feature dei basket aperti |
| `knowledge/calibration.csv`, `insights_YYYYWW.md`, `models_registry.csv`, `proposals.csv`, `forward_registry.csv`, `preregistrazione.csv` | vedi `docs/LEDGER_SCHEMA.md` |
Test: `tests/Encelado.Tests/LearningTests.cs` (i: gradient check, apprendimento walk-forward, MLP contro logistica su una regola non lineare, bandit, volatilità; j: leak; l: blocchi).