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>
11 KiB
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:
- almeno 300 basket chiusi;
- AUC walk-forward ≥ 0,55 con l'intervallo bootstrap che esclude 0,50;
- 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:
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:
- ricostruisce il dataset dal ledger;
- valuta e riaddestra logistica (walk-forward) e MLP (fold purgati);
- 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 statoproposta); - 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).