diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index d992434..791e307 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -703,6 +703,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 35 | FUNDING | il costo mai prezzato di TENERE una posizione aperta | **LEAD** — **−2,16%/anno di drift**, muri **+17,5%** | | 36 | PIANO-VERO | la tabella congiunta fisco+funding, che non esisteva | 🚨 **€250/m: P(20a) 92% → 14-26%** | | 37 | GATE-C | l'ultima gamba del gate funded + il biglietto come opzione | **PASS 2/2** — gate senza date; **P(≥50/g) 4,4% → 2,6%** | +| 38 | SCALE-SPEC | specificare il knob di leva che non esiste | **LEAD** — specificata, **1 nodo irrisolto** sul disaster-SL | | 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | **SCARTATO** — il segno dipende da `<` contro `<=` | | 23 | BOCPD | un rilevatore di cambio di regime vero batte il miglior lookback COSTANTE? | **SCARTATO** — filone chiuso definitivamente | | 24 | CRITICO | cosa NON e' stato misurato, e quale singola misura mancante vale di piu' | **3 correzioni all'ondata**, 1 regola mia ritirata | @@ -1503,7 +1504,7 @@ l'hold-out di TP01. E' la **terza** dimensione, e l'agente la dichiara. massimo scelto su cui barare, quindi non c'e' un DSR da calcolare*. Il filone che il critico dell'ondata aveva indicato come **la misura di maggior valore mai fatta**: -il gradino di leva **1,00x → 1,25-1,50x** vale **14,7 → 11,6 anni** al capitale-rendita (~€300/mese di +il gradino di leva vale **14,7a → 12,9a a 1,25x** e **→ 11,6a a 1,50x** al capitale-rendita (⚠️ **correzione 23/08**: la prima stesura scriveva *"1,25-1,50x vale 14,7 → 11,6a, ~€300/mese"* **fondendo due gradini** — il ~€300/mese e' del **1,50x**; a **1,25x** l'equivalente e' **+€164/mese**, ricalcolato sulla tabella netta) (~€282/mese a 1,50x di versamenti equivalenti, a costo zero) ed era bloccato da **un solo parametro scelto a mano**: *"un giorno a −10% ogni anno"*. @@ -1853,3 +1854,65 @@ scommessa **perde la mediana 6×** ($132 contro $787) e **vince la media 12×**: atteso positivo che un conto da $654 puo' permettersi di giocare **una volta sola**. ⚠️ E il vantaggio poggia **interamente** sul drift di XS01 misurato su **13 gambe su 19**. **Smentitore che azzera tutto senza che il modello lo segnali:** una **morte-firm ≫10%/anno**. + +--- + +## 38 — SCALE-SPEC (la chiave di scala che non esiste, specificata) + +`docs/research/SPEC-scale-key.md` (38 KB) + `scripts/research/r0822e_scale_proto.py` (1,0 s, zero +rete, **zero scritture**). **~630 valutazioni, nessuna cella scelta** → niente DSR. +✅ **Produzione non toccata** (`git status` vuoto su `src/`, `config/`, `scripts/live/`, `tests/`), e +la sez. 0 del prototipo **verifica l'isolamento a runtime** — *ed e' fallita su se stessa alla prima +stesura, che e' il modo giusto per un controllo di scoprire che funziona.* +✅ **Replica a k=1,00 superata 6/6** contro WORST-DAY (peggior giorno −3,95% il 2020-03-13, bound +strutturale −14,32%, morso del clamp 3/5.648 contro 3/5.650, drift e vol al centesimo). + +| | k=1,00 | **k=1,25** | +|---|---|---| +| Sharpe FULL | 1,5051 | **1,5051** (invariante) | +| drift / vol | 17,11% / 11,37% | 21,39% / 14,21% | +| g(k) esatto (CAGR geom.) | 17,90% | **22,61%** | +| maxDD storico | −12,17% | −15,01% | +| peggior giorno possibile | −14,32% | −17,90% | +| **costo di un disaster-SL** | 30,0% | **37,5%** ← la guardia che morde | +| margine di liquidazione | 15,8x | 10,1x | + +⚠️ **CORREZIONE A UN NUMERO CHE AVEVO SCRITTO IO** (nel briefing e in CLAUDE.md): *"il gradino +1,00x→1,25-1,50x vale 14,7 → 11,6 anni, ~€300/mese"* **fondeva due gradini**. §2 da' **1,25x → 12,9a** +e **1,50x → 11,6a**; il ~€300/mese e' del **1,50x**. Ricalcolato sulla tabella netta: **1,25x = ++€164/mese equivalenti** (l'interpolazione riproduce €282 per 1,50x = controllo superato). + +**Tre risultati che il mandato non prevedeva:** +1. 🚨 **`test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` NON si rompe — ed e' PEGGIO che + rompersi.** Misura `frac · n_asset` mentre la grandezza vera diventerebbe `frac · n_asset · scala` + → **continua a passare e smette di controllare**. **4ª occorrenza della forma "un controllo puntato + su una configurazione diversa da quella che gira"** (dopo `fee_watch` sugli inverse, la taratura di + `venue_watch` con bitfinex, i test di `book_live` senza potenza a libro flat). **Va cancellato con + nota, non aggiustato.** +2. 🚨 **La guardia che morde per prima e' il DISASTER-SL, non il peggior giorno:** dal bound + strutturale k ≤ 3,49x, **dal disaster-SL k ≤ 1,67x** = **2,1× piu' stringente**. **C1 di WORST-DAY + guardava la grandezza sbagliata.** → il tetto giusto e' l'invariante + `n_asset · frac · scala · disaster_sl_pct ≤ 0,50`, che lega **tre** chiavi. +3. 🚨 **Il tetto va sul PRODOTTO, non sulla chiave:** `frac=0,625` + `scala=1,25` fa leva **1,562x** e + **passerebbe** un tetto *"scala ≤ 1,25"*. Ed e' concreto: WORST-DAY ha appena pubblicato che alzare + `frac` da solo *"non produce k=1,25"* — **combinato con la scala lo produce eccome**. + +**Gate proposto `GATE SCALA-01`** — 9 condizioni, fra cui **A2** (180 giorni sul gradino precedente), +**A5** (tetto sul **prodotto**), **A6 anti-recency** (`k_ammesso` calcolato **togliendo gli ultimi 90 +giorni** dev'essere ≥ k richiesto) e **A9** (11 test verdi), piu' un sorvegliante giornaliero a **tre +stati**. Nasce dal fatto misurato che **la scala e' invisibile a tutti i gate esistenti**: Sharpe +**1,5051 identico** a tutte e 5 le celle di k → `deflated_sharpe` e `marginal_vs_tp01` **non +falliscono, non vedono la variabile**. ✅ `weights_tilt_null` invece **passa e con potenza**: peso +implicito TP01 **0,750000 a ogni k**, `max|target(k) − k·target(1)| = 0,00e+00`, e il **controllo +positivo** (scala sulla sola gamba TP01 = cio' che farebbe `target_vol`) lo porta a **0,789** e viene +smascherato. + +**VERDETTO: LEAD** (specifica di processo). +**`LA CHIAVE DI SCALA: SPECIFICATA MA CON UN NODO IRRISOLTO`** — `execution.py:196-223` **ri-ancora +il disaster-SL al mark a ogni giro** (tolleranza 5%): il **−30% e' ROTOLANTE, non un pavimento**. +Se e' cosi', il 37,5% e' il costo di **un episodio** e un ribasso lento ne produce **piu' d'uno**, e +il margine 1,33x del gradino non e' quello vero. ⚠️ **Letto nel codice, NON misurato** — e il log non +lo permette (servirebbe `pos_prima`): **non ricostruibile a posteriori.** +**Secondo smentitore, con soglia dichiarata:** il ramo di **fallback dell'equity** oggi scatta +**0 volte su 1.442 giri**; se superasse il **2% dei giri in 90 giorni** il libro girerebbe a leva +**mista** e `g(k)` smetterebbe di descriverlo. diff --git a/docs/research/SPEC-scale-key.md b/docs/research/SPEC-scale-key.md new file mode 100644 index 0000000..b9fc593 --- /dev/null +++ b/docs/research/SPEC-scale-key.md @@ -0,0 +1,618 @@ +# SPEC — la CHIAVE DI SCALA del libro live (`book_scale_k`) + +**Filone SCALE-SPEC, ondata 2026-08-22, branch `research/wave-0822`.** +**Questo documento e' una SPECIFICA. Non e' stata implementata: `src/`, `config/`, `scripts/live/`, +`scripts/cron_*.sh` e `tests/` non sono stati toccati.** 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: + +1. **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); +2. **il tetto sta sul PRODOTTO, non sulla chiave** (`frac` e `scala` moltiplicano: un tetto sulla + sola chiave lascia aperta la porta accanto — vedi §4.2, tabella); +3. **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 + +```python +# 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: + +```python +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(...)`: + +```python +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 su + `k·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 = k` esattamente, 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: + +1. resta **proporzionale all'equity** (`cap = equity·frac` sul percorso fidato) → un deposito non + strozza il libro e un prelievo lo rimpicciolisce da solo; +2. 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`: + +```python +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`: asserisce + `len(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_sensitivity` va **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** + +```python +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 + +```python +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.** + +```python +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| == 0.0`) | la modifica cambia qualcosa **prima** che qualcuno decida di cambiarlo | +| **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:** +1. `k_ammesso` del 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 di + `r0822c_worst_day`. +2. **Oggi A6 non morde**: `k_ammesso ≈ 3,5x` contro 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'. +3. 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) + +1. Scrivere `tests/test_book_scale.py` con T1–T11 (§5). **Prima.** T7 deve passare **contro il + codice attuale** (la chiave non esiste ancora → default 1,0 → bit-exact). +2. Implementare §2.2 (`scala` in `book_net_target`, `_scala()`, `LEVA_LORDA_MAX`, `SCALA_LADDER`, + `scala` nel report e nel print di `book_execute`). **Senza toccare `config/live.json`**: la chiave + assente vale 1,0, il libro non cambia, i test lo dimostrano. +3. Cancellare `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` **con la nota** (§3.4). +4. Scrivere `scale_watch` (§6.4) + `data/live/scale_history.jsonl` con la riga iniziale + `{"scala": 1.00, "dal": "...", "gate": "iniziale, nessun gradino"}`, e aggiungerlo a + `cron_daily.sh` **e** al backup di `/opt/docker/scripts/backup.sh` (il giornale non e' ricostruibile). +5. **Lasciare girare a 1,00 per almeno 30 giorni** con il sorvegliante attivo. Una manopola nuova si + guarda funzionare **prima** di girarla. +6. Solo allora: eseguire `GATE SCALA-01` (§6.2). Se PASSA, una riga di config + una di giornale. +7. Rifare `r0726_fee_sensitivity` al 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 + +1. **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, `B` e' 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: aggiungere `pos_prima` al log di + `ensure_disaster_sl` e ri-misurare — **non ricostruibile a posteriori**. +2. **`LEVA_LORDA_MAX = 1.25` e' 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**. +3. **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)` e `k_ammesso` + **lo sono** — e vanno lette con la banda del libro (Sharpe FULL de-luckato 1,95, banda [1,81; 2,12]). +4. **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.* diff --git a/scripts/research/r0822e_scale_proto.py b/scripts/research/r0822e_scale_proto.py new file mode 100644 index 0000000..02a76ca --- /dev/null +++ b/scripts/research/r0822e_scale_proto.py @@ -0,0 +1,698 @@ +"""r0822e — PROTOTIPO ISOLATO della CHIAVE DI SCALA del libro live (filone SCALE-SPEC). + + ⚠️ QUESTO FILE NON E' PRODUZIONE E NON E' IMPORTATO DA NIENTE IN PRODUZIONE. + Non scrive nulla: ne' config/, ne' data/, ne' src/. Non manda ordini. Non tocca il conto. + La sezione (0) VERIFICA entrambe le cose a runtime invece di dichiararle: + - nessun file fuori da scripts/research/ nomina questo modulo; + - il sorgente di questo file non contiene una sola primitiva di scrittura. + Documento di specifica che accompagna questo prototipo: docs/research/SPEC-scale-key.md + +COSA DIMOSTRA + Il filone WORST-DAY (r0822c) ha misurato che il gradino di leva 1,00x -> 1,25x vale + 14,7 -> 11,6 anni al capitale-rendita, che C1-C5 passano, e che **C6 (processo) FALLISCE**: + `config/live.json` non ha una chiave di scala e il cap e' un CLAMP, non un moltiplicatore + (morde su 3 osservazioni-asset su 5.650). Questo prototipo mostra la MECCANICA della chiave + che manca, senza costruirla in produzione: + + (1) SANITY la copia della funzione a scala 1.0 e' BIT-EXACT alla produzione + (2) DOVE la catena segnale -> target -> ordine, letta dal sorgente vero con le righe + (3) SIMMETRIA la scala e' identica sui due sleeve (e il controllo positivo la smaschera) + (4) ORDINE scala PRIMA o DOPO il clamp: i due ordini danno k_eff diversi, misurato + (5) REPLICA i numeri a k=1,00x coincidono con quelli pubblicati da WORST-DAY + (6) GUARDIE tetto composito, disaster-SL, liquidazione: quale guardia MORDE per prima + (7) ANTI-CREEP il gate mancante: la finestra recente non deve poter autorizzare il gradino + + uv run python scripts/research/r0822e_scale_proto.py +""" +from __future__ import annotations + +import json +import re +import subprocess +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 book_series (lente LIVE del libro) + +from src.data.downloader import load_data # noqa: E402 +from src.live.book import ( # noqa: E402 SOLA LETTURA (nessuna di queste scrive) + CAP_UNKNOWN_USD, W_SKH, W_TP01, _cap, _read_watermark, book_net_target) +from src.live.shadow import WEIGHT # noqa: E402 0.5 = split BTC/ETH +from src.strategies.skyhook import SKH01_V2_DD # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, realized_vol, resample_1d, simple_returns) + +ASSETS = ("BTC", "ETH") +ANN = 365.0 +DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor) +MM_RATE = 0.01 # maintenance margin perp lineari Deribit (valore prudente) +WIPE = 0.90 + +# ---- i numeri PUBBLICATI da WORST-DAY (sez. 33 di RESULTS-0822), per il confronto di replica --- +PUB = { + "worst_close": -0.0394, # peggior giorno in chiusura, lente LIVE + "worst_date": "2020-03-13", + "worst_min": -0.0733, # peggior minimo intra-giorno (lente accoppiata) + "L_struct": 0.1432, # peggior giorno POSSIBILE a k=1,00x, in chiusura + "cap_bite_n": 3, # osservazioni-asset in cui il clamp morde + "cap_bite_tot": 5650, + "drift_deluck": 0.1711, # r0822_growth_policy + "vol": 0.1137, + "sharpe_deluck": 1.51, +} + +# ---- la PROPOSTA che questo prototipo specifica ------------------------------------------ +SCALA_PROPOSTA = 1.25 +LEVA_LORDA_MAX = 1.25 # tetto sul PRODOTTO n_asset * frac * scala (costante di CODICE) +MEZZO_EQUITY = 0.50 # C1: il peggior giorno possibile deve lasciare meta' equity +GRIGLIA_K = (1.00, 1.10, 1.25, 1.50, 2.00) + + +def sez(t: str) -> None: + print("\n" + "=" * 108) + print(f" {t}") + print("=" * 108) + + +# ========================================================================================== +# LA COPIA — l'unica differenza rispetto a src/live/book.book_net_target e' `scala` +# ========================================================================================== +def book_net_target_scalato(tp_frac: float, skh_sign: int, equity: float, cap: float, + weight: float = WEIGHT, scala: float = 1.0, + scala_prima_del_clamp: bool = False) -> float: + """COPIA di src/live/book.book_net_target con la chiave di scala. + + `scala_prima_del_clamp` esiste SOLO per misurare l'ordine sbagliato nella sezione (4): + la specifica propone l'ordine DOPO (scala * clamp(raw)), non PRIMA. + """ + raw = weight * equity * (W_TP01 * max(tp_frac, 0.0) + W_SKH * float(skh_sign)) + if scala_prima_del_clamp: + raw = float(scala) * raw + return max(-cap, min(cap, raw)) + clamped = max(-cap, min(cap, raw)) + return float(scala) * clamped + + +def book_net_target_ASIMMETRICO(tp_frac: float, skh_sign: int, equity: float, cap: float, + weight: float = WEIGHT, scala: float = 1.0) -> float: + """CONTROLLO POSITIVO (deve essere SMASCHERATO dal test di simmetria): la scala applicata + alla sola gamba TP01. E' esattamente cio' che fa `target_vol`, ed e' un TILT DI PESI.""" + raw = weight * equity * (W_TP01 * float(scala) * max(tp_frac, 0.0) + W_SKH * float(skh_sign)) + return float(scala) * 0.0 + max(-cap, min(cap, raw)) + + +# ========================================================================================== +# (0) ISOLAMENTO — verificato, non dichiarato +# ========================================================================================== +def sezione_0_isolamento() -> None: + sez("(0) ISOLAMENTO — questo file non e' produzione, e la verifica e' qui dentro") + slug = Path(__file__).stem + try: + out = subprocess.run(["grep", "-rl", slug, str(ROOT / "src"), str(ROOT / "scripts"), + str(ROOT / "tests"), str(ROOT / "config")], + capture_output=True, text=True, timeout=60).stdout.split() + except Exception as e: # grep assente: si dichiara, non si finge + out = [] + print(f" ⚠️ grep non disponibile ({type(e).__name__}): controllo non eseguito") + fuori = [p for p in out if not p.endswith(f"scripts/research/{slug}.py")] + print(f" file che nominano `{slug}` fuori da se stesso : {len(fuori)} {fuori}") + assert not fuori, "QUALCUNO IMPORTA IL PROTOTIPO: non e' piu' isolato" + + src = Path(__file__).read_text() + # I nomi delle primitive vietate sono COMPOSTI a runtime: scriverli per esteso qui li + # farebbe trovare dal controllo stesso (primo tentativo: il file falliva su se' stesso). + vietate = tuple("".join(p) for p in (("write", "_text("), ("to_", "parquet("), ("to_", "csv("), + ("json.", "dump("), ("os.", "remove"), ("shu", "til."), + ("mk", "dir("), ("os.", "rename"))) + scritture = [m for m in vietate if m in src] + aperture_w = re.findall(r"open\([^)]*['\"][wa]", src) + print(f" primitive di scrittura nel sorgente : {scritture or 'nessuna'}") + print(f" open() in scrittura/append : {len(aperture_w)}") + assert not scritture and not aperture_w, "il prototipo scrive: non e' piu' innocuo" + print(" -> il prototipo LEGGE il codice di produzione e non lo tocca. Il cron non lo vede.") + + +# ========================================================================================== +# (1) SANITY — la copia e' la produzione +# ========================================================================================== +def sezione_1_sanity() -> None: + sez("(1) SANITY — a scala 1,00 la copia e' BIT-EXACT alla funzione di produzione") + dif = 0.0 + n = 0 + for eq in (600.0, 2_000.0, 6_047.0, 50_000.0): + for cap in (300.0, eq * 0.5, eq * 0.25, 1e9): + for tp in (0.0, 0.13, 1 / 3, 0.5, 2 / 3, 0.87, 1.0, 1.4, 2.0, -1.0): + for sg in (-1, 0, 1): + a = book_net_target(tp, sg, eq, cap) + b = book_net_target_scalato(tp, sg, eq, cap, scala=1.0) + dif = max(dif, abs(a - b)) + n += 1 + print(f" {n} combinazioni (equity x cap x tp_frac x segno) · max|copia - produzione| = {dif:.2e}") + assert dif == 0.0, "la copia NON e' la produzione: ogni numero sotto e' senza valore" + print(" -> tutto cio' che segue e' una proprieta' della FUNZIONE DI PRODUZIONE, non di una sua imitazione.") + + +# ========================================================================================== +# (2) DOVE VA LA CHIAVE — la catena letta dal sorgente vero +# ========================================================================================== +def _righe(path: Path, pattern: str) -> list[tuple[int, str]]: + out = [] + for i, ln in enumerate(path.read_text().splitlines(), 1): + if re.search(pattern, ln): + out.append((i, ln.rstrip())) + return out + + +def sezione_2_dove() -> None: + sez("(2) DOVE — la catena segnale -> target -> ordine, letta da src/live/ con le righe vere") + book = ROOT / "src" / "live" / "book.py" + shadow = ROOT / "src" / "live" / "shadow.py" + exe = ROOT / "scripts" / "live" / "book_execute.py" + for titolo, path, pat in ( + ("segnale TP01 (frazione long-flat, causale)", shadow, r"current_target"), + ("segnale SKH01 (solo il SEGNO: nessuna taglia)", book, r"_skh_sign|_skyhook_positions\(load5m"), + ("cap per-asset (clamp, watermark-bound)", book, r"^def _cap|cap = _cap\("), + ("TARGET NETTO <== QUI VA LA SCALA", book, r"^def book_net_target|net = book_net_target\("), + ("ordine (delta verso il target)", book, r"^def build_book_order|order = build_book_order\("), + ("invio (porta il conto al target)", exe, r"rebalance_signed|ensure_disaster_sl\(inst"), + ): + print(f"\n {titolo}") + for i, ln in _righe(path, pat)[:3]: + print(f" {path.relative_to(ROOT)}:{i:>4} {ln.strip()[:92]}") + print(f""" + LETTURA. `book_net_target` e' l'UNICO punto in cui le due gambe si incontrano con una + TAGLIA: sopra di lei TP01 da' una frazione e SKH01 da' un SEGNO (+1/-1/0, nessuna + grandezza); sotto di lei `build_book_order` calcola solo un DELTA verso il target. + Percio' e' l'unico punto in cui un moltiplicatore e' un puro cambio di scala: + · su TP01 (`target_vol`) -> scala il 75% del libro e lascia fermo il 25% = TILT DI PESI + · su SKH01 -> non c'e' niente da scalare: e' un segno + · su `WEIGHT` (0.5) -> e' il SPLIT BTC/ETH, ed e' condiviso con shadow_report/dashboard + (percorso TP01-only): cambiarlo cambierebbe un ALTRO libro + · su `W_TP01`/`W_SKH` -> sono il RAPPORTO 75/25 e sono legati per test ai pesi del + backtest (`deribit_book_sleeves`): moltiplicarli entrambi + romperebbe quella parita' e confonderebbe scala e mix + · su `build_book_order` -> scalerebbe il DELTA, non il target: cambia la VELOCITA' di + convergenza, non la taglia, e il reconciler litigherebbe con se' + · dentro l'esecutore -> il report e il log stamperebbero un target diverso da quello + eseguito (il difetto che questo progetto ha gia' pagato 3 volte)""") + + +# ========================================================================================== +# (3) SIMMETRIA +# ========================================================================================== +def sezione_3_simmetria() -> None: + sez("(3) SIMMETRIA — la scala e' identica sui due sleeve, e il controllo positivo lo prova") + eq, cap = 6_047.0, 1e12 # cap disattivato: qui si guarda SOLO la formula + print(f" {'k':>6}{'solo TP01 (tp=1,sg=0)':>24}{'solo SKH01 (tp=0,sg=1)':>25}" + f"{'rapporto TP/SKH':>18}{'peso implicito TP01':>21}") + base_ratio = None + for k in GRIGLIA_K: + t = book_net_target_scalato(1.0, 0, eq, cap, scala=k) + s = book_net_target_scalato(0.0, 1, eq, cap, scala=k) + rap = t / s + w = t / (t + s) + base_ratio = rap if base_ratio is None else base_ratio + print(f" {k:>6.2f}{t:>24,.2f}{s:>25,.2f}{rap:>18.6f}{w:>21.6f}") + assert abs(rap - W_TP01 / W_SKH) < 1e-12, "la scala ha cambiato il MIX: e' un tilt" + print(f" -> rapporto costante = W_TP01/W_SKH = {W_TP01/W_SKH:.4f} a OGNI k. La scala non e' un tilt.") + + print("\n proprieta' forte (quella che il test deve asserire): target(k) == k * target(1)") + peggio = 0.0 + for k in GRIGLIA_K: + for tp in (0.0, 1 / 3, 0.5, 2 / 3, 1.0, 1.35): + for sg in (-1, 0, 1): + a = book_net_target_scalato(tp, sg, eq, cap, scala=k) + b = k * book_net_target_scalato(tp, sg, eq, cap, scala=1.0) + peggio = max(peggio, abs(a - b)) + print(f" max|target(k) - k*target(1)| su {len(GRIGLIA_K)*6*3} combinazioni (cap disattivato) = {peggio:.2e}") + assert peggio == 0.0 + + print("\n CONTROLLO POSITIVO — la stessa proprieta' su un'implementazione ASIMMETRICA") + print(" (scala sulla sola gamba TP01 = cio' che farebbe `target_vol`):") + print(f" {'k':>6}{'solo TP01':>16}{'solo SKH01':>14}{'rapporto':>12}{'peso TP01':>12} esito del test") + smascherato = False + for k in (1.00, 1.25, 1.50): + t = book_net_target_ASIMMETRICO(1.0, 0, eq, cap, scala=k) + s = book_net_target_ASIMMETRICO(0.0, 1, eq, cap, scala=k) + ok = abs(t / s - W_TP01 / W_SKH) < 1e-12 + smascherato |= (not ok) + print(f" {k:>6.2f}{t:>16,.0f}{s:>14,.0f}{t/s:>12.4f}{t/(t+s):>12.6f} " + f"{'passa (k=1: non c e nulla da vedere)' if ok else 'FALLISCE <- smascherato'}") + assert smascherato, "il test di simmetria NON smaschera l'asimmetria: non sta controllando niente" + print(" -> a k=1 le due implementazioni COINCIDONO: un test che gira solo a k=1 ha potenza ZERO.") + print(" Il test di simmetria deve girare su una GRIGLIA di k, o non e' un test.") + + +# ========================================================================================== +# dati storici: peso TENUTO da TP01 per asset (stessa costruzione di r0822c gambe_tp01) +# ========================================================================================== +def pesi_tp01() -> dict: + out = {} + tv = CANONICAL["target_vol"] + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + c = df["close"].values.astype(float) + r = simple_returns(c) + tgt = np.nan_to_num(np.asarray(TrendPortfolio(**CANONICAL).target_series(df), float)) + held = np.zeros(len(tgt)) + held[1:] = tgt[:-1] # cio' che il libro TIENE il giorno d + vol = realized_vol(r, CANONICAL["vol_win_days"], 365.25) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + out[a] = pd.DataFrame({"held": held, "ret": r, "vol_prev": np.r_[np.nan, vol[:-1]]}, + index=idx).iloc[1:] + out[a] = out[a].assign(z=out[a]["ret"] / (out[a]["vol_prev"] / np.sqrt(365.25))) + out[a].attrs["tv"] = tv + return out + + +# ========================================================================================== +# (4) L'ORDINE COL CAP +# ========================================================================================== +def sezione_4_ordine(TPQ: dict, frac: float) -> None: + sez("(4) L'ORDINE COL CAP — il punto sottile: scala PRIMA o DOPO il clamp") + print(f""" + Il cap e' un CLAMP: net = clamp(0.5*E*(0.75*tp + 0.25*sg), ±cap), cap = frac*E = {frac:.2f}*E. + A piena convinzione (tp=1, sg=+1) il grezzo vale ESATTAMENTE 0.5*E*1.0 = {0.5:.2f}*E = cap. + Quindi il clamp e' *tangente* al libro proprio nei giorni di massima esposizione.""") + E = 1.0 # tutto in frazione di equity + cap = frac * E + print(f"\n {'k':>6}{'ordine':>10}{'giorni-asset':>14}{'clampati':>11}{'quota':>9}" + f"{'k_eff medio':>13}{'k_eff a piena convinz.':>24}{'lordo max (x E)':>17}") + for k in GRIGLIA_K: + for prima in (True, False): + tot = n_cl = 0 + num = den = 0.0 + for a in ASSETS: + h = np.clip(TPQ[a]["held"].values, 0.0, None) + for sg in (1,): # SKH long: il caso che spinge di piu' + base = np.array([book_net_target_scalato(x, sg, E, cap, scala=1.0) for x in h]) + got = np.array([book_net_target_scalato(x, sg, E, cap, scala=k, + scala_prima_del_clamp=prima) for x in h]) + m = np.abs(base) > 1e-12 + num += float(np.sum(got[m])) + den += float(np.sum(base[m])) + n_cl += int(np.sum(np.abs(got) < np.abs(k * base) - 1e-12)) + tot += int(m.sum()) + piena = book_net_target_scalato(1.0, 1, E, cap, scala=k, scala_prima_del_clamp=prima) / \ + book_net_target_scalato(1.0, 1, E, cap, scala=1.0) + lordo = len(ASSETS) * abs(book_net_target_scalato(1.0, 1, E, cap, scala=k, + scala_prima_del_clamp=prima)) + print(f" {k:>6.2f}{'PRIMA' if prima else 'DOPO':>10}{tot:>14}{n_cl:>11}{n_cl/tot:>9.2%}" + f"{num/den:>13.4f}{piena:>24.4f}{lordo:>17.3f}") + print(f""" + LETTURA — l'ordine non e' un dettaglio, e' la differenza fra una scala e un'altra strategia. + + · SCALA PRIMA DEL CLAMP: il clamp la mangia proprio nei giorni di massima convinzione + (k_eff a piena convinzione = 1,00 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 — la + curva g(k) di growth_policy/WORST-DAY NON descrive quel libro, perche' quella curva e' + calcolata su k * r, cioe' sull'ordine DOPO. + · SCALA DOPO IL CLAMP: k_eff = k esattamente, 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. + + ⚠️ IL PREZZO, e va detto perche' e' il cuore della specifica: con l'ordine DOPO il cap + SMETTE di essere il tetto assoluto del nozionale. Il lordo massimo diventa + n_asset * frac * k = {len(ASSETS)} * {frac:.2f} * k, cioe' {len(ASSETS)*frac*SCALA_PROPOSTA:.3f}x l'equity a k={SCALA_PROPOSTA:.2f}. + E' ESATTAMENTE la quantita' che `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` + (tests/test_fee_sensitivity.py) asserisce essere <= 1x. Quel test DEVE rompersi: sta + misurando `frac * n_asset` mentre la grandezza vera diventa `frac * n_asset * scala`.""") + + +# ========================================================================================== +# (5) REPLICA DEI NUMERI DI WORST-DAY a k = 1,00 +# ========================================================================================== +def sezione_5_replica(TPQ: dict, frac: float) -> dict: + sez("(5) REPLICA — a k=1,00 i numeri devono coincidere con quelli pubblicati da WORST-DAY") + book = CC.book_series("hourly") + r = book.values.astype(float) + r_del = r - (1.0 - DELUCK) * float(r.mean()) + m = { + "n": len(r), + "drift_deluck": float(r_del.mean()) * ANN, + "vol": float(r_del.std(ddof=0)) * np.sqrt(ANN), + "sharpe_deluck": float(r_del.mean() / r_del.std(ddof=0)) * np.sqrt(ANN), + "worst_close": float(r_del.min()), + "worst_date": str(book.index[int(np.argmin(r_del))].date()), + } + print(f"\n serie del libro LIVE (`book_series('hourly')`), {m['n']} giorni " + f"{book.index.min().date()} -> {book.index.max().date()}") + print(f" {'grandezza':>26}{'qui':>12}{'pubblicato':>13}{'scarto':>10}") + for nome, ch, pu, fmt in (("drift de-luckato x0.89", "drift_deluck", PUB["drift_deluck"], "%"), + ("vol annua", "vol", PUB["vol"], "%"), + ("Sharpe de-luckato", "sharpe_deluck", PUB["sharpe_deluck"], "f"), + ("peggior giorno (chiusura)", "worst_close", PUB["worst_close"], "%")): + v = m[ch] + s = f"{v:>12.2%}{pu:>13.2%}{v-pu:>10.2%}" if fmt == "%" else f"{v:>12.3f}{pu:>13.3f}{v-pu:>10.3f}" + print(f" {nome:>26}{s}") + print(f" {'data del peggior giorno':>26}{m['worst_date']:>12}{PUB['worst_date']:>13}" + f"{' coincide' if m['worst_date'] == PUB['worst_date'] else ' DIVERSA':>10}") + assert m["worst_date"] == PUB["worst_date"] + assert abs(m["worst_close"] - PUB["worst_close"]) < 5e-4 + + # --- il clamp morde su 3 osservazioni-asset su 5.650 + n_cap = sum(int(((0.5 * W_TP01 * TPQ[a]["held"].values + 0.5 * W_SKH) > frac).sum()) for a in ASSETS) + n_tot = sum(len(TPQ[a]) for a in ASSETS) + print(f"\n il CLAMP a frac={frac:.2f} morde su {n_cap} osservazioni-asset su {n_tot} " + f"({n_cap/n_tot:.3%}) [pubblicato: {PUB['cap_bite_n']}/{PUB['cap_bite_tot']}]") + for f2 in (0.50, 0.5625, 0.625, 0.75): + nn = sum(int(((0.5 * W_TP01 * TPQ[a]["held"].values + 0.5 * W_SKH) > f2).sum()) for a in ASSETS) + print(f" frac={f2:.4f} -> morde {nn:>3} volte · leva lorda MASSIMA {2*f2:.3f}x · " + f"leva EFFETTIVA mediana {np.median(np.concatenate([np.minimum(0.5*W_TP01*TPQ[a]['held'].values+0.5*W_SKH, f2)*2 for a in ASSETS])):.3f}x") + print(" -> alzare `frac` NON alza la leva: alza un tetto che non tocca quasi mai il libro.") + + # --- il bound strutturale a k = 1 + tv = CANONICAL["target_vol"] + sl_l = SKH01_V2_DD.sl_pct + ex_giu = {} + for a in ASSETS: + d = load_data(a, "1h") + ex_giu[a] = float((d["close"].values / d["open"].values - 1.0).min()) + lim_tp, lim_sk = {}, {} + for a in ASSETS: + d = TPQ[a].dropna(subset=["z"]) + dv = d[d["vol_prev"] >= tv] + z = float(dv["z"].min()) + lim_tp[a] = 0.5 * W_TP01 * tv * abs(z) / np.sqrt(365.25) + lim_sk[a] = 0.5 * W_SKH * (sl_l + abs(ex_giu[a])) + L = sum(lim_tp[a] + lim_sk[a] for a in ASSETS) + print(f"\n bound STRUTTURALE (scenario crash: TP01 a piena convinzione + SKH01 long stoppato)") + for a in ASSETS: + print(f" {a}: TP01 {-lim_tp[a]:>7.2%} SKH01 {-lim_sk[a]:>7.2%} -> {-(lim_tp[a]+lim_sk[a]):>7.2%}") + print(f" TOTALE a k=1,00x {-L:>7.2%} [pubblicato: {-PUB['L_struct']:.2%}]") + assert abs(L - PUB["L_struct"]) < 5e-3, "il bound strutturale non si replica" + print(" -> replica superata: la sezione (6) puo' scalare QUESTI numeri e non dei numeri copiati.") + return {"r_del": r_del, "L": L, "book": book} + + +# ========================================================================================== +# (6) LE GUARDIE — quale morde per prima +# ========================================================================================== +def sezione_6_guardie(rep: dict, TPQ: dict, frac: float, sl_pct: float) -> None: + sez("(6) GUARDIE — il tetto e' sul PRODOTTO, e la guardia che morde per prima non e' quella attesa") + L = rep["L"] + g_max = sum(0.5 * W_TP01 * TPQ[a]["held"].max() + 0.5 * W_SKH for a in ASSETS) + m_min = PUB["worst_min"] # peggior minimo intra-giorno misurato (accoppiato) + print(f""" + Tre grandezze scalano LINEARMENTE con k e vanno lette insieme (sl disaster = {sl_pct:.0%}): + A) peggior giorno POSSIBILE in chiusura : k * {L:.2%} + B) costo in equity se scatta il disaster-SL : k * n_asset * frac * {sl_pct:.0%} + (il -{sl_pct:.0%} e' un movimento di PREZZO sul mark; l'impatto in equity e' quel + movimento moltiplicato per il LORDO, quindi cresce con k anche se sl_pct non cambia) + C) distanza dalla liquidazione sul MINIMO : margine = x_liq / (|min| * k) +""") + print(f" {'k':>6}{'lordo max':>11}{'A) peggior giorno':>19}{'B) costo disaster-SL':>22}" + f"{'C) x_liq':>10}{'min intra k':>13}{'margine C':>11} verdetto") + for k in GRIGLIA_K: + G = min(g_max * k, len(ASSETS) * frac * k) + x_liq = (1 - MM_RATE * G) / (G * (1 - MM_RATE)) + A = k * L + B = k * len(ASSETS) * frac * sl_pct + mk = abs(m_min) * k + margine = x_liq / mk + v = [] + if A > MEZZO_EQUITY: + v.append("A sfonda meta' equity") + if B > MEZZO_EQUITY: + v.append("B sfonda meta' equity") + if margine < 3.0: + v.append("C sotto 3x") + if k * len(ASSETS) * frac > LEVA_LORDA_MAX: + v.append(f"oltre il tetto {LEVA_LORDA_MAX:.2f}x") + print(f" {k:>6.2f}{G:>11.3f}{A:>19.2%}{B:>22.2%}{x_liq:>10.1%}{mk:>13.2%}" + f"{margine:>11.1f}x {'; '.join(v) if v else 'ok'}") + + k_da_A = MEZZO_EQUITY / L + k_da_B = MEZZO_EQUITY / (len(ASSETS) * frac * sl_pct) + print(f""" + 📌 IL RISULTATO CHE WORST-DAY NON AVEVA CALCOLATO: la guardia che morde per prima NON e' + il peggior giorno possibile, e' il DISASTER-SL. + · dal peggior giorno (A): k <= {MEZZO_EQUITY:.0%} / {L:.2%} = {k_da_A:.2f}x + · dal disaster-SL (B): k <= {MEZZO_EQUITY:.0%} / ({len(ASSETS)} * {frac:.2f} * {sl_pct:.0%}) = {k_da_B:.2f}x + Il disaster-SL e' {k_da_A/k_da_B:.1f}x piu' stringente. A k={SCALA_PROPOSTA:.2f} il margine di (A) e' + {MEZZO_EQUITY/(SCALA_PROPOSTA*L):.1f}x mentre quello di (B) e' solo {MEZZO_EQUITY/(SCALA_PROPOSTA*len(ASSETS)*frac*sl_pct):.2f}x. + -> il tetto di codice non puo' essere un numero solo: deve essere l'INVARIANTE + `n_asset * frac * scala * disaster_sl_pct <= {MEZZO_EQUITY:.2f}`, che lega TRE chiavi. + Con sl_pct={sl_pct:.0%} e frac={frac:.2f} quell'invariante da' scala <= {k_da_B:.2f}x. + + ⚠️ E il buco che un tetto sulla SOLA chiave lascerebbe aperto: `frac` e `scala` MOLTIPLICANO. + Un tetto "scala <= {SCALA_PROPOSTA:.2f}" e' rispettato anche da frac=0.75 + scala={SCALA_PROPOSTA:.2f} + = leva lorda {2*0.75*SCALA_PROPOSTA:.3f}x. Il tetto va messo sul PRODOTTO.""") + print(f"\n {'frac':>8}{'scala':>8}{'prodotto = leva lorda':>24}{'tetto scala-only':>18}{'tetto sul PRODOTTO':>21}") + for f2, k in ((0.50, 1.00), (0.50, 1.25), (0.50, 1.50), (0.625, 1.25), (0.75, 1.25), (0.50, 2.00)): + p = len(ASSETS) * f2 * k + print(f" {f2:>8.3f}{k:>8.2f}{p:>24.3f}{'passa' if k <= SCALA_PROPOSTA else 'BLOCCA':>18}" + f"{'passa' if p <= LEVA_LORDA_MAX else 'BLOCCA':>21}") + + +# ========================================================================================== +# (6-bis) GLI STATI DELL'EQUITY — la scala non deve rendere il FALLBACK piu' permissivo +# ========================================================================================== +def cap_copia(equity, real_equity, eq_fallback, fixed, frac, wm): + """COPIA di src/live/book._cap (verificata sotto contro l'originale). Riprodotta qui perche' + l'originale legge il watermark REALE da disco e questo prototipo non deve dipenderne.""" + if frac is None: + return fixed + trusted = (real_equity is not None) and (not eq_fallback) and (equity is not None) and (equity > 0) + if trusted: + return float(equity) * float(frac) + return min(fixed, wm * float(frac)) if wm is not None else min(fixed, CAP_UNKNOWN_USD) + + +def sezione_6bis_equity(frac: float, fixed: float) -> None: + sez("(6-bis) GLI STATI DELL'EQUITY — la regola proposta: la scala vive SOLO sul percorso fidato") + wm = _read_watermark() + print(f" watermark reale sul disco (letto, non scritto): " + f"{'$%.2f' % wm if wm is not None else 'assente'}") + + # la copia e' la produzione, verificata sui due rami che non dipendono dal file + d = max(abs(cap_copia(6047.0, 6047.0, None, fixed, frac, wm) - _cap(6047.0, 6047.0, None)), + abs(cap_copia(2000.0, None, "fallback", fixed, frac, wm) - _cap(2000.0, None, "fallback"))) + print(f" max|copia del cap - produzione| sui due rami reali = {d:.2e}") + assert d == 0.0, "la copia del cap non e' la produzione" + + E_VERO = wm if wm is not None else 635.0 + print(f""" + Proposta: `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 il conto + non si sa quanto vale, non si prende. Cosi' il vincolo "il fallback non e' piu' permissivo" + e' vero PER COSTRUZIONE e non per aritmetica. + Colonna che conta: LORDO / equity VERA (${E_VERO:,.2f}), cioe' la leva davvero corsa.""") + scen = [ + ("normale (equity reale letta)", E_VERO, E_VERO, None, wm), + ("online ma equity illeggibile", 2000.0, None, "fallback", wm), + ("watermark assente (1o avvio)", 2000.0, None, "fallback", None), + ("watermark vecchio e basso", 2000.0, None, "fallback", 100.0), + ("dopo un deposito a $6.047", 6047.0, 6047.0, None, 6047.0), + ] + print(f"\n {'stato':>32}{'cap':>11}{'scala eff.':>12}{'lordo max':>12}" + f"{'/ equity vera':>15}{'tetto rispettato':>18}") + for nome, eq, real, fb, w in scen: + cap = cap_copia(eq, real, fb, fixed, frac, w) + k_eff = SCALA_PROPOSTA if (real is not None and not fb) else 1.0 + lordo = len(ASSETS) * cap * k_eff + # l'equity VERA e' quella del conto (nota: qui la si conosce), NON il watermark: un + # watermark vecchio e basso non rende il conto piccolo, rende il libro SOTTO-esposto. + ev = real if real is not None else E_VERO + lev = lordo / ev + print(f" {nome:>32}{cap:>11,.2f}{k_eff:>12.2f}{lordo:>12,.2f}{lev:>15.3f}x" + f"{'si' if lev <= LEVA_LORDA_MAX + 1e-9 else 'NO':>18}") + print(""" + -> in nessuno stato degradato la leva supera quella dello stato normale: il fallback resta + la configurazione PIU' PRUDENTE, come oggi. E il caso "watermark assente" resta a + CAP_UNKNOWN_USD ($300) con scala 1.00: due protezioni indipendenti, 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 ogni oscillazione. Costo: un delta di + (k-1)*frac*E per asset, cioe' ~$%.2f a testa sul conto di oggi, a 3,5 bps = $%.4f. + Frequenza MISURATA sui log (`logs/cron_book.log`): %s.""" % ( + (SCALA_PROPOSTA - 1) * frac * E_VERO, + (SCALA_PROPOSTA - 1) * frac * E_VERO * 0.00035 * 2, + _freq_fallback())) + + +def _freq_fallback() -> str: + """Quante volte il ramo 'equity fallback' e' stato preso davvero. Un prezzo dichiarato senza + la sua frequenza non e' un prezzo.""" + log = ROOT / "logs" / "cron_book.log" + if not log.exists(): + return "log assente, NON misurata" + try: + txt = log.read_text(errors="ignore") + except Exception: + return "log illeggibile, NON misurata" + giri = txt.count("BOOK DERIBIT LIVE EXECUTE") + fb = txt.count("EQUITY FALLBACK") + off = txt.count("conto non leggibile") + return (f"{fb} giri su {giri} ({fb/max(giri,1):.2%}); " + f"{off} giri sono invece OFFLINE (nessun ordine, la scala non si pone)") + + +# ========================================================================================== +# (7) IL GATE MANCANTE — anti-creep, e la finestra recente non deve poter autorizzare +# ========================================================================================== +def g_exact(r: np.ndarray, k: float) -> float: + x = 1.0 + k * r + if np.any(x <= 0): + return -np.inf + return float(np.expm1(ANN * np.mean(np.log(x)))) + + +def k_star(r: np.ndarray, kmax: float = 30.0, step: float = 0.05) -> float: + ks = np.arange(step, kmax + step, step) + return float(ks[int(np.nanargmax([g_exact(r, k) for k in ks]))]) + + +def _k_ammesso(r: np.ndarray, L: float) -> dict: + """k autorizzato dal criterio: mezzo-Kelly sul drift a -2 SE, con la coda TRONCATA al bound + strutturale (senza troncatura E[log(1+kR)] non esiste). Deliberatamente SEMPLIFICATO + rispetto a r0822c (nessuna GPD): qui interessa la MECCANICA del gate, non il suo numero.""" + mu, sd = float(r.mean()), float(r.std(ddof=0)) + se = sd * np.sqrt(ANN) / np.sqrt(len(r) / ANN) + r2 = np.maximum(r - mu + (mu * ANN - 2 * se) / ANN, -L) # -2SE + coda troncata + ks = k_star(r2) + return {"drift_pt": mu * ANN, "se": se, "drift_2se": mu * ANN - 2 * se, + "k_star": ks, "k_ammesso": 0.5 * ks, "n": len(r)} + + +def sezione_7_gate(rep: dict) -> None: + sez("(7) IL GATE MANCANTE — la scala e' invisibile allo Sharpe: serve un gate su un ALTRO asse") + r = rep["r_del"] + L = rep["L"] + book = rep["book"] + + print("\n (a) PERCHE' i gate esistenti non possono servire: lo Sharpe e' INVARIANTE alla scala.") + print(f" {'k':>6}{'Sharpe':>10}{'drift':>10}{'vol':>10}{'maxDD':>10}{'g(k) esatto':>14}") + for k in GRIGLIA_K: + x = k * r + eq = np.cumprod(1.0 + x) + dd = float((eq / np.maximum.accumulate(eq) - 1.0).min()) + print(f" {k:>6.2f}{x.mean()/x.std(ddof=0)*np.sqrt(ANN):>10.4f}{x.mean()*ANN:>10.2%}" + f"{x.std(ddof=0)*np.sqrt(ANN):>10.2%}{dd:>10.2%}{g_exact(r,k):>14.2%}") + print(" -> `deflated_sharpe` e `marginal_vs_tp01` leggono la 1a colonna: non FALLISCONO,") + print(" NON VEDONO la variabile. Un gate sulla scala deve guardare g(k), il DD e la rovina.") + + print("\n (b) IL CRITERIO ANTI-RECENCY — «una chiave di scala si alza dopo un mese buono».") + print(" Forma verificabile: il gradino dev'essere autorizzato ANCHE dai dati che") + print(" ESCLUDONO la finestra recente che ha motivato la richiesta.") + print(f"\n {'finestra':>34}{'n':>7}{'drift punto':>13}{'drift -2SE':>12}{'k*':>8}" + f"{'k ammesso':>11} {SCALA_PROPOSTA:.2f}x?") + fin = [("TUTTA la storia", r), + ("esclusi gli ultimi 90 giorni", r[:-90]), + ("esclusi gli ultimi 180 giorni", r[:-180]), + ("esclusi gli ultimi 365 giorni", r[:-365]), + ("SOLO gli ultimi 365 giorni", r[-365:])] + for nome, rr in fin: + d = _k_ammesso(rr, L) + ok = SCALA_PROPOSTA <= d["k_ammesso"] + print(f" {nome:>34}{d['n']:>7}{d['drift_pt']:>13.2%}{d['drift_2se']:>12.2%}" + f"{d['k_star']:>8.2f}{d['k_ammesso']:>11.2f} {'SI' if ok else 'NO'}") + d_full = _k_ammesso(r, L) + d_ex = _k_ammesso(r[:-90], L) + print(f""" + ⚠️ LETTURA ONESTA, e la prima stesura di questa sezione l'aveva sbagliata. La riga + «SOLO gli ultimi 365 giorni» NON e' l'esempio di «un anno buono»: quell'anno e' stato + DEBOLE (drift di punto {_k_ammesso(r[-365:], L)['drift_pt']:.2%}). Cio' che quella riga mostra e' solo che su + 365 osservazioni l'errore standard divora tutto — vero e insufficiente, perche' il rischio + da coprire e' l'opposto: un tratto recente BUONO che alza la richiesta.""") + + print("\n Percio' la domanda va posta al contrario: QUANTO puo' spostare il k ammesso un") + print(" tratto recente artificialmente buono? Si sovrascrive il drift degli ultimi 90") + print(" giorni e si guarda quanto si muove il criterio, sull'INTERA storia.") + print(f"\n {'ultimi 90g portati a':>24}{'drift punto (full)':>21}{'drift -2SE':>13}" + f"{'k ammesso':>11}{'delta vs base':>15}") + base_k = d_full["k_ammesso"] + for shock in (0.0, 0.25, 0.50, 1.00, 2.00): + rr = r.copy() + rr[-90:] = rr[-90:] - rr[-90:].mean() + shock / ANN + d = _k_ammesso(rr, L) + print(f" {shock:>23.0%}{d['drift_pt']:>21.2%}{d['drift_2se']:>13.2%}" + f"{d['k_ammesso']:>11.2f}{d['k_ammesso']-base_k:>+15.2f}") + print(f""" + -> un trimestre portato a +100%/anno (assurdo) muove il k ammesso di poche unita' su una + base di {base_k:.2f}: e' il -2 SE che fa il lavoro, perche' un tratto eccezionale alza + insieme il drift di punto E la sua incertezza. **Ma questo NON basta come gate**: il + criterio resta comunque una funzione dei dati recenti, e la protezione vera e' la + DOPPIA VALUTAZIONE — la richiesta dev'essere autorizzata anche da `r[:-90]` + (qui k ammesso {d_ex['k_ammesso']:.2f} contro {base_k:.2f}), cioe' da dati che NON contengono + la finestra che ha motivato la richiesta. E' l'analogo, sull'asse della scala, di + `select_cell_insample`. + ⚠️ E va detto il numero che rende tutto questo poco drammatico OGGI: con questa versione + semplificata il k ammesso e' {base_k:.2f}x, cioe' il gradino di 1,25x non e' vicino a + quel vincolo. **Il vincolo che morde e' quello della sez.(6) — il disaster-SL — non + il mezzo-Kelly.** Il criterio anti-recency serve al gradino SUCCESSIVO, non a questo.""") + + print("\n (c) LA CADENZA — un gate che si valuta solo quando lo si chiede non protegge da niente.") + ultimo = book.index.max() + print(f" ultimo giorno della serie: {ultimo.date()}. Il sorvegliante deve rispondere a") + print(" TRE domande diverse, e sono davvero tre (azioni diverse):") + print(" 1. la config dichiara una scala che il giornale non ha mai autorizzato? -> 🚨") + print(" 2. il criterio che autorizzo' la scala CORRENTE passa ancora oggi? -> 🚨 (si scende)") + print(" 3. il prodotto frac*scala*n_asset ha superato il tetto di codice? -> 🚨 (si blocca)") + print(" Nessuna delle tre e' deducibile dalle altre due.") + + +# ========================================================================================== +def main() -> None: + t0 = time.time() + print("=" * 108) + print(" r0822e — PROTOTIPO ISOLATO della CHIAVE DI SCALA (filone SCALE-SPEC)") + print(" Specifica: docs/research/SPEC-scale-key.md · NON e' produzione, non scrive nulla") + print("=" * 108) + + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + frac = float(cfg["max_notional_per_asset_frac"]) + sl_pct = float(cfg["disaster_sl_pct"]) + print(f"\n config/live.json (SOLA LETTURA): frac={frac} · cap fisso ${cfg['max_notional_per_asset_usd']:,.0f}" + f" · min order ${cfg['min_order_usd']} · disaster-SL {sl_pct:.0%}") + print(f" chiavi presenti: {sorted(k for k in cfg if not k.startswith('_'))}") + print(" -> nessuna chiave di scala. E' esattamente il buco che questo filone specifica.") + + sezione_0_isolamento() + sezione_1_sanity() + sezione_2_dove() + sezione_3_simmetria() + TPQ = pesi_tp01() + sezione_4_ordine(TPQ, frac) + rep = sezione_5_replica(TPQ, frac) + sezione_6_guardie(rep, TPQ, frac, sl_pct) + sezione_6bis_equity(frac, float(cfg['max_notional_per_asset_usd'])) + sezione_7_gate(rep) + + sez("RIEPILOGO — cosa questo prototipo ha DIMOSTRATO (e cosa resta alla specifica)") + print(f""" + DIMOSTRATO QUI + · la copia e' bit-exact alla produzione a k=1 -> le proprieta' misurate sono SUE; + · la scala applicata in `book_net_target` e' simmetrica sui due sleeve a ogni k, e il + controllo positivo prova che il test ha potenza SOLO se gira su una griglia di k; + · l'ordine col clamp decide se "leva k" significa la stessa cosa nel codice e nel + backtest: DOPO il clamp k_eff = k sempre; PRIMA il clamp la mangia a piena convinzione; + · i numeri a k=1,00 replicano WORST-DAY (peggior giorno, data, bound strutturale, morso + del clamp) -> le righe scalate della sez.(6) non sono numeri copiati; + · la guardia che morde per prima e' il DISASTER-SL, non il peggior giorno: il tetto va + messo sul PRODOTTO `n_asset * frac * scala * disaster_sl_pct`; + · lo Sharpe e' invariante alla scala a tutte le celle -> nessun gate esistente la vede; + · il criterio anti-recency e' misurabile: si valuta escludendo la finestra recente. + + NON DIMOSTRATO QUI, E VA DETTO + · non e' un backtest della leva: g(k) qui e' la forma chiusa su una serie storica, con + lo stesso limite gia' dichiarato da growth_policy (ritorno indipendente dalla size); + · la lente e' CLOSE-ONLY tranne dove usa il minimo pubblicato da WORST-DAY: su una regola + a UN giorno close-only e' CIECA (25/07), e la sez.(6) prende quel numero da fuori; + · il funding dei perpetual non e' in nessuna di queste serie (difetto trovato dal filone + FUNDING): a k=1 il libro paga gia' ~1,5%/anno che nessun backtest sottrae, e il gradino + lo amplifica del 25%; + · niente qui autorizza il gradino: autorizzarlo e' il gate della specifica, non un run. + + tempo: {time.time()-t0:.1f}s""") + + +if __name__ == "__main__": + main()