La galassia
ORBITA II · COSTELLAZIONE

Signal

Quant e research

Sistemi di ricerca che non mentono a se stessi: ingest read-only, classificazione del regime, Market Memory e calibrazione. La spina dorsale di XCAP.

Alimenta XCAP
6 moduli · 6 lezioni
MODULI DELLA COSTELLAZIONE Apri ogni modulo per la lezione completa
SG-01

Ingest read-only ed edge senza chiavi

lezione

Costruire un edge di ingestione read-only che porta dati pubblici del mondo su disco senza esporre una sola credenziale, e che è strutturalmente incapace di operare o spostare denaro.

Un sistema di research che tocca la rete dappertutto è un sistema che può far trapelare chiavi, eseguire trade per sbaglio e contaminare i risultati con dati non riproducibili. L'edge keyless è la frontiera dove il mondo entra senza poter fare danni.

LA LEZIONE

L'intuizione centrale di XCAP è brutale e poco intuitiva per chi viene dal SaaS: in un sistema di ricerca per il trading, la rete non è una comodità, è la superficie di attacco più ampia che hai. Ogni endpoint con chiave è una credenziale che può trapelare; ogni chiamata di scrittura è un trade che può scattare per via di un bug. Per questo XCAP collassa tutta la sua esposizione di rete in un unico punto: un edge di ingestione read-only, keyless e GET-only. Il resto del sistema è deterministico a partire dal disco. Se capisci questo, capisci perché `binance_public.py` chiede solo klines (candele OHLCV storiche) via HTTP GET, senza API key, senza firmare nulla, senza toccare mai un endpoint di account o di ordine.

In concreto, il pattern è questo: una sorgente astratta (in XCAP, una gerarchia `OHLCSource`) con due implementazioni. `LocalCSVSource` legge CSV già su disco — zero rete, il caso di default, quello che usano TUTTI i loop automatici. E `HttpOHLCSource` (basato su `binance_public.py`) che si attiva solo quando un caller passa esplicitamente `allow_network=True` o invoca il comando `data-fetch`. L'asimmetria è deliberata: la via sicura è il default e la via con rete pretende un'opzione esplicita a ogni chiamata. Mai un flag globale ambientale, mai una variabile d'ambiente che resta accesa.

L'endpoint di Binance che usi è quello pubblico di mercato: `GET https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1d&limit=1000`. Restituisce un array di array: `[openTime, open, high, low, close, volume, closeTime, ...]`. Fai caso a ciò che qui NON c'è: non c'è l'header `X-MBX-APIKEY`, non c'è il query param di firma HMAC, non c'è un timestamp firmato. Quell'endpoint fisicamente non può leggere il tuo saldo né piazzare un ordine, perché l'API di account di Binance vive su un altro percorso e pretende la firma. La sicurezza non è una tua promessa di comportarti bene: è una proprietà dell'endpoint che hai scelto.

Il secondo pilastro è la normalizzazione sul bordo. I dati del mondo reale arrivano sporchi: timestamp in millisecondi, numeri come stringhe, candele duplicate, buchi. Il tuo edge deve normalizzare in UN formato canonico prima che qualunque cosa a valle li tocchi. In XCAP i CSV di `data/real` sono lowercase (`open,high,low,close,volume`), i timestamp parsati a date, e `LocalCSVSource` li legge sempre allo stesso modo. È questa disciplina a rendere il sistema riproducibile: il classificatore di regime (SG-02) e la Market Memory (SG-03) leggono da disco un formato stabile, non una risposta HTTP volatile che cambia tra un'esecuzione e l'altra.

La verifica è la chiusura del cerchio. XCAP ha un comando `check` (si esegue con `PYTHONPATH=src python3 -m xcap.control check`) che afferma come invariante che il connector è keyless, read-only, non può operare e non può spostare denaro. Non è un test che passa una volta: è un'asserzione che gira a ogni `check`, così che se qualcuno aggiunge una chiave o un metodo di scrittura, il `check` diventa rosso. È così che trasformi un'intenzione di design in una proprietà che il sistema difende da solo. La regola d'oro: la sicurezza che dipende dal fatto che tu ti ricordi qualcosa non è sicurezza; la sicurezza che un test rosso ti obbliga a rispettare, sì.

Una sfumatura operativa importante: il dashboard di XCAP fa bind solo su `127.0.0.1`, mai su `0.0.0.0`. L'edge di ingestione è l'unica superficie in uscita; quella in entrata (il dashboard) non ascolta sulla rete. E FileVault (AES-256) cifra il disco a riposo. La catena completa — disco cifrato, ingest keyless read-only, dashboard local-only, Capital Gate chiuso — è ciò che ti permette di avere un sistema di trading-research su un portatile senza che sia una bomba a orologeria.

ESERCIZIO

