Chiude il debito che CLAUDE.md dichiarava da settimane: la regola "ogni cambio di scala passa dal cap di config, non da target_vol" NON era implementabile perche' una chiave di scala non esisteva — qualunque cambio sarebbe finito su WEIGHT/W_TP01/W_SKH, cioe' codice su un percorso con soldi veri e per giunta nel posto sbagliato (W_TP01/W_SKH sono il RAPPORTO 75/25, non la taglia). Specifica gia' scritta in docs/research/SPEC-scale-key.md (618 righe, 9 condizioni di gate, prototipo). Non ho progettato: ho eseguito i punti 1-4. 🚨 config/live.json NON E' STATO TOCCATO. La chiave e' assente, vale 1,00, e T7 dimostra bit-exact che il libro e' quello di ieri (max|diff| = 0.0). Verificato anche a runtime: book_execute in dry-run da' gli stessi target del cron delle 15:47 (BTC $+355, ETH $+214). LE QUATTRO DECISIONI CHE NON SONO DI COMODO - La scala si applica DOPO il clamp. Prima, il cap se la mangerebbe proprio nei giorni di massima convinzione (a tp=1/sg=+1 il grezzo vale esattamente cap => k_eff tornerebbe a 1,00 a ogni k): sarebbe un cambio di FORMA travestito da cambio di taglia, e la curva g(k) con cui il gradino viene autorizzato non descriverebbe quel libro. Prezzo dichiarato: il cap diventa il tetto del libro UNITARIO, e la guardia sulla leva lorda va ricostruita. - Il tetto e' sul PRODOTTO e sta nel CODICE. Sulla sola chiave lascerebbe aperta la porta accanto (frac 0,625 x scala 1,25 = 1,562x); in config sarebbe un lucchetto con la chiave attaccata. LEVA_LORDA_MAX 1,25 in src/live/book.py => il gradino a 1,50 richiede codice, quindi review. - Fuori scaletta o fuori tetto = STOP, non clamp. book_execute si ferma, non invia, allerta (ScalaNonAutorizzata). E SCALA_LADDER (1,00 · 1,25) rende INESPRIMIBILE "solo un po'": 1,05 non e' prudente, e' fuori scaletta. - La scala vive solo sul percorso fidato (equity illeggibile => 1,00), cosi' "il fallback non e' piu' permissivo" e' vero per costruzione. Ma la VALIDAZIONE avviene sempre: una config rotta non si nasconde dietro un giro in cui l'equity non era leggibile. LA GUARDIA CHE MORDE PER PRIMA non e' il peggior giorno (k <= 3,49x) ma il COSTO di un disaster-SL (k <= 1,67x, 2,1x piu' stringente): l'invariante n_asset x frac x scala x disaster_sl_pct <= 0,50 scatta anche se qualcuno allarga lo stop invece di alzare la scala. TEST T1-T11 (tests/test_book_scale.py, 18 verdi). Il piu' importante e' T1b: a k=1 l'implementazione simmetrica e quella asimmetrica danno lo STESSO numero, quindi un test di simmetria scritto sul caso di default ha potenza ZERO. T1b verifica che le due coincidano a k=1 (il rischio e' reale) e che fuori da k=1 l'asserzione le SEPARI, con un'implementazione asimmetrica scritta nel test apposta perche' fallisca. SORVEGLIANTE scale_watch (cron_daily, 3 domande / 3 azioni / 3 stati, una allerta per streak, marcatore scritto solo dopo invio riuscito — debito #2). Riporta la frequenza del ramo di fallback, che sopra il 2% in 90 giorni invaliderebbe la regola: misurata 0/1.676, coi 19 giri "paper capital" (pre-finanziamento, dove il libro non invia) contati e dichiarati a parte. Non puo' impedire la modifica: la rende visibile entro 24h e attribuibile. CHIUDE il debito #5 di §5: T2/T3 sostituiscono il vecchio test_leva_massima_da_config (che misurava frac x n_asset mentre la grandezza vera e' frac x n_asset x scala), e T11 verifica che sia rimasto cancellato. NON FATTO, deliberato: la chiave in config (punto 2 lo vieta), GATE SCALA-01 (A2 richiede >=30 giorni a 1,00 col sorvegliante attivo — "l'unico modo di scoprire che il sorvegliante e' rotto mentre la leva e' ancora 1,00"), r0726_fee_sensitivity rifatto (A7: serve solo al gradino; a 1,25x una liquidazione costerebbe 1,25% non 1,00%, e ereditarlo sarebbe l'errore). Nessun ordine. Suite: 825 passati. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
38 KiB
SPEC — la CHIAVE DI SCALA del libro live (book_scale_k)
Filone SCALE-SPEC, ondata 2026-08-22, branch research/wave-0822.
✅ IMPLEMENTATA IL 2026-09-01 — punti 1-4 della checklist §8.
src/live/book.py(book_scale_k,_scala,LEVA_LORDA_MAX,SCALA_LADDER,DISASTER_SL_BUDGET,ScalaNonAutorizzata,scalanel report),scripts/live/book_execute.py(riga «scala libro» + stop-e-allerta),src/live/scale_watch.py+scripts/live/scale_watch.pyincron_daily.sh,data/live/scale_history.jsonl,tests/test_book_scale.py(T1-T11, 18 test verdi). 🚨config/live.jsonNON e' stato toccato, come prescrive il punto 2: la chiave e' assente, vale 1,00, e T7 dimostra bit-exact che il libro e' quello di sempre (max|diff| = 0.0). Restano i punti 5-7: ≥30 giorni a 1,00 col sorvegliante attivo, poiGATE SCALA-01, poir0726_fee_sensitivityrifatto. Il punto 5 non e' burocrazia — e' l'unico modo di scoprire che il sorvegliante e' rotto mentre la leva e' ancora 1,00.
Questo documento era una SPECIFICA scritta prima dell'implementazione. Il prototipo che dimostra la meccanica e'
scripts/research/r0822e_scale_proto.py (isolato, non importato da niente, non scrive nulla; la
sua sezione (0) verifica entrambe le cose a runtime invece di dichiararle).
Nasce da RESULTS-0822.md §33 (WORST-DAY), che ha misurato che il gradino di leva e' l'unica leva
grande rimasta e che C6 (processo) FALLISCE: la regola del progetto — «ogni cambio di SCALA
passa dal cap di config, non da target_vol» — non e' implementabile come scritta, perche' il
cap e' un CLAMP e non un moltiplicatore.
0. IL RISCHIO CHE QUESTA SPECIFICA DEVE RENDERE IMPOSSIBILE
Una chiave di scala e' esattamente il tipo di parametro che si alza «solo un po'» dopo un mese buono. E' un numero, sta in un file di config, non richiede di capire niente per cambiarlo, e il suo effetto e' immediato e piacevole.
Il progetto ha regole contro questa forma di deriva su ogni altro asse — weights_tilt_null per i
pesi, gate pre-registrati per i candidati, select_cell_insample per le griglie, anchor_luck_band
per le ancore — e nessuna per la scala. Non per dimenticanza: per aritmetica.
Lo Sharpe e' invariante alla scala. Misurato nel prototipo (sez. 7a), sulla serie vera del libro:
| k | Sharpe | drift | vol | maxDD | g(k) esatto |
|---|---|---|---|---|---|
| 1,00 | 1,5051 | 17,11% | 11,37% | −12,17% | 17,90% |
| 1,25 | 1,5051 | 21,39% | 14,21% | −15,01% | 22,61% |
| 2,00 | 1,5051 | 34,22% | 22,74% | −23,10% | 37,24% |
deflated_sharpe e marginal_vs_tp01 leggono la prima colonna. Non falliscono: non vedono la
variabile. Un raddoppio della leva passa ogni gate esistente del progetto senza far scattare nulla,
e produce lo stesso identico numero di sintesi che il progetto usa per giudicare tutto il resto.
Da qui discendono i tre vincoli non negoziabili del disegno:
- il tetto sta nel CODICE, non in config (una config puo' essere modificata da chiunque, in qualunque momento, senza review; una costante di codice no);
- il tetto sta sul PRODOTTO, non sulla chiave (
fracescalamoltiplicano: un tetto sulla sola chiave lascia aperta la porta accanto — vedi §4.2, tabella); - il gate e' uno SCRIPT che stampa PASSA/FALLISCE, non un giudizio (§6), e c'e' un sorvegliante giornaliero che se ne accorge dopo il fatto se qualcuno lo salta (§6.4).
⚠️ Correzione dovuta al mandato di questo filone. Il gradino non vale quello che dice la frase
che circola. RESULTS-0822.md §2 misura 1,00x → 1,25x = 14,7a → 12,9a, e 1,00x → 1,50x =
14,7a → 11,6a; il «~€300/mese di versamenti equivalenti» appartiene al gradino 1,50x.
Ricalcolato sulla tabella netta pubblicata (r0807_piano_netto: €500/m → 14,7a, €800/m → 11,4a,
interpolazione lineare dichiarata):
| gradino | anni al capitale-rendita | versamento mensile equivalente | delta |
|---|---|---|---|
| 1,00x (oggi) | 14,7a | €500/m | — |
| 1,25x | 12,9a | ~€664/m | +€164/m |
| 1,50x | 11,6a | ~€782/m | +€282/m |
L'interpolazione riproduce il «~€300/mese» pubblicato per 1,50x (€282) → è un controllo, non solo un conto. Questa specifica autorizza il gradino a 1,25x e vale ~€165/mese, non ~€300.
1. COSA SPECIFICA, E COSA NON FA
Specifica: dove va la chiave, in che ordine si applica rispetto al cap, quali guardie servono, quali test devono esistere prima della modifica, quale gate deve essere superato per alzarla e con quale cadenza si sorveglia.
NON fa: non autorizza il gradino, non implementa niente, non tocca config/live.json. Con la
chiave assente o = 1.0 il libro e' bit-exact quello di oggi (§5, test T7): la modifica e'
inerte finche' qualcuno non scrive un numero, e quel numero passa dal gate del §6.
2. DOVE VA LA CHIAVE, ESATTAMENTE
2.1 La catena, letta dal sorgente vero
Il prototipo la stampa leggendo i file (sez. 2), cosi' le righe qui sotto sono verificabili e non citate a memoria:
| stadio | file:riga | cosa produce |
|---|---|---|
| segnale TP01 | src/live/shadow.py:119 targets = {a: tp.current_target(dfs[a])...} |
una frazione ≥0, long-flat |
| segnale SKH01 | src/live/book.py:217 skh = _skyhook_positions(...) → :228 sign = _skh_sign(st) |
un segno +1/−1/0 |
| cap per-asset | src/live/book.py:107 def _cap(...) → :194 cap = _cap(...) |
il clamp, legato al watermark |
| TARGET NETTO | src/live/book.py:138 def book_net_target(...) → :229 net = book_net_target(tp_frac, sign, equity, cap) |
← QUI VA LA SCALA |
| ordine | src/live/book.py:153 def build_book_order(...) → :232 |
un delta verso il target |
| invio | scripts/live/book_execute.py:208 trader.rebalance_signed(inst, net, mark, ...) |
il fill |
2.2 La firma proposta
# src/live/book.py
def book_net_target(tp_frac: float, skh_sign: int, equity: float, cap: float,
weight: float = WEIGHT, scala: float = 1.0) -> float:
raw = weight * equity * (W_TP01 * max(tp_frac, 0.0) + W_SKH * float(skh_sign))
return scala * max(-cap, min(cap, raw)) # <-- la scala e' FUORI dal clamp: vedi §3
più, accanto a _cap(), un lettore dedicato:
LEVA_LORDA_MAX = 1.25 # COSTANTE DI CODICE, non di config. Vedi §4.1.
SCALA_LADDER = (1.00, 1.25) # i soli valori esprimibili. Vedi §4.1 e §6.
def _scala(real_equity, eq_fallback) -> float:
"""Scala del libro. 1.0 (= il libro di oggi) in ogni stato degradato: vedi §4.3."""
if real_equity is None or eq_fallback:
return 1.0
... # legge config, valida contro LEVA_LORDA_MAX e SCALA_LADDER, ALZA se fuori (§4.1)
e in book_report, una riga sotto cap = _cap(...):
scala = _scala(sh.get("real_equity"), sh.get("eq_fallback"))
...
net = book_net_target(tp_frac, sign, equity, cap, scala=scala)
book_report deve esporre scala nel dict di ritorno accanto a cap_per_asset, e
book_execute deve stamparla nella riga «sizing base»: un parametro che agisce sul nozionale e
non compare nel report e' la forma esatta del difetto che questo progetto ha gia' pagato tre volte
(docstring che dichiarano una causalita' che il codice non ha).
2.3 Perche' li' e non altrove — e la dimostrazione
book_net_target e' l'unico punto della catena in cui le due gambe si incontrano con una TAGLIA:
sopra di lei TP01 da' una frazione e SKH01 da' solo un segno; sotto di lei si calcolano delta e
ordini. Percio' e' l'unico punto in cui un moltiplicatore e' un puro cambio di scala.
Il prototipo (sez. 3) lo misura sulla funzione di produzione stessa — la copia e' bit-exact
(max|copia − produzione| = 0.00e+00 su 480 combinazioni):
| k | solo TP01 (tp=1, sg=0) | solo SKH01 (tp=0, sg=1) | rapporto TP/SKH | peso implicito TP01 |
|---|---|---|---|---|
| 1,00 | 2.267,62 | 755,88 | 3,000000 | 0,750000 |
| 1,25 | 2.834,53 | 944,84 | 3,000000 | 0,750000 |
| 2,00 | 4.535,25 | 1.511,75 | 3,000000 | 0,750000 |
max|target(k) − k·target(1)| = 0.00e+00 su 90 combinazioni. Il mix 75/25 e' invariante a ogni k:
e' una scala, non un tilt.
Le alternative, e perche' cadono:
| dove | perche' NO |
|---|---|
TrendPortfolio.target_vol |
scala il 75% del libro e lascia fermo il 25% → e' anche un tilt di pesi, e fallisce weights_tilt_null. Misurato nel prototipo come controllo positivo: a k=1,25 il peso implicito di TP01 passa da 0,750 a 0,789. |
_skyhook_positions |
non c'e' niente da scalare: produce un segno. |
WEIGHT (=0,5) |
e' lo split BTC/ETH, ed e' condiviso con shadow_report/build_rebalance_order (shadow.py:144) e con la dashboard, cioe' con il percorso TP01-only: cambiarlo cambierebbe anche un altro libro. E confonderebbe in un solo numero «quanti asset» e «quanta leva». |
W_TP01 / W_SKH |
sono il rapporto 75/25 e sono legati per test ai pesi del backtest (test_weights_match_backtest_sleeves asserisce W_TP01+W_SKH == 1.0 e la parita' con deribit_book_sleeves). Moltiplicarli entrambi romperebbe quella parita' e renderebbe impossibile distinguere scala e mix. |
build_book_order |
scalerebbe il delta, non il target: cambia la velocita' di convergenza, non la taglia, e il reconciler litigherebbe con se' stesso a ogni giro. |
dentro DeribitTrader |
il report, il dry-run e book_executions.jsonl stamperebbero un target diverso da quello eseguito. |
3. L'INTERAZIONE COL CAP — il punto sottile
3.1 Il fatto geometrico
Il cap e' un clamp: net = clamp(0.5·E·(0,75·tp + 0,25·sg), ±cap) con cap = frac·E = 0,50·E.
A piena convinzione (tp=1, sg=+1) il grezzo vale esattamente 0,5·E·1,0 = 0,50·E = cap:
il clamp e' tangente al libro proprio nei giorni di massima esposizione.
3.2 I due ordini, misurati (prototipo sez. 4, su 5.648 giorni-asset veri)
| k | ordine | clampati | quota | k_eff medio | k_eff a piena convinzione | lordo max |
|---|---|---|---|---|---|---|
| 1,25 | PRIMA del clamp | 32 | 0,57% | 1,2487 | 1,0000 | 1,000 |
| 1,25 | DOPO il clamp | 0 | 0,00% | 1,2500 | 1,2500 | 1,250 |
| 2,00 | PRIMA | 793 | 14,04% | 1,9235 | 1,0000 | 1,000 |
| 2,00 | DOPO | 0 | 0,00% | 2,0000 | 2,0000 | 2,000 |
L'ordine corretto e' DOPO il clamp, e la ragione non e' di comodo:
- PRIMA: il clamp mangia la scala proprio nei giorni di massima convinzione (k_eff = 1,00 a
piena convinzione, a ogni k). Il risultato non e' «il libro a leva k»: e' un libro che alza i
giorni piccoli e lascia fermi i grandi, cioe' un appiattimento del profilo di convinzione. E'
un cambio di FORMA travestito da cambio di taglia — e allora la curva g(k) di
r0822_growth_policy/ WORST-DAY non descrive quel libro, perche' quella curva e' calcolata suk·r, cioe' sull'ordine DOPO. Autorizzare il gradino con una curva e implementarne un'altra e' il modo piu' silenzioso di sbagliare che questa specifica possa produrre. - DOPO:
k_eff = kesattamente, in ogni giorno e in ogni stato del segnale. E' l'unico ordine per cui «leva k» significa la stessa cosa nel codice e nel backtest.
3.3 Cosa implica per il SIGNIFICATO del cap — e va detto, perche' e' il prezzo
Con l'ordine DOPO, il cap smette di essere il tetto assoluto del nozionale. Diventa il tetto
del libro unitario: limita la forma (nessun asset oltre frac·E di esposizione unitaria),
mentre la taglia la decide la scala. Il lordo massimo passa da n_asset·frac a
n_asset·frac·scala = 1,250x l'equity a k=1,25.
E il cap non smette di fare le altre due cose che fa oggi, che sono quelle che contano davvero:
- resta proporzionale all'equity (
cap = equity·fracsul percorso fidato) → un deposito non strozza il libro e un prelievo lo rimpicciolisce da solo; - resta legato al watermark sul percorso di fallback (
min(fisso, watermark·frac)) → il caso che il 26/07 valeva «3,35x di leva su un conto da $597» resta chiuso (§4.3).
⚠️ Cio' che il cap NON puo' piu' fare da solo e' essere la guardia di sicurezza sulla leva lorda. Quella guardia va ricostruita esplicitamente, ed e' il §4.
3.4 Il test che si rompe — e deve rompersi
tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x:
leva_max = frac * len(FS.ASSETS)
assert leva_max <= 1.0, ("leva massima ...: la liquidation fee 1% torna rilevante, "
"rivedere r0726_fee_sensitivity e il diario 2026-07-26-fee-deribit.md")
Si rompe? No — e questo e' il punto. Con frac invariato a 0,50 il test continua a passare,
perche' misura frac · n_asset mentre la grandezza vera diventa frac · n_asset · scala.
Non si rompe, e smette di controllare cio' che era stato scritto per controllare.
E' esattamente la forma di difetto che il progetto ha catturato tre volte in un giorno il
2026-08-21: un controllo puntato su una configurazione diversa da quella che gira passa sempre, e
non sta controllando niente (fee_watch sugli inverse, venue_watch con bitfinex, i test di
book_live a libro flat).
Quindi va sostituito, non aggiustato, e la sostituzione e' una coppia:
- T2 (§5) —
test_la_leva_lorda_da_config_resta_sotto_il_tetto: asseriscelen(ASSETS) * frac * scala <= LEVA_LORDA_MAX, cioe' il prodotto, e importa la scala dallo stesso posto da cui la importa la produzione (regola del 21/08: un sorvegliante deve DERIVARE il proprio bersaglio dal codice sorvegliato, mai ridichiararlo). - prerequisito del gate (§6):
r0726_fee_sensitivityva rifatto al nuovo lordo, perche' la sua conclusione «liquidation fee 1% irrilevante» era esplicitamente condizionata a nozionale lordo ≤1x. Al gradino il lordo e' 1,25x → una liquidazione costerebbe 1,25% dell'equity, non 1,00%. Non e' un problema; e' un numero che va ricalcolato e ripubblicato, non ereditato.
Il vecchio test va cancellato con una nota che dice perche' (e rimandi a questo documento), non modificato in silenzio: un test di decisione che cambia soglia senza lasciare traccia e' un test di decisione perso.
4. LE GUARDIE
4.1 G1 — il tetto: sul PRODOTTO, in CODICE, e non silenzioso
LEVA_LORDA_MAX = 1.25 # src/live/book.py — costante di CODICE
SCALA_LADDER = (1.00, 1.25)
Tre proprieta', tutte necessarie:
(a) Sul prodotto. frac e scala moltiplicano. Un tetto sulla sola chiave lascia aperta la
porta accanto (prototipo sez. 6):
| frac | scala | leva lorda | tetto «scala ≤ 1,25» | tetto sul PRODOTTO |
|---|---|---|---|---|
| 0,500 | 1,25 | 1,250 | passa | passa |
| 0,625 | 1,25 | 1,562 | passa | BLOCCA |
| 0,750 | 1,25 | 1,875 | passa | BLOCCA |
| 0,500 | 2,00 | 2,000 | BLOCCA | BLOCCA |
Ed e' un rischio concreto, non teorico: WORST-DAY ha appena pubblicato che «alzare frac da
0,50 a 0,625 non produce k=1,25», il che rende frac un candidato naturale al prossimo ritocco —
salvo che, combinato con la scala, lo produce eccome.
(b) In codice. Il tetto di config sarebbe modificabile dalla stessa mano, nello stesso file, nel
momento in cui si modifica la chiave: sarebbe un lucchetto con la chiave attaccata. Con
LEVA_LORDA_MAX in src/live/book.py, il gradino 1,25 → 1,50 richiede una modifica di codice,
quindi una review, quindi un gate. Il tetto e' basso apposta: e' il meccanismo del cricchetto, non
un'opinione sulla leva massima sopportabile (che dalla §4.5 sarebbe 1,67x).
(c) Non silenzioso. Una scala fuori tetto (o fuori SCALA_LADDER) NON viene tagliata al
tetto: book_execute si ferma, non invia ordini, e allerta. Un clamp silenzioso farebbe
girare una config che dichiara un numero e un libro che ne esegue un altro — la stessa classe di
difetto del §3.4. SCALA_LADDER esiste perche' una scala a gradini rende inesprimibile «solo un
po'»: 1,05 non e' un valore prudente, e' un valore fuori scaletta, e viene rifiutato.
4.2 G2 — l'invariante che lega TRE chiavi
assert len(ASSETS) * frac * scala * disaster_sl_pct <= 0.50
Vedi §4.5: e' la guardia che morde per prima, e cattura chi allarga il disaster-SL.
4.3 G3/G4 — il watermark e l'equity non leggibile
Regola proposta: la scala vive SOLO sul percorso fidato.
scala_effettiva = scala se l'equity REALE e' leggibile, altrimenti 1.00
Motivo: una scala e' una decisione di rischio presa conoscendo il conto; quando non si sa quanto vale il conto, non si prende. Cosi' «il fallback non e' piu' permissivo» e' vero per costruzione, non per aritmetica — e non dipende dal fatto che il conto degli invarianti torni.
Misurato (prototipo sez. 6-bis; watermark reale letto da disco, $634,996; copia di _cap
verificata bit-exact contro la produzione):
| stato | cap | scala eff. | lordo max | / equity vera | tetto |
|---|---|---|---|---|---|
| normale (equity reale letta) | 317,50 | 1,25 | 793,74 | 1,250x | si |
| online ma equity illeggibile | 317,50 | 1,00 | 635,00 | 1,000x | si |
| watermark assente (1° avvio) | 300,00 (CAP_UNKNOWN_USD) |
1,00 | 600,00 | 0,945x | si |
| watermark vecchio e basso | 50,00 | 1,00 | 100,00 | 0,157x | si |
| dopo un deposito a $6.047 | 3.023,50 | 1,25 | 7.558,75 | 1,250x | si |
In nessuno stato degradato la leva supera quella dello stato normale, e il caso «watermark assente» ha due protezioni indipendenti (cap $300 e scala 1,00), non una sola.
⚠️ Prezzo dichiarato. Nei giri in cui l'equity non e' leggibile il libro gira a k=1 e al giro
dopo torna a k → un ribilancio in piu' per oscillazione, di (k−1)·frac·E per asset = ~$79 a
testa sul conto di oggi, cioe' ~$0,056 di fee a 3,5 bps. Frequenza misurata su
logs/cron_book.log: 0 giri su 1.442 (0,00%); i 21 giri «conto non leggibile» sono OFFLINE, e
li' il libro non manda ordini affatto, quindi la scala non si pone. → il prezzo esiste, e' noto,
ed e' nullo sul campione osservato.
⚠️ E questo e' anche il limite dichiarato della regola: se un giorno il ramo di fallback
diventasse frequente, il libro girerebbe a una leva mista e la curva g(k) smetterebbe di
descriverlo. → il sorvegliante del §6.4 deve riportare quella frequenza, e se supera il 2%
dei giri in 90 giorni la regola va ridiscussa (soglia dichiarata qui, prima di vedere il caso).
Alternativa scartata: congelare l'ultimo target scalato durante il fallback. Aggiunge stato a un percorso con soldi veri — cioe' un nuovo modo di essere sbagliati — per risparmiare $0,056.
4.4 G5 — il min_order_usd non e' un problema, e va detto
La scala aumenta i delta, quindi allontana dal pavimento $5 invece di avvicinarcisi. Nessuna azione. (Lo si dice perche' «non e' un problema» misurato vale quanto un problema trovato: evita che la domanda torni.)
4.5 G6 — il disaster-SL: la guardia che morde per prima, e non era stata calcolata
Il disaster-SL e' un movimento di prezzo (−30% sul mark), non una perdita in equity. L'impatto
in equity e' quel movimento moltiplicato per il lordo, quindi cresce con k anche se
disaster_sl_pct non cambia. Prototipo sez. 6, tutte le righe scalate dai numeri replicati
di WORST-DAY (§7):
| k | lordo max | A) peggior giorno possibile | B) costo disaster-SL | C) x_liq | min intra·k | margine C |
|---|---|---|---|---|---|---|
| 1,00 | 0,864 | 14,32% | 30,00% | 115,8% | 7,33% | 15,8x |
| 1,25 | 1,081 | 17,90% | 37,50% | 92,5% | 9,16% | 10,1x |
| 1,50 | 1,297 | 21,48% | 45,00% | 76,9% | 11,00% | 7,0x |
| 2,00 | 1,729 | 28,64% | 60,00% ← sfonda | 57,4% | 14,66% | 3,9x |
📌 La guardia che morde per prima NON e' il peggior giorno possibile, e' il disaster-SL:
- da (A):
k ≤ 50% / 14,32%= 3,49x - da (B):
k ≤ 50% / (2 · 0,50 · 30%)= 1,67x ← 2,1x piu' stringente
A k=1,25 il margine di (A) e' 2,8x (il numero che WORST-DAY pubblica come C1) mentre quello di
(B) e' 1,33x. Il criterio C1 di WORST-DAY guardava la grandezza sbagliata: guardava il
peggior giorno del segnale, non il costo del dispositivo di emergenza che il libro ha davvero
armato sul conto. Da qui l'invariante G2 (§4.2), che con sl_pct=30% e frac=0,50 da'
scala ≤ 1,67x — e che scatta se qualcuno allarga il disaster-SL.
Decisione: disaster_sl_pct NON si tocca. Stringerlo a 24% per tenere invariante l'impatto in
equity lo trasformerebbe da dispositivo di catastrofe in stop di strategia: verrebbe toccato dal
rumore, e il progetto ha gia' misurato cosa succede quando uno stop stretto incontra un gap (SKH01,
sl 2% modellato → −11/−23% realizzato). Si accetta il 37,5% e lo si scrive.
⚠️ Osservazione da LETTURA DEL CODICE, non misurata, e va citata cosi'.
src/live/execution.py:196-223 ri-ancora il disaster-SL al mark a ogni giro: se lo stop esistente
dista piu' del 5% relativo da mark·(1−sl_pct), viene cancellato e ripiazzato. Per un long
questo significa che lo stop segue il prezzo anche in discesa: il −30% e' un −30% rotolante
dall'ultimo piazzamento, non un pavimento assoluto sul trade. Quindi B e' il costo di UN
episodio, e un ribasso lento puo' produrne piu' d'uno.
Evidenza circostanziale, non prova: nel log ci sono 15 placed contro 259 ok su asset con
posizione aperta — coerente con qualche ri-ancoraggio, ma i placed includono anche le prime
aperture e non sono separabili dal log. → e' una proprieta' di oggi che il gradino amplifica,
non una che il gradino crea, ed e' un follow-up con una domanda precisa: quante volte il bracket
e' stato ri-piazzato mentre la posizione restava aperta? (misurabile aggiungendo pos_prima al log,
non ricostruibile da quello attuale).
5. I TEST CHE DEVONO ESISTERE PRIMA DELLA MODIFICA
Scritti come specifiche eseguibili: nome + cosa asserisce + il caso che deve far fallire.
File proposto: tests/test_book_scale.py (piu' le modifiche dichiarate a
tests/test_fee_sensitivity.py).
| # | test | asserisce | deve fallire se |
|---|---|---|---|
| T1 | test_la_scala_e_identica_sui_due_sleeve |
su una griglia di k (1,00/1,10/1,25/1,50/2,00) × tp_frac × segno, con cap disattivato: target(k) == k·target(1) e target(tp=1,sg=0)/target(tp=0,sg=1) == W_TP01/W_SKH costante |
qualcuno applica la scala a una sola gamba (o dentro TrendPortfolio) |
| T1b | test_il_test_di_simmetria_smaschera_una_scala_asimmetrica |
CONTROLLO POSITIVO: la stessa asserzione, applicata a un'implementazione asimmetrica scritta nel test, fallisce | il test T1 gira solo a k=1 (dove le due implementazioni coincidono: potenza zero — misurato nel prototipo) |
| T2 | test_la_leva_lorda_da_config_resta_sotto_il_tetto |
len(ASSETS) * frac * scala <= LEVA_LORDA_MAX, con frac e scala letti da config/live.json e LEVA_LORDA_MAX/ASSETS importati dal modulo di produzione |
qualcuno alza frac, o la scala, o entrambe di poco |
| T3 | test_l_invariante_disaster_sl_tiene_meta_equity |
len(ASSETS) * frac * scala * disaster_sl_pct <= 0.50 |
qualcuno allarga disaster_sl_pct o alza la scala fino a 1,67x |
| T4 | test_il_fallback_non_e_piu_permissivo_del_percorso_fidato |
in ogni stato degradato (equity illeggibile / watermark assente / watermark vecchio) il lordo massimo / equity vera ≤ quello dello stato normale | qualcuno applica la scala anche in fallback, o slega il cap dal watermark |
| T5 | test_il_cap_continua_a_limitare_cio_che_limitava |
il target per-asset non supera mai cap·scala; e il clamp morde ancora sulle stesse osservazioni di prima (le 3 su 5.650) |
qualcuno inverte l'ordine (§3.2) o toglie il clamp «tanto c'e' la scala» |
| T6 | test_una_scala_fuori_tetto_ferma_l_esecuzione_e_non_viene_tagliata |
con scala fuori SCALA_LADDER o fuori tetto: book_execute non invia, allerta, e il valore non viene silenziosamente clampato |
qualcuno «rende robusto» il lettore con un min(scala, LEVA_LORDA_MAX/…) |
| T7 | test_senza_la_chiave_il_libro_e_quello_di_oggi |
con chiave assente o = 1.0, book_net_target e' bit-exact alla versione attuale su una griglia (`max |
diff |
| T8 | test_il_report_e_il_log_dichiarano_la_scala |
book_report()['scala'] esiste e book_execute la stampa |
un parametro che muove il nozionale resta invisibile nel report |
| T9 | test_la_scala_e_derivata_dal_codice_non_ridichiarata |
il sorvegliante e i test importano LEVA_LORDA_MAX, ASSETS, SCALA_LADDER da src/live/book e non li riscrivono |
due liste in due file (regola del 21/08, 4 occorrenze) |
| T10 | test_il_sorvegliante_segnala_una_scala_non_autorizzata |
CONTROLLO POSITIVO del §6.4: con una config che dichiara una scala assente dal giornale, il sorvegliante allerta; con config e giornale concordi, tace | il sorvegliante e' tarato per non segnalare mai (regola del 26/07: un rilevatore tarato per non segnalare e' indistinguibile da uno rotto) |
| T11 | test_il_vecchio_test_di_leva_e_stato_sostituito_non_rilassato |
test_leva_massima_da_config_resta_sotto_o_uguale_a_1x non esiste piu' e T2/T3 esistono |
qualcuno alza la soglia del vecchio test da 1,0 a 1,25 e chiude li' |
⚠️ T1b e' il test piu' importante dell'elenco, ed e' la ragione per cui questo paragrafo esiste:
il prototipo (sez. 3) mostra che a k=1 l'implementazione simmetrica e quella asimmetrica danno lo
stesso identico numero. Un test di simmetria scritto sul caso di default passa sempre e non
controlla niente — la stessa firma dei test di book_live scoperti senza potenza a libro flat.
6. IL GATE MANCANTE — GATE SCALA-01
6.1 Perche' serve un gate NUOVO
Vedi §0: nessun gate esistente vede la scala. Il gate deve quindi vivere sull'unico asse su cui la
scala si vede: crescita e rovina (g(k), maxDD, distanza dalla liquidazione), non Sharpe.
6.2 Le condizioni — tutte necessarie
Per portare book_scale_k da k0 a k1:
| condizione | misurata come | |
|---|---|---|
| A1 | k1 e' il gradino successivo di SCALA_LADDER (1,00 → 1,25). Nessun valore intermedio. |
confronto con la costante di codice |
| A2 | k0 e' in produzione da ≥180 giorni con il criterio passato ogni giorno |
giornale data/live/scale_history.jsonl (append-only) + registrazioni del sorvegliante |
| A3 | C1–C5 di WORST-DAY ricalcolati oggi a k1, tutti PASSA — non ereditati dal 22/08 |
ri-esecuzione di r0822c_worst_day sui dati correnti |
| A4 | C7 = G2: n_asset · frac · k1 · disaster_sl_pct ≤ 0,50 |
§4.5 |
| A5 | C8 = G1: n_asset · frac · k1 ≤ LEVA_LORDA_MAX |
§4.1 |
| A6 | ANTI-RECENCY: il criterio deve passare anche escludendo gli ultimi 90 giorni: k_ammesso(r[:-90]) ≥ k1 |
§6.3 |
| A7 | r0726_fee_sensitivity rifatto al nuovo lordo e la conclusione «liquidation fee irrilevante» ri-derivata o ritirata |
§3.4 |
| A8 | la modifica e' una riga di config + una riga di giornale, zero codice. Se serve toccare LEVA_LORDA_MAX, non e' una decisione di config |
diff |
| A9 | i test T1–T11 (§5) esistono e passano prima | pytest |
6.3 A6 — l'anti-recency, e come si misura davvero
La forma verificabile e': il gradino dev'essere autorizzato anche dai dati che non contengono la
finestra recente che ha motivato la richiesta. E' l'analogo, sull'asse della scala, di
select_cell_insample.
Il prototipo (sez. 7b) misura la meccanica su k_ammesso = ½·k*(drift a −2 SE, coda troncata al bound strutturale):
| finestra | n | drift punto | drift −2SE | k ammesso | ≥1,25x? |
|---|---|---|---|---|---|
| tutta la storia | 2.719 | 17,11% | 8,78% | 3,52 | SI |
| esclusi ultimi 90 g | 2.629 | 16,70% | 8,15% | 3,20 | SI |
| esclusi ultimi 365 g | 2.354 | 19,52% | 10,20% | 3,80 | SI |
| solo ultimi 365 g | 365 | 1,59% | −13,69% | 0,03 | NO |
e la sensibilita' a un tratto recente artificialmente buono (ultimi 90 giorni riscritti a un drift dato, sull'intera storia):
| ultimi 90 g portati a | drift −2SE | k ammesso | delta |
|---|---|---|---|
| 0%/anno | 7,82% | 3,12 | −0,40 |
| +50%/anno | 9,47% | 3,80 | +0,28 |
| +100%/anno | 11,11% | 4,45 | +0,93 |
| +200%/anno | 14,35% | 5,63 | +2,10 |
Il −2 SE fa gran parte del lavoro (un tratto eccezionale alza insieme il drift e la sua incertezza), ma non basta: il criterio resta una funzione dei dati recenti, e la protezione vera e' la doppia valutazione di A6.
⚠️ Tre onesta' obbligatorie su questa sezione:
k_ammessodel prototipo e' deliberatamente semplificato (troncatura al bound, nessuna GPD): serve a mostrare la meccanica, non a produrre il numero. Il gate reale usa la versione dir0822c_worst_day.- Oggi A6 non morde:
k_ammesso ≈ 3,5xcontro un gradino di 1,25x. Il vincolo che morde e' quello del §4.5 (disaster-SL). A6 serve al gradino successivo, non a questo — e va scritto ora, quando non e' comodo per nessuno, non quando servira'. - La prima stesura di questa sezione presentava «solo ultimi 365 giorni» come l'esempio di «un anno buono»: falso, quell'anno e' stato debole (drift 1,59%). Quella riga mostra solo che un campione corto ha un SE enorme. La riga che risponde alla domanda vera e' la tabella dello shock.
6.4 La cadenza — scale_watch
Sorvegliante giornaliero (proposto: src/live/scale_watch.py + scripts/live/scale_watch.py in
cron_daily.sh; non implementato), che risponde a tre domande diverse — e sono davvero tre,
perche' hanno tre azioni diverse e nessuna e' deducibile dalle altre:
| domanda | esito | azione |
|---|---|---|
| la config dichiara una scala che il giornale non ha mai autorizzato? | 🚨 | ricostruire come ci e' arrivata; scendere a k0 |
| il criterio che autorizzo' la scala corrente passa ancora oggi? | 🚨 | scendere di un gradino |
il prodotto frac·scala·n_asset ha superato LEVA_LORDA_MAX? |
🚨 | bloccare l'esecuzione |
Piu' due requisiti che il progetto ha gia' imparato a pagare:
- tre stati, non due:
OK/ALLARME/NON MISURABILE(serie troppo corta, dati mancanti). «Non vedo» non e' «va tutto bene» —paper_dvolspread,venue_watch. - disciplina degli allarmi: una allerta per streak (
lock_step), non una per giro; e il messaggio deve dire cosa (decide) e perche' (ripara), con il valore letto, mai cablato.
E deve riportare la frequenza del ramo di fallback (§4.3), che oggi e' 0/1.442 e che se salisse sopra il 2% in 90 giorni invalida la regola «scala solo sul percorso fidato».
⚠️ Cio' che il sorvegliante NON puo' fare: impedire la modifica. Chi ha accesso al file puo'
scriverci dentro. Il sorvegliante rende la modifica visibile entro 24 ore e attribuibile.
E' la stessa onesta' di venue_watch: non protegge il saldo, compra tempo.
7. IL COSTO DI NON FARLO, E IL RISCHIO DI FARLO
7.1 Il costo di restare a 1,00x
~€165/mese di versamenti equivalenti, a costo zero, per sempre (§0): 14,7a → 12,9a al capitale-rendita a €500/m netto fisco. Sulla scala delle leve misurate dal progetto sta sotto «versare o no» (da mai a 16 anni) e «quando versare» (fattore 6), e sopra tutto cio' che la ricerca ha prodotto in due mesi: l'ottimizzazione del peso SKH01 vale +0,030 di Sharpe e ha fallito il suo gate; il gradino vale 1,8 anni.
Ed e' l'unica leva grande che non richiede soldi nuovi.
7.2 Il rischio di farlo, in una tabella
| grandezza | k=1,00 | k=1,25 | fonte |
|---|---|---|---|
| peggior giorno possibile (chiusura) | 14,32% | 17,90% | replicato, §4.5 |
| peggior giorno vissuto (chiusura) | 3,95% | 4,94% | replicato |
| peggior minimo intra-giorno | 7,33% | 9,16% | WORST-DAY (lente accoppiata) |
| costo di un disaster-SL | 30,0% | 37,5% | §4.5 ← la guardia che morde |
| margine di liquidazione | 15,8x | 10,1x | §4.5 |
| maxDD storico | 12,17% | 15,01% | §0 |
| costo funding non modellato | ~1,53%/a | ~1,91%/a | FUNDING §35 |
| costo di una liquidazione (fee 1%) | 1,00% eq | 1,25% eq | §3.4, da ricalcolare |
7.3 La riga che WORST-DAY ha dichiarato, e che va tenuta in cima alla decisione
Se si credono entrambe le riserve insieme — drift a −2 SE e un giorno a −10% ogni anno — il drift atteso e' −0,61%/anno, e a quel punto non e' il gradino a essere sbagliato, e' il libro: chi usa quella combinazione sta chiedendo di spegnere anche il conto di oggi.
Questa specifica non risolve quel dilemma e non deve: lo rende esplicito. Chi rifiuta il gradino in nome di entrambe le riserve deve dire cosa propone di fare del libro a k=1.
7.4 Cosa questa specifica non rende sicuro
- La lente e' close-only dove non usa i numeri intra-giorno di WORST-DAY. Su una regola a un giorno close-only e' esattamente cieca (25/07, riconfermato dallo scettico il 22/08). Sul canale prop/funded — dove il vincolo binding e' una barriera di DD giornaliera — questa specifica non si applica e la lente accoppiata resta obbligatoria.
g(k)assume ritorno indipendente dalla size. A $635 e' innocuo (SLIP-AUDIT: nessun impatto misurabile, e un fill ha preso il 21,9% del volume della sua barra 5m in un range di 1,08 bps), ma quella misura ha una data di scadenza — la partecipazione scala lineare col capitale.- Il funding non e' in nessuna serie di questo documento. Il libro paga gia' ~1,5%/anno che nessun backtest sottrae; il gradino lo amplifica del 25%. E' dentro C5, ma il livello a k=1 resta un difetto aperto del progetto, non di questa specifica.
- La leva non compra probabilita', compra fretta (PROP-ALLOC). Il gradino accorcia la mediana;
non alza
P(arrivare)in modo proporzionale, e alza la varianza del percorso.
8. PROCEDURA DI ATTIVAZIONE (checklist, in ordine)
- Scrivere
tests/test_book_scale.pycon T1–T11 (§5). Prima. T7 deve passare contro il codice attuale (la chiave non esiste ancora → default 1,0 → bit-exact). - Implementare §2.2 (
scalainbook_net_target,_scala(),LEVA_LORDA_MAX,SCALA_LADDER,scalanel report e nel print dibook_execute). Senza toccareconfig/live.json: la chiave assente vale 1,0, il libro non cambia, i test lo dimostrano. - Cancellare
test_leva_massima_da_config_resta_sotto_o_uguale_a_1xcon la nota (§3.4). - Scrivere
scale_watch(§6.4) +data/live/scale_history.jsonlcon la riga iniziale{"scala": 1.00, "dal": "...", "gate": "iniziale, nessun gradino"}, e aggiungerlo acron_daily.she al backup di/opt/docker/scripts/backup.sh(il giornale non e' ricostruibile). - Lasciare girare a 1,00 per almeno 30 giorni con il sorvegliante attivo. Una manopola nuova si guarda funzionare prima di girarla.
- Solo allora: eseguire
GATE SCALA-01(§6.2). Se PASSA, una riga di config + una di giornale. - Rifare
r0726_fee_sensitivityal nuovo lordo (A7) e ripubblicare la conclusione.
Il punto 5 non e' burocrazia: e' l'unico modo di scoprire che il sorvegliante e' rotto mentre la leva e' ancora 1,00.
9. NODI APERTI — dichiarati, non nascosti
- Il disaster-SL rotolante (§4.5, ⚠️): letto nel codice, non misurato. Il log non permette
di separare i ri-piazzamenti dalle prime aperture. Se davvero rotola,
Be' il costo di un episodio e un ribasso lento ne produce piu' d'uno. E' il nodo piu' serio di questo documento, perche' tocca la guardia che morde per prima. Follow-up: aggiungerepos_primaal log diensure_disaster_sle ri-misurare — non ricostruibile a posteriori. LEVA_LORDA_MAX = 1.25e' una scelta di processo, non una misura. Dall'invariante del §4.5 il massimo sostenibile sarebbe 1,67x. Il valore basso e' il cricchetto. Chi vuole 1,50x deve toccare codice — ed e' voluto.- La banda d'ancora non e' stata girata su nessun numero di questo documento. Tutte le serie
sono all'ancora canonica con la correzione ×0,89 sul drift. Le grandezze usate per decidere
(bound strutturale, lordo, invarianti) sono aritmetiche e non ancorate;
g(k)ek_ammessolo sono — e vanno lette con la banda del libro (Sharpe FULL de-luckato 1,95, banda [1,81; 2,12]). - Il gradino non e' stato autorizzato qui. Questo documento specifica come si autorizza.
10. IL PROTOTIPO
scripts/research/r0822e_scale_proto.py — 0,9 s, nessuna rete, nessuna scrittura.
nice -n 19 timeout 900 uv run python scripts/research/r0822e_scale_proto.py
Sezioni: (0) isolamento verificato · (1) sanity bit-exact vs produzione · (2) la catena letta dal sorgente · (3) simmetria + controllo positivo · (4) l'ordine col clamp · (5) replica di WORST-DAY · (6) guardie · (6-bis) stati dell'equity · (7) il gate mancante.
Replica a k=1,00 (sez. 5) — il motivo per cui le righe scalate di questo documento non sono numeri copiati:
| grandezza | prototipo | pubblicato | scarto |
|---|---|---|---|
| drift de-luckato ×0,89 | 17,11% | 17,11% | 0,00% |
| vol annua | 11,37% | 11,37% | −0,00% |
| Sharpe de-luckato | 1,505 | 1,510 | −0,005 |
| peggior giorno (chiusura) | −3,95% | −3,94% | −0,01% |
| data del peggior giorno | 2020-03-13 | 2020-03-13 | coincide |
| morso del clamp | 3 / 5.648 | 3 / 5.650 | 2 righe di bordo* |
| bound strutturale | −14,32% | −14,32% | 0,00% |
* differenza di costruzione: il prototipo scarta la prima barra per asset (serve vol_prev).
Documento scritto senza modificare nulla in produzione. Il branch research/wave-0822 e' quello da
cui il cron esegue: ogni riga di src/, config/, scripts/live/, scripts/cron_*.sh e tests/
e' rimasta identica.