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>
118 lines
11 KiB
Markdown
118 lines
11 KiB
Markdown
# 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).
|