Implementa una sorgente di dati con due backend. Crea `ohlc_source.py` con una classe base `OHLCSource` (metodo `get(symbol, interval, limit) -> list[Bar]`). Implementa `LocalCSVSource` (legge da `./data/{symbol}.csv`, senza rete) e `HttpOHLCSource` (GET a `api.binance.com/api/v3/klines`, senza nessuna API key). Il costruttore di `HttpOHLCSource` deve ricevere `allow_network: bool = False` e lanciare `RuntimeError('network not allowed')` se si chiama `get()` con `allow_network=False`. Normalizza entrambi nella stessa struttura `Bar(time, open, high, low, close, volume)` con float e data parsata. Scrivi `check_connector()` che verifichi per introspezione che la classe HTTP non ha nessun metodo né attributo il cui nome contenga 'order', 'trade', 'sign', 'key', 'secret' o 'withdraw', e che fallisca se lo trova.

CONSEGNA

`ohlc_source.py` con `LocalCSVSource` + `HttpOHLCSource(allow_network=False)` di default, normalizzazione in `Bar` canonico, e una funzione `check_connector()` che afferma keyless/read-only per introspezione e restituisce un report PASS/FAIL.

INTUIZIONE CHIAVE

La via sicura deve essere il default e la via pericolosa deve pretendere un'opzione esplicita a ogni chiamata — mai un flag globale. Un `allow_network=True` passato come argomento a ogni uso è sicuro; una variabile d'ambiente `ALLOW_NETWORK=1` che resta accesa è una falla che aspetta solo di succedere.

ERRORI DA EVITARE

  • ×Usare un endpoint che richiede API key 'perché dà più dati' — nel momento in cui firmi una richiesta, il tuo edge non è più strutturalmente incapace di operare, e tutta la garanzia crolla.
  • ×Mettere il controllo della rete in una variabile d'ambiente globale invece che in un argomento esplicito per chiamata; resta accesa tra un'esecuzione e l'altra e i loop automatici ereditano la rete senza volerlo.
  • ×Non normalizzare sul bordo: lasciare che timestamp in ms e numeri come stringa si propaghino a valle, rompendo la riproducibilità tra la sorgente locale e quella HTTP.
  • ×Fare bind del dashboard su `0.0.0.0` per 'vederlo dal cellulare' — esponi alla rete tutta la tua ricerca; usa `127.0.0.1` e un tunnel SSH se davvero ne hai bisogno.
  • ×Verificare la proprietà keyless una sola volta a mano invece di codificarla come asserzione in un `check` che gira sempre; la garanzia si erode al primo refactor che nessuno controlla.
SG-02

Classificatore di regime di prezzo

lezione

Costruire un classificatore di regime di prezzo puro e deterministico che etichetta lo stato del mercato (tendenza, volatilità, comportamento, stress) senza mai dimensionare né operare una posizione.

Decidere senza sapere in quale regime ti trovi è operare alla cieca: una strategia che vince in tendenza si dissangua in laterale. L'etichetta di regime è la condizione preliminare onesta di qualunque decisione, e separarla dall'esecuzione evita che il bias del 'voler operare' contamini la diagnosi.

LA LEZIONE

L'errore concettuale che XCAP ha corretto nel suo audit di giugno 2026 è sottile e vale oro: confondere salute operativa con regime di prezzo. `market_state.py` misura la salute del sistema (ho dati freschi? i flag sono a posto?). `regime.py` misura qualcosa di completamente diverso: in quale STATO si trova il mercato stesso? Sono assi ortogonali, e mescolarli produce un classificatore che non sa cosa sta dicendo. La prima disciplina è questa: un classificatore di regime parla solo del prezzo, mai dello stato della tua infrastruttura.

Il design di XCAP classifica lungo quattro assi indipendenti. Trend (sale, scende, laterale? — via pendenza delle medie o rendimento cumulato su una finestra). Volatility (calmo o agitato? — via deviazione standard dei rendimenti, normalizzata). Behavior (persistente/tendenziale o mean-reverting? — via autocorrelazione dei rendimenti). E Stress (c'è un drawdown in sviluppo?). Ogni asse si calcola in modo puro: stessa entrata, stessa uscita, senza stato nascosto, senza casualità. Poi una cascata di priorità documentata deriva un `Regime` titolare unico a partire dai quattro assi.

L'insight più tagliente di XCAP sta in come definisce il Panic, ed è proprio lì che quasi tutti sbagliano. La tentazione è definire il panico come 'volatilità alta' (ratio di stdev elevato). È sbagliato. Un crash monotono — il prezzo che cade quasi in linea retta — ha volatilità INTERNA bassa, perché ogni candela assomiglia alla precedente. Se definisci il panico per stdev, ti perdi proprio il crash più pericoloso. La definizione corretta di XCAP: il panico è un drawdown grande in sviluppo VELOCE, misurato da un rendimento recente molto negativo e brusco, non dalla dispersione. È questo che distingue un classificatore pensato da chi ha guardato crash reali da uno che ha copiato una formula da un libro.

La purezza non è un capriccio estetico, è la base della fiducia. `regime.py` è una funzione pura: `classify(bars) -> Regime`. Non legge da disco al suo interno, non chiama la rete, non muta nulla di globale, non ha rami casuali. Questo significa che puoi eseguirla mille volte sulla stessa finestra e ottenere la stessa etichetta, puoi testarla con fixture deterministiche, e puoi verificare esattamente perché ha detto 'Panic' in una data precisa. Un classificatore con stato nascosto è un classificatore di cui non puoi fidarti per costruirci sopra la Market Memory.

La regola più importante, e quella che collega tutta la costellazione: il classificatore è label-only. NON dimensiona né opera MAI. Restituisce un'etichetta e nient'altro. La ragione è di architettura dell'onestà: se lo stesso modulo che diagnostica il regime decide anche la dimensione della posizione, hai un incentivo a far sì che la diagnosi giustifichi l'operazione che già volevi fare. Separare la diagnosi dall'azione è ciò che permette, in SG-03, di misurare onestamente se il regime ha valore predittivo — perché l'etichetta è stata generata senza sapere cosa ne avresti fatto.

Per implementarlo bene, documenta la cascata di priorità come codice leggibile, non come un groviglio di if. Per esempio: se Stress scatena Panic, vince Panic su qualsiasi altra cosa; altrimenti, se Volatility è alta con Trend forte, è Trending-Volatile; se Behavior è mean-reverting e Trend è laterale, è Range; ecc. Ogni ramo deve avere un commento che spiega il perché di mercato, non solo la soglia. Quel commento è ciò di cui un revisore — o tu stesso fra sei mesi — ha bisogno per fidarsi dell'etichetta.

ESERCIZIO

Scrivi `regime.py` con una funzione pura `classify(bars: list[Bar]) -> Regime`. Calcola quattro sotto-segnali: `trend` (segno e magnitudine del rendimento sulla finestra), `vol` (stdev dei rendimenti giornalieri), `behavior` (autocorrelazione lag-1 dei rendimenti), `stress` (drawdown massimo recente E velocità = rendimento degli ultimi N giorni). Implementa la cascata di priorità dove Panic = drawdown grande + rendimento recente fortemente negativo (NON stdev alto). Restituisci un `Regime` con il label titolare e i quattro assi esposti. Prova con tre fixture sintetiche: una tendenza rialzista pulita, un crash monotono (deve dare Panic nonostante la vol interna bassa), e un laterale rumoroso. Verifica che la funzione è pura eseguendo `classify` due volte sulla stessa entrata e affermando l'uguaglianza.

CONSEGNA

`regime.py` con `classify(bars) -> Regime` puro e deterministico, quattro assi (trend/vol/behavior/stress), cascata di priorità documentata, e tre test di fixture dove il crash monotono viene classificato correttamente come Panic.

INTUIZIONE CHIAVE

Il panico non è volatilità alta — un crash monotono ha volatilità interna bassa perché ogni candela imita la precedente. Definisci il panico per la velocità del drawdown (rendimento recente brusco), non per la dispersione, o ti perderai proprio il regime più pericoloso.

ERRORI DA EVITARE

  • ×Mescolare regime di prezzo e salute operativa nello stesso modulo; sono assi ortogonali e unirli produce etichette che non significano nulla di concreto.
  • ×Definire il Panic per ratio di stdev alto: ti perdi i crash monotoni, che sono proprio quelli che contano.
  • ×Mettere un ramo casuale o una lettura da disco dentro al classificatore, rompendone la purezza e rendendo impossibile verificare perché ha etichettato una data.
  • ×Lasciare che il classificatore dimensioni o suggerisca operazioni; nel momento in cui diagnosi e azione condividono il modulo, la diagnosi inizia a giustificare l'operazione che già volevi.
  • ×Nascondere la cascata di priorità in if senza commentare il perché di mercato; nessuno potrà fidarsi né rivedere la soglia fra sei mesi.
SG-03

Market Memory: forecast + calibrazione

lezione

Costruire la Market Memory: un ledger che blocca una previsione falsificabile PRIMA dell'esito, la risolve contro il rendimento realizzato, e ne valuta la calibrazione (hit-rate, Brier, calibrazione per confidenza e per regime).

Senza un registro di previsioni bloccate prima del risultato, un sistema di research non ha modo onesto di sapere se sa qualcosa. È la differenza tra 'credo che funzioni' e 'ecco il mio storico out-of-sample valutato'. È l'unica fonte reale di evidenza di edge.

LA LEZIONE

La scoperta centrale dell'audit di XCAP è stata devastante: ogni modulo precedente era o computazione stateless o un accumulatore passivo; NULLA registrava una previsione falsificabile prima dell'esito per poi valutarne la propria calibrazione. Quel loop assente è esattamente ciò che rende un sistema di research difficile da ingannare, e l'unica fonte onesta di evidenza di edge. La Market Memory (`market_memory.py`) è quel loop. Se non costruisci questo modulo, tutto il resto è teatro: calcoli graziosi che non si confrontano mai con la realtà.

Il cuore è `advance_market_memory(universe, state_dir, horizon_days=5, ...)` e la sua sequenza è sacra in quest'ordine: osservare date nuove → classificare il regime (usando il `regime.py` di SG-02) → registrare un forecast BLOCCATO (un baseline derivato dal regime, con la sua probabilità e il suo orizzonte) → risolvere i forecast già maturati contro il rendimento forward realizzato → valutare. Il blocco è la parola chiave: una volta che registri 'alla data T prevedo prob_up=0.62 a 5 giorni', quel record è immutabile. Non puoi tornare a toccarlo quando vedi il risultato. È quell'immutabilità a trasformare il ledger in evidenza e non in una razionalizzazione a posteriori.

Le metriche non sono una sola; sono un cruscotto onesto. Hit-rate (quale frazione di previsioni direzionali ha azzeccato?). Brier score (quanto erano ben calibrate le probabilità? — penalizza tanto l'eccesso di confidenza quanto il difetto). Calibrazione per bucket di confidenza (quando hai detto 70%, hai azzeccato il 70% delle volte?). E il gioiello: hit-rate PER REGIME. Quest'ultima è quella che davvero informa: ti dice se la tua previsione ha valore in tendenza ma è spazzatura in laterale, o se azzecchi solo in calma. Un hit-rate globale del 55% può nascondere il 70% in tendenza e il 40% in panico — ed è quella scomposizione a dire dove, se c'è, esiste edge.

La proprietà anti-fabbricazione è per costruzione (la approfondiamo in SG-04, ma qui è strutturale): si risolvono solo i forecast il cui orizzonte è già maturato con dati che esistevano DOPO la data del forecast. È out-of-sample by design. Non puoi mai 'prevedere' una data il cui risultato già conosci, perché il forecast si è bloccato a T e si risolve solo quando arriva T+horizon con dati reali. Lo state dir è gitignored — lo storico non si versiona, così non c'è la tentazione di editarlo a mano e truccare i risultati.

Le tre proprietà di integrità che XCAP impone, e che devi replicare: accumula (il ledger cresce tra un'esecuzione e l'altra, non si resetta mai), idempotente (eseguire il tick due volte sulle stesse date non duplica i forecast né li risolve due volte), e Gate-chiuso (non opera mai). È la stessa famiglia di invarianti del capital invariant di SG-05: lo stato del sistema è la somma di eventi reali, mai un replay né un baseline fresco. Il CLI `market-memory-tick` orchestra tutto questo e legge CSV in formato `data/real` (lowercase) via `LocalCSVSource` — zero rete, tutto deterministico da disco.

Un punto che separa l'ingegnere dal dilettante: il baseline che registri deve essere onesto e modesto. XCAP registra un 'regime_baseline' — una probabilità derivata dal regime, non un modello elaborato già ottimizzato guardando il passato. Perché partire umili? Perché il baseline è il tuo metro di misura. Se un forecaster sofisticato non batte il baseline di regime out-of-sample, non hai edge, hai overfitting. La Market Memory esiste per confutare le tue idee, non per confermarle. E un buon registro di forecast è evidenza, mai prova — per questo il Capital Gate resta chiuso per quanto bene possa apparire l'hit-rate.

ESERCIZIO

Costruisci `market_memory.py` con un ledger in JSON-lines append-only. Implementa `advance(universe, state_dir, horizon_days=5)` che: (1) legga le date nuove, (2) per ogni data classifichi il regime con il tuo `regime.py`, (3) registri un forecast bloccato `{date, symbol, regime, prob_up, horizon, resolved: false}`, (4) risolva i forecast maturi (dove esiste la barra a date+horizon) calcolando il rendimento forward reale e segnando `hit`, (5) scriva le metriche: hit-rate globale, Brier, e una scomposizione di hit-rate PER regime. Rendi il tick idempotente: se esiste già un forecast per (date, symbol), non duplicarlo; se è già risolto, non ri-risolverlo. Dimostra l'accumulazione eseguendo il tick su due intervalli di date consecutivi e mostrando che il ledger cresce senza resettarsi.

CONSEGNA

`market_memory.py` + ledger append-only in `state/` (gitignored) con forecast bloccati prima dell'esito, risoluzione out-of-sample, metriche hit-rate/Brier/calibrazione-per-regime, e un CLI `market-memory-tick` idempotente e accumulativo.

INTUIZIONE CHIAVE

La metrica che davvero informa è l'hit-rate PER REGIME, non quello globale. Un 55% globale può nascondere il 70% in tendenza e il 40% in panico — ed è la sola scomposizione che ti dice dove, se c'è, esiste edge reale da sfruttare.

ERRORI DA EVITARE

  • ×Registrare la previsione e risolverla nella stessa esecuzione guardando già il risultato; quello non è un forecast, è un aggiustamento a posteriori, e distrugge ogni evidenza di edge.
  • ×Permettere che il ledger si resetti tra un'esecuzione e l'altra; perdi lo storico che è proprio l'asset, e rompi la proprietà di accumulazione.
  • ×Rendere il tick non idempotente: eseguirlo due volte duplica i forecast o li ri-risolve, gonfiando o corrompendo le metriche.
  • ×Riportare solo l'hit-rate globale e nascondere la scomposizione per regime, che è dove vive l'informazione azionabile.
  • ×Versionare lo state dir in git, aprendo la porta a editare lo storico a mano e truccare la calibrazione; tienilo gitignored.
SG-04

Anti-fabbricazione per costruzione

lezione

Progettare il sistema perché sia strutturalmente incapace di inventarsi dati o edge: null control, significatività con correzione per confronti multipli, valutazione out-of-sample e un edge_demonstrated che è sempre False di default.

Il modo più costoso di mentirsi nel research è produrre numeri che sembrano edge ma sono rumore, deriva o data-snooping. L'anti-fabbricazione non è una revisione finale: è una proprietà architetturale che rende impossibile a un forecaster cantare vittoria senza aver battuto il caso E la deriva, con significatività.

LA LEZIONE

La domanda che definisce questo modulo: come costruisci un sistema che NON POSSA mentirti sul fatto di avere edge? La risposta di XCAP non è 'essere prudenti'. È introdurre avversari strutturali — null control — contro cui ogni affermazione di edge deve competere. L'Hypothesis Lab (`hypothesis_lab.py`) ha un registro di forecaster reali (regime_baseline, trend_follow, momentum_follow, mean_revert) E due controlli nulli deliberati: `always_up` (il null della DERIVA — il mercato sale in media, il tuo forecaster sta solo catturando quello?) e `coin_flip` (il null del CASO — seminato e deterministico, batti il lancio di una moneta?). Un forecaster che non batte ENTRAMBI i null non ha edge, ha un'illusione.

Il secondo pilastro è la significatività con correzione per confronti multipli. Se provi venti forecaster, qualcuno sembrerà buono per puro caso — è il problema del data-snooping. XCAP lo neutralizza con una soglia z corretta con Bonferroni che SCALA con K (il numero di forecaster valutati): più ipotesi provi, più alta è l'asticella che ciascuna deve superare. `evaluate_forecasters(universe, names, horizon_days, min_samples, alpha)` confronta ogni forecaster reale contro entrambi i null (Δcaso e Δderiva) e gli concede `candidate_edge` solo se batte il caso E la deriva E supera la soglia corretta E ha almeno `min_samples` osservazioni. Le quattro condizioni, congiunte. Toglierne una qualsiasi apre una porta alla fabbricazione.

Il dettaglio che dimostra che il sistema è onesto e non truccato: `edge_demonstrated` è SEMPRE False. Il lab può concedere `candidate_edge` — 'questo merita più ricerca' — ma non dichiara mai edge dimostrato, perché nessun backtest, per quanto buono, prova l'edge futuro. È una decisione di design che ti protegge dal tuo stesso ottimismo: il sistema letteralmente non ha un percorso di codice che dica 'sì, hai edge, opera'. Il Capital Gate resta chiuso per costruzione, non per la tua disciplina.

La verifica che lo scoreboard non sia truccato è elegante e la devi replicare: XCAP ha controllato che su tendenze sintetiche, `trend_follow` vince alla grande (rileva il segnale reale), `mean_revert` finisce significativamente NEGATIVO (rileva un anti-edge — scommettere contro la tendenza perde), e `coin_flip` atterra intorno al ~50% (il caso non viene premiato). Se il tuo coin_flip uscisse con un hit-rate del 60%, la tua pipeline è rotta o hai leakage. I null non sono solo avversari: sono sonde di calibrazione del tuo stesso sistema di misura. Un coin_flip che non dà ~50% è un sistema che si sta mentendo, e devi fermare tutto finché non capisci perché.

Il fondamento condiviso evita il trucco di spostare il bersaglio: `scoring.py` è l'unica fonte di verità per le primitive (prob_up, is_hit, brier, conf_bucket, hit_rate_z). Sia la Market Memory (SG-03) sia l'Hypothesis Lab valutano con LO STESSO codice. Se avessi due implementazioni di 'hit', potresti — consapevolmente o no — usare la più generosa per il tuo forecaster preferito. Una sola primitiva condivisa chiude quella porta. Questa è anti-fabbricazione per costruzione: non puoi scegliere la regola che ti conviene perché c'è una sola regola.

Ogni valutazione è out-of-sample per la stessa meccanica di SG-03: ogni barra si classifica una sola volta, il forecast si genera senza vedere il futuro, e si risolve contro il rendimento forward reale. Il doc `docs/HYPOTHESIS_LAB.md` e i 4 invarianti del `check` codificano queste garanzie. La lezione di fondo, che è la tesi di tutta la costellazione Signal: il vincolo stringente per operare capitale reale è edge DIMOSTRATO, e questi strumenti misurano l'edge onestamente (null control + significatività) invece di affermarlo. Un sistema che può emettere solo `candidate_edge` e mai `edge_demonstrated` è un sistema che rispetta ciò che non sa.

ESERCIZIO

Estendi il tuo Hypothesis Lab con `forecasters.py` (registro con almeno `trend_follow`, `mean_revert` e due null: `always_up` per la deriva e `coin_flip` seminato per il caso) e `evaluate_forecasters(names, horizon_days, min_samples, alpha)`. Per ogni forecaster reale calcola il suo hit-rate out-of-sample e il delta contro ENTRAMBI i null. Implementa una soglia z con correzione Bonferroni che scali con K = numero di forecaster. Concedi `candidate_edge=True` solo se batte il caso E la deriva E supera la soglia E ha >= min_samples. Fissa `edge_demonstrated=False` sempre. Valida la tua pipeline su una serie sintetica con tendenza: `trend_follow` deve uscire significativamente positivo, `mean_revert` significativamente negativo, e `coin_flip` deve atterrare intorno al ~50% (se no, la tua misura ha leakage).

CONSEGNA

`forecasters.py` + `hypothesis_lab.py` con due null control (caso e deriva), soglia z Bonferroni che scala con K, gate di quattro condizioni per `candidate_edge`, `edge_demonstrated` sempre False, e una validazione dove coin_flip esce ~50% provando che lo scoreboard non è truccato.

INTUIZIONE CHIAVE

I null control non sono solo avversari contro cui competi — sono sonde di calibrazione del tuo stesso sistema di misura. Se il tuo coin_flip non esce ~50%, hai leakage o un bug, e devi fermare TUTTO finché non capisci perché, prima di credere a qualunque altro numero.

ERRORI DA EVITARE

  • ×Confrontare il forecaster solo contro il caso e dimenticare la deriva; molti 'edge' sono solo il bias rialzista di fondo del mercato travestito.
  • ×Non correggere per confronti multipli: prova 20 forecaster e uno sembrerà significativo per puro caso (data-snooping).
  • ×Permettere un percorso di codice che dichiari `edge_demonstrated=True`; nessun backtest prova l'edge futuro, e quel flag invita ad aprire il Capital Gate prematuramente.
  • ×Avere due implementazioni di 'hit' o 'brier' invece di una sola primitiva condivisa; ti lascia scegliere la regola più generosa per il tuo forecaster preferito.
  • ×Ignorare che il coin_flip è uscito 60%: quella non è fortuna, è leakage nella tua pipeline, e qualunque altro risultato del lab è spazzatura finché non lo sistemi.
SG-05

Capital invariant

lezione

Implementare il capital invariant: un ledger dove il capitale accumula tra le sessioni, si muove solo su chiusure REALI (P&L realizzato), e non si resetta mai — così che il numero a schermo rifletta sempre risultati chiusi, non replay né guadagni di carta.

Un saldo che si gonfia con P&L non realizzato o che si resetta tra le esecuzioni è un saldo che mente. L'integrità del capitale è la condizione senza la quale nessun altro numero del sistema merita fiducia: se la contabilità può mentire, anche tutto il resto può.

LA LEZIONE

Questo modulo nasce da un fallimento reale e doloroso in XCAP, ed è per questo che insegna così bene. Un autorun programmato stava silenziosamente RESETTANDO il ledger al saldo iniziale di 500 e riproducendo la stessa identica fixture ogni 5 minuti. Il sistema mostrava attività, numeri che cambiavano, sembrava vivo — ed era completamente falso. Tre regole venivano violate insieme, e sono le tre che definiscono il capital invariant. Memorizza queste regole perché sono il nucleo: accumula, realized-only, e la re-entry continua le sessioni precedenti.

Regola 1 — Accumula. Il capitale NON si resetta tra esecuzioni né sessioni. Se hai chiuso la sessione di ieri con 547.30, oggi parti da 547.30. Un baseline fresco a ogni avvio non è contabilità, è una demo travestita. Il ledger si carica dal disco all'avvio e si persiste alla chiusura; lo stato vive tra un'esecuzione e l'altra.

Regola 2 — Realized-only. Il saldo si muove solo quando una posizione viene CHIUSA/venduta. Il P&L non realizzato (mark-to-market, il valore fluttuante di una posizione aperta) non gonfia mai il saldo. L'invariante esatta, che devi poter affermare in qualunque momento: `current_balance == starting_balance + realized_pnl`. Una posizione aperta con +200 di guadagno fluttuante NON cambia il saldo; lo fa solo quando la chiudi e quei +200 diventano realizzati. È così che eviti la trappola psicologica più antica del trading: contare i guadagni prima di intascarli.

Regola 3 — La re-entry continua. Al riavvio, il capitale è il risultato accumulato delle operazioni delle sessioni precedenti, non una nuova linea di base. Il sistema deve poter essere spento e riacceso mille volte e il capitale essere sempre la somma onesta di tutte le chiusure reali avvenute. È la stessa cosa della proprietà di accumulazione della Market Memory (SG-03): lo stato è la somma di eventi reali, una sola direzione, senza replay.

La lezione di architettura è la distinzione tra due comandi che XCAP tiene deliberatamente separati. L'autorun programmato esegue `sim-autopilot-tick`: carica il ledger esistente, ruota regimi sintetici, e ACCUMULA su ciò che c'era prima. Esiste anche `sim-run-agent`, che resetta e riproduce una fixture — ma quello si conserva SOLO per demo deterministiche, e NON deve MAI essere ciò che esegue l'autorun. Il bug era proprio puntare lo scheduler al comando sbagliato. L'insegnamento: un comando che resetta e un comando che accumula non devono mai essere confondibili; nominali in modo che quello pericoloso urli ciò che fa.

Come si blinda tutto questo perché non si rompa di nuovo: l'invariante è LOCKED da `tests/test_sim_autopilot_tick.py`. Se tocchi il ledger o l'autorun, quei test devono restare verdi. Non è documentazione, è un lucchetto eseguibile. La regola operativa che l'utente ha imposto con enfasi: se tocchi la contabilità, esegui quel test, e se diventa rosso, hai rotto l'integrità del capitale e ti fermi. È la stessa filosofia del Capital Gate e del `check` di SG-01: le invarianti che davvero contano si codificano come test che si rifiutano di lasciartele rompere. La contabilità di XCAP non è affidabile perché l'autore è prudente; è affidabile perché un test rosso gli impedisce di essere distratto.

ESERCIZIO

Implementa `ledger.py` con un saldo persistito in `state/ledger.json`. Funzioni: `load()` (legge il saldo accumulato, o lo starting_balance solo al primissimo avvio), `open_position(symbol, entry, size)`, `mark(symbol, price)` (calcola il P&L NON realizzato ma NON tocca il saldo), e `close_position(symbol, exit)` (realizza il P&L e SOLO allora aggiorna il saldo, poi persiste). Mantieni l'invariante `current_balance == starting_balance + sum(realized_pnl)` e affermala dopo ogni operazione. Scrivi `test_capital_invariant.py` che: (1) apra una posizione vincente, chiami `mark` con un prezzo superiore, e verifichi che il saldo NON è cambiato; (2) la chiuda e verifichi che ora SÌ è salito esattamente del P&L realizzato; (3) simuli un riavvio (load dopo aver persistito) e verifichi che il saldo continua, non si resetta.

CONSEGNA

`ledger.py` con saldo accumulativo persistito + `test_capital_invariant.py` che prova le tre regole: mark non muove il saldo, close sì lo muove del P&L realizzato esatto, e un riavvio continua l'accumulato invece di resettare.

INTUIZIONE CHIAVE

L'invariante `current_balance == starting_balance + realized_pnl` è una sola riga che, affermata dopo ogni operazione, rende strutturalmente impossibile che il P&L di carta o un replay gonfino il tuo saldo. Quando la contabilità è un'equazione verificabile e non una credenza, smette di poterti mentire.

ERRORI DA EVITARE

  • ×Resettare il ledger allo starting_balance a ogni avvio dell'autorun — il bug originale di XCAP; mostra attività falsa e cancella tutto lo storico reale.
  • ×Lasciare che il P&L non realizzato (mark-to-market) tocchi il saldo; conti guadagni che non hai ancora intascato e il numero a schermo mente.
  • ×Confondere il comando che accumula (`sim-autopilot-tick`) con quello che resetta-e-riproduce (`sim-run-agent`); nomina quello pericoloso così che sia impossibile puntargli lo scheduler per errore.
  • ×Toccare il ledger o l'autorun senza eseguire poi `test_capital_invariant.py`; l'invariante si erode al primo refactor che nessuno ha verificato.
  • ×Trattare l'invariante come un commento nel codice invece che come un'asserzione eseguita dopo ogni operazione; un commento non ti impedisce di romperla, un assert sì.
SG-06

Autopilot tick e loop onesti

lezione

Costruire loop di autopilot che accumulano apprendimento reale tra un tick e l'altro — deterministici da disco, idempotenti, Gate-chiusi — invece di generare rumore o attività falsa.

Un loop autonomo mal progettato è pericoloso: gira da solo, senza nessuno che guarda, e può resettare lo stato, duplicare record o innescare azioni reali. La differenza tra un loop che impara e uno che si limita a muoversi è tutta la differenza tra research e teatro automatizzato.

LA LEZIONE

Un tick di autopilot è codice che gira senza un umano che guarda, ed è esattamente la ragione per cui deve essere il codice PIÙ disciplinato del sistema. L'errore che XCAP ha subito sulla propria pelle insegna tutto: un tick programmato che ogni 5 minuti resettava il ledger e replicava una fixture identica. Visto da fuori, il sistema sembrava vivo — numeri in movimento, attività costante. Dentro era una menzogna in loop. La domanda che definisce un buon tick: questo loop, eseguito mille volte, ACCUMULA qualcosa di vero, o genera solo l'apparenza di attività?

Proprietà 1 — Deterministico da disco. Il tick legge il suo input da disco (CSV `data/real` via `LocalCSVSource`), non dalla rete. I loop automatici di XCAP NON abilitano MAI la rete — `allow_network` resta False, sempre. È diretta continuità di SG-01: la rete è opt-in esplicito per chiamata umana (il comando `data-fetch`), mai qualcosa che un loop autonomo attivi da solo. Un tick che tocca la rete senza supervisione è un tick che può far trapelare, fallire in modo non riproducibile o restare appeso ad aspettare un socket. Deterministico da disco significa che lo stesso stato su disco produce lo stesso risultato, audace e riproducibile.

Proprietà 2 — Idempotente. Eseguire il tick due volte sulle stesse date non deve duplicare i forecast, né ri-risolvere ciò che è già risolto, né contare due volte una chiusura. Conta moltissimo nei loop perché gli scheduler falliscono, ritentano, si sovrappongono. Se il tuo tick non è idempotente, un retry corrompe lo stato in silenzio. Come ottenerlo: ogni evento (un forecast, una chiusura) ha una chiave naturale (data+simbolo) e il tick controlla l'esistenza prima di scrivere. `advance_market_memory` e `sim-autopilot-tick` sono idempotenti per questo motivo.

Proprietà 3 — Accumula, non resetta. Il tick carica lo stato precedente e costruisce SU di esso. `sim-autopilot-tick` carica il ledger, ruota regimi sintetici, e accumula — esattamente la regola del capital invariant di SG-05 e della Market Memory di SG-03. È la stessa invariante che compare per la terza volta nella costellazione, e non è un caso: l'onestà di un sistema autonomo È il suo rifiuto di resettarsi. Un loop che riparte da zero ogni volta non impara nulla; un loop che accumula è l'unica cosa che può costruire uno storico di calibrazione che valga.

Proprietà 4 — Gate-chiuso per costruzione. Il tick NON opera MAI capitale reale. Il Capital Gate (`capital/broker/live_trading/automation` tutti su False) è verificato da `python3 -m xcap.control check`. Un autopilot è proprio il punto in cui un sistema di trading uccide il suo proprietario: gira da solo, e se avesse la capacità di operare in reale, un bug alle 3 di notte svuota il conto. Per questo in XCAP l'autopilot è sim-only, senza eccezioni, e `check` lo afferma a ogni esecuzione. L'automazione e la capacità di operare in reale sono due cose che NON devono MAI coesistere senza una decisione umana ultra-deliberata.

Come si opera e si blinda tutto questo: il tick gira via lo scheduler (in XCAP, un task programmato che invoca `sim-autopilot-tick`, il comando che accumula — MAI `sim-run-agent`, quello che resetta per le demo). Il comportamento resta LOCKED da `tests/test_sim_autopilot_tick.py`: se tocchi l'autorun, quei test restano verdi o hai rotto l'integrità del loop. E ogni esecuzione termina con un `check` che riafferma gli invarianti (keyless, Gate-chiuso, accumulazione). Il principio finale, che sigilla l'intera costellazione: un loop onesto è uno le cui garanzie non dipendono dal fatto che qualcuno lo sorvegli, perché sono codificate come test e invarianti che il loop stesso verifica a ogni giro. Costruisci loop che si rifiutano di mentire anche quando nessuno guarda — è l'unica automazione di cui un sistema di research può fidarsi.

ESERCIZIO

Costruisci `autopilot_tick.py` con una funzione `tick(state_dir)` che a ogni chiamata: (1) carichi lo stato precedente da disco (ledger + ledger dei forecast), (2) legga le barre nuove via `LocalCSVSource` con `allow_network=False`, (3) faccia avanzare la Market Memory (blocca i forecast nuovi, risolve i maturi) accumulando, (4) esegua `check()` affermando Gate-chiuso e keyless, (5) persista. Rendilo idempotente: esegui `tick` due volte di fila sullo stesso stato e verifica che la seconda non aggiunga né cambi nulla. Scrivi `test_autopilot_tick.py` che provi: idempotenza (doppio tick = stato identico), accumulazione (due tick su intervalli diversi fanno crescere lo stato), e che `check` fallisca se qualcuno mette `allow_network=True` o apre il Gate. Programma il tick con un task ricorrente che invochi SOLO il comando che accumula.

CONSEGNA

`autopilot_tick.py` (deterministico da disco, idempotente, accumulativo, Gate-chiuso) + `test_autopilot_tick.py` che blocca idempotenza e accumulazione, e uno scheduler che punta al comando che accumula — mai a quello che resetta.

INTUIZIONE CHIAVE

L'invariante di accumulazione (non resettare) compare tre volte in Signal — capitale, Market Memory e autopilot — e non è un caso: l'onestà di un sistema autonomo È il suo rifiuto di resettarsi. Un loop che riparte da zero a ogni giro non impara; simula soltanto di essere vivo.

ERRORI DA EVITARE

  • ×Puntare lo scheduler al comando che resetta-e-riproduce (`sim-run-agent`) invece di quello che accumula (`sim-autopilot-tick`); il bug esatto che falsificava l'attività di XCAP.
  • ×Lasciare che il loop abiliti la rete (`allow_network=True`); un tick autonomo che tocca la rete può far trapelare, restare appeso o fallire in modo non riproducibile senza nessuno che guarda.
  • ×Rendere il tick non idempotente: quando lo scheduler ritenta o si sovrappone, duplicherà i forecast e corromperà lo stato in silenzio.
  • ×Dare all'autopilot una qualsiasi capacità di operare in reale senza una decisione umana deliberata; automazione + capacità di trade reale è il modo in cui un loop uccide un conto alle 3 di notte.
  • ×Affidarsi alla sorveglianza umana invece di codificare le garanzie come test e invarianti che il loop stesso verifica con `check` a ogni giro.

Costellazione successiva

Brand & Surface

Brand e superficie