diff --git a/CLAUDE.md b/CLAUDE.md index 5d53375..5640246 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -889,9 +889,9 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis stesso conto Deribit**: tre sleeve quasi-ortogonali sui ritorni, **perfettamente correlati sul fallimento del venue** — cosa che la matrice di correlazione del book non vede per costruzione. (0-bis) ⚠️ **Limite dei muri del 25-26/07:** `book_series` gira a **`alloc=$600` col book a 2 - sleeve** → portarlo fino a $272k assume (a) che a $272k si giri ancora il book da $600 (falso: - a $3k GTAA01, $5k XSR01, $20k XS01 → il muro vero e' **piu' basso**) e (b) **"tutto su Deribit" - per 10-20 anni, senza dirlo**. + sleeve** → portarlo fino a $272k assume (a) che a $272k si giri ancora il book da $600 e (b) + **"tutto su Deribit" per 10-20 anni, senza dirlo**. ✅ **(a) MISURATO e REFUTATO il 26/07** — + vedi bullet "MURO COME PUNTO FISSO": il muro **non** scende, $273.900 vs $272.061 (**+1%**). (1) **La misura** (accumulo da $600, €250/m, 20a, bersaglio $272k, ×0.89, jump di venue a probabilita' annua `p`; CONC = 100% Deribit vs SPLIT = Deribit 65 / HL 15 / IB 20; **bersaglio identico → conservativo CONTRO lo split**): @@ -954,6 +954,39 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis **curva**, non aspettando il numero; (b) dichiarare la **qualita' della fonte** (immagine non letta = dato secondario); (c) una conclusione che poggia su un parametro di config va legata a un **test su quel parametro**, o sopravvive al cambio che la rende falsa. +- ❌ **MURO COME PUNTO FISSO — la mia previsione era SBAGLIATA e il numero pubblicato era giusto + per caso (2026-07-26).** Script `r0726_wall_fixedpoint.py`, test `tests/test_wall_fixedpoint.py` + (11), diario `2026-07-26-wall-fixedpoint.md`. **Book/pesi/cron INVARIATI.** + Il follow-up dichiarato diceva: *"i muri usano il book a 2 sleeve da $600 estrapolato a $272k; + il book diversificato ha Sharpe piu' alto → **il muro vero e' piu' basso**"*. **Misurato: FALSO.** + (1) **Struttura giusta: il muro e' un PUNTO FISSO** — serve capitale C per girare il book che + determina il muro C → si itera `C_{n+1} = muro(book(C_n))`. Converge in **1 iterazione** perche' + il muro cade **sopra** la soglia XS01 ($117k), quindi la composizione non cambia; la struttura + conta solo se il muro atterra vicino a una soglia — ma **va iterato per saperlo**. + (2) **Risultato:** book deployable (TP01 38/SKH01 23/GTAA01 23/XS01 17; **VRP01 escluso** per + regola short-vol, **XSR01 escluso** per gate 23/10; costi capital-aware; ancora ×0.860 misurata + su questo book) → Sharpe **1.94**, vol 8.9%, CAGR 18.3%. **Muro $273.900 vs $272.061 = +1%.** + (3) **Perche':** diversificare alza lo Sharpe (1.64 → 1.94) ma abbassa **drift e vol insieme**; + la rendita perpetua vive sul **drift** → 10.91% → 10.84% = invariata. **Il guadagno di Sharpe va + in meno rischio, non in piu' reddito** — cioe' il fatto gia' misurato il 25/07 §3, che avevo + dimenticato scrivendo il follow-up. + (4) ⚠️ **Stavo violando una regola gia' codificata:** *"un diversificatore a basso CAGR si giudica + a ISO-RISCHIO, mai a iso-nozionale"* (25/07 §3). A iso-rischio (leva **1.28x**): rendita 13.35%, + **muro $222.406 = −18%** — ✅ **replica indipendente** del −19% misurato il 25/07 con macchineria + e book diversi. Vale **solo** se la leva e' disponibile e a costo < uplift (IB: Reg-T 2x, + portfolio margin da ~$110k, margine ~5.5%/a): **$222k e' un TETTO, non una stima.** + (5) ⚠️ **BUG catturato prima di pubblicare:** la prima corsa dava Sharpe 0.95 e muro **$854k** + ("diversificare triplica il muro" — spettacolare e falso). Causa: `CC.gtaa_banded` ritorna la + storia GTAA **dal 1996** mentre lo sleeve di produzione tronca a `GTAA_BOOK_ACTIVATION`; con la + rinormalizzazione per-riga di `combine_outer` il **75% del campione** era **GTAA01 da solo al + 100%**. Preso **non da un test** ma perche' la somma pesata dei componenti (~18%) non tornava col + drift del combinato (6.8%). **Diagnostica decisiva: la COPERTURA PER COLONNA** (TP01 24.6% / + SKH01 24.6% / GTAA01 100.0% / XS01 8.6%) — un 100% accanto a valori bassi dice tutto. + **REGOLE:** (a) un follow-up dichiarato contiene una **previsione**, che va misurata non assunta; + (b) **rileggere le regole gia' codificate prima di impostare il confronto**; (c) quando un + aggregato non torna con la somma dei suoi pezzi, **fermarsi** — era l'unico segnale del bug + (nessun test, nessuna eccezione, output plausibile); (d) **la copertura per colonna e' la prima + diagnostica di un outer-join**. - **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55. - **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.** diff --git a/docs/diary/2026-07-26-wall-fixedpoint.md b/docs/diary/2026-07-26-wall-fixedpoint.md new file mode 100644 index 0000000..32b3a33 --- /dev/null +++ b/docs/diary/2026-07-26-wall-fixedpoint.md @@ -0,0 +1,132 @@ +# 2026-07-26 — Il muro come PUNTO FISSO: la mia previsione era sbagliata, e il numero pubblicato era giusto per caso + +**Richiesta:** *"fai follow up"* — il follow-up dichiarato poche ore prima: + +> I muri assumono il book a 2 sleeve da $600 fino a $272k. Il book a 5 sleeve ha Sharpe piu' alto, +> quindi **il muro vero e' piu' basso di $272k**. + +**Script:** `r0726_wall_fixedpoint.py`. **Test:** `tests/test_wall_fixedpoint.py` (11). +**Book, pesi, cron, config: INVARIATI.** + +--- + +## 0. Esito in una riga + +**La previsione era sbagliata: il muro NON scende.** A pari nozionale e' **$273.900** contro i +**$272.061** pubblicati — **+1%**, cioe' l'estrapolazione col book a 2 sleeve era **giusta per +caso**. Solo a **iso-rischio** scende, a **$222.406 (−18%)**, e li' serve leva a costo minore +dell'uplift, che non e' gratis. + +--- + +## 1. La struttura giusta: il muro e' un punto fisso + +Il difetto era reale: `book_series(alloc=$600)` e' il book a **2 sleeve**, estrapolato fino a +$272k. A quella taglia gireresti un altro book — a ~$13k si accende GTAA01 (IB), a ~$118k XS01 +(Hyperliquid, 19 gambe). + +E la correzione ha una forma: **serve capitale C per girare il book che determina il muro C**. +Si itera `C_{n+1} = muro(book(C_n))`. + +``` + iter capitale ipotesi sleeve Sharpe rendita -> muro + 0 $272,061 TP01+SKH01+GTAA01+XS01 1.94 10.84% $273,900 +CONVERGE a $273,900 (variazione < 2%) +``` + +Converge in **una** iterazione, e il motivo e' istruttivo: il muro cade **sopra** la soglia di +XS01 ($117k), quindi la composizione del book non cambia fra un'iterazione e l'altra. La struttura +a punto fisso conta solo se il muro atterra **vicino a una soglia** — qui non succede, ma non si +poteva sapere prima di iterare. + +**Regole di ammissione rispettate** (e' il book *deployable*, non il *migliore*): **VRP01 escluso** +(regola permanente: niente short-vol da modello in deploy), **XSR01 escluso** (gate pre-registrato +23/10), pesi = book attivo rinormalizzato, costi **capital-aware** (TP01 haircut min-order, GTAA01 +commissione IB reale a banda+cadenza, SKH01 sul path live), fattore d'ancora **misurato su questo +book** (×0.860) e non ereditato. + +--- + +## 2. Perche' il muro non scende + +``` +sleeve peso drift/a vol/a Sharpe +TP01 38% 15.7% 12.2% 1.29 +SKH01 23% 27.6% 23.4% 1.18 +GTAA01 23% 6.1% 5.4% 1.12 +XS01 17% 28.0% 20.9% 1.34 + +book 2-sleeve (riferimento) drift 18.6% vol 11.4% Sharpe 1.64 +book deployable al p.fisso CAGR 18.3% vol 8.9% Sharpe 1.94 +``` + +Diversificare **alza lo Sharpe** (1.64 → 1.94) ma abbassa **drift e vol insieme**. La rendita +perpetua vive sul drift: 10.91% → 10.84%, cioe' **invariata**. Il guadagno di Sharpe finisce tutto +in **meno rischio**, non in piu' reddito. + +E' esattamente il fatto gia' misurato il 25/07 (§3): *"Diversificare NON crea reddito a pari +nozionale — libera BUDGET DI RISCHIO"*. Non me ne ero ricordato scrivendo il follow-up. + +## 3. Il confronto onesto e' a ISO-RISCHIO — e stavo violando una regola del progetto + +CLAUDE.md ha una regola codificata il 25/07: + +> **Un diversificatore a basso CAGR si giudica a ISO-RISCHIO, mai a iso-nozionale** (e' il null +> de-levering al contrario). + +Il confronto che avevo impostato era **iso-nozionale**: quello sbagliato secondo la regola del +progetto stesso. + +``` +lente rendita muro +2 sleeve, ×0.89 (pubblicato) 10.91% $272,061 +deployable, ISO-NOZIONALE (leva 1.00x) 10.84% $273,900 (+1%) +deployable, ISO-RISCHIO (leva 1.28x) 13.35% $222,406 (-18%) +``` + +✅ **Replica indipendente:** il 25/07 misurava, con macchineria diversa e su un book diverso, +iso-rischio → **−19% di muro**. Qui esce **−18%**. Due implementazioni separate, stesso numero. + +⚠️ L'iso-rischio vale **solo se la leva e' (a) disponibile e (b) a costo minore dell'uplift** — +addendum IB del 25/07: su ETF a IB c'e' Reg-T 2x (portfolio margin da ~$110k) e il margine costa +~5.5%/anno. **Il $222k NON sconta il costo del margine: e' un tetto, non una stima.** + +--- + +## 4. Il bug che ha quasi prodotto un titolo grottesco + +La prima corsa dava: Sharpe **0.95**, CAGR 6.7%, muro **$854.013** — cioe' *"diversificare triplica +il muro"*, un risultato spettacolare e falso. Il numero era plausibile abbastanza da poter essere +pubblicato. + +**Causa:** `CC.gtaa_banded` ritorna la storia GTAA **dal 1996**, mentre lo sleeve di produzione +(`sleeves._gtaa_daily_returns`) la tronca a `GTAA_BOOK_ACTIVATION` (2019-03). Con la +rinormalizzazione per-riga di `combine_outer`, per il **75% del campione** (1996-2019) il "book a +4 sleeve" era **GTAA01 da solo al 100%** — uno sleeve al 4% di drift. + +**Come l'ho preso:** non da un test, ma perche' **la somma pesata dei componenti (~18%) non +tornava col drift del combinato (6.8%)**. Una discrepanza di 11 punti che non sapevo spiegare. + +**Diagnostica decisiva:** la copertura per colonna — `TP01 24.6% / SKH01 24.6% / GTAA01 100.0% / +XS01 8.6%`. Un 100% accanto a tre valori bassi dice tutto in una riga. + +**Cablato:** `test_gtaa_e_troncato_all_era_book` (la causa), `test_il_book_deployable_non_parte_dal_1996` +(l'effetto, per prendere una gamba futura che reintroducesse storia antecedente) e +`test_il_book_a_4_sleeve_ha_sharpe_superiore_al_singolo_gtaa` (plausibilita'). + +--- + +## 5. Regole trasferibili + +1. **Un follow-up dichiarato contiene una PREVISIONE, e la previsione va misurata, non assunta.** + Avevo scritto in CLAUDE.md "il muro vero e' piu' basso": era una deduzione ragionevole + (piu' Sharpe → piu' rendita) e sbagliata, perche' la rendita vive sul **drift**. +2. **Prima di misurare, rileggere le regole gia' codificate.** La risposta ("diversificare non + crea reddito a pari nozionale, si giudica a iso-rischio") era gia' in CLAUDE.md dal 25/07 e ho + impostato comunque il confronto sbagliato. +3. **Quando un aggregato non torna con la somma dei suoi pezzi, fermarsi.** L'11% di discrepanza + era l'unico segnale del bug: nessun test lo copriva, nessuna eccezione, output plausibile. +4. **La copertura per colonna e' la prima diagnostica di un outer-join.** Un 100% accanto a valori + bassi = una gamba con storia diversa che domina la rinormalizzazione. +5. **Un punto fisso converge subito se non e' vicino a una soglia** — ma va iterato lo stesso per + saperlo. diff --git a/scripts/research/r0726_wall_fixedpoint.py b/scripts/research/r0726_wall_fixedpoint.py new file mode 100644 index 0000000..d5427fd --- /dev/null +++ b/scripts/research/r0726_wall_fixedpoint.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python +"""r0726_wall_fixedpoint.py — il muro di capitale come PUNTO FISSO, col book giusto alla taglia. + +FOLLOW-UP dichiarato il 2026-07-26 e chiuso qui. + +IL DIFETTO. `r0726_capwall_refresh.py` (e prima `r0725_capcurve`) calcola il muro con +`book_series(alloc=$600)` = **book a 2 sleeve**, e lo estrapola fino a $272.061. Ma a $272k non +gireresti quel book: a ~$13k si accende GTAA01 (IB), a ~$118k XS01 (Hyperliquid). Il book +diversificato ha Sharpe piu' alto → **rendita perpetua piu' alta → muro piu' basso**. + +E la correzione ha una struttura: **il muro e' un PUNTO FISSO**. Serve capitale C per girare il +book che determina il muro C. Si itera C_{n+1} = muro(book(C_n)) fino a convergenza. + +REGOLE DI AMMISSIONE RISPETTATE (non e' il book "migliore", e' quello DEPLOYABILE): + * **VRP01 ESCLUSO** — regola permanente del progetto: *niente short-vol da modello in deploy* + (premio modellato su DVOL ATM, skew non esplicito, f di stress reale non catturato); + * **XSR01 ESCLUSO** — non ammesso: gate pre-registrato al 2026-10-23, oggi forward-monitor; + * pesi = quelli del book ATTIVO rinormalizzati sugli sleeve inclusi; + * costi **capital-aware**: TP01 con haircut min-order, GTAA01 con commissione IB reale a banda + e cadenza settimanale, SKH01 sul **path live** (lente hourly); + * soglie: GTAA01 alloc >= $3.000 (`GTAA_MIN_CAPITAL`), XS01 alloc >= $20.000 (19 gambe). + +DE-LUCK: fattore d'ancora **misurato sul book deployable stesso** (estrazioni congiunte sulle +serie d'ancora in cache), non ereditato da un altro book. + + uv run python scripts/research/r0726_wall_fixedpoint.py +""" +from __future__ import annotations + +import sys +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")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import r0724_skh_live_weight as skl # noqa: E402 +import r0725_capcurve as CC # noqa: E402 +import r0726_capwall_refresh as CW # noqa: E402 +import r0726_loo_deluck as L # noqa: E402 +from src.portfolio.gtaa import GTAA_MIN_CAPITAL # noqa: E402 +from src.portfolio.portfolio import combine_outer, metrics # noqa: E402 +from src.portfolio.sleeves import (GTAA_BOOK_ACTIVATION, # noqa: E402 + _xsec_returns) + +DPY = 365.25 +XS_MIN_CAPITAL = 20_000.0 # 19 gambe market-neutral (soglia documentata) + +# book ATTIVO senza VRP01 (regola: niente short-vol da modello in deploy), rinormalizzato su 0.88 +W_DEPLOY = {"TP01": 0.33 / 0.88, "SKH01": 0.20 / 0.88, + "GTAA01": 0.20 / 0.88, "XS01": 0.15 / 0.88} +# nome deployable -> nome nella griglia d'ancora di r0726_loo_deluck +ANCHOR_NAME = {"TP01": "TP01_trend_1d", "SKH01": "SKH01_skyhook", + "GTAA01": "GTAA01_eq_trend", "XS01": "XS01_xsec_hl"} +MIN_ALLOC = {"TP01": 0.0, "SKH01": 0.0, "GTAA01": GTAA_MIN_CAPITAL, "XS01": XS_MIN_CAPITAL} + + +def _daily(s: pd.Series) -> pd.Series: + s = s.dropna().sort_index() + if s.index.tz is None: + s.index = s.index.tz_localize("UTC") + return s + + +def included_at(capital: float) -> dict: + """Sleeve eseguibili a quel capitale, con pesi rinormalizzati fra i soli inclusi.""" + inc = {k: w for k, w in W_DEPLOY.items() if capital * w >= MIN_ALLOC[k]} + tot = sum(inc.values()) + return {k: w / tot for k, w in inc.items()} + + +def anchor_factor(weights: dict, draws: int = 800) -> float: + """Fattore d'ancora sul DRIFT, misurato su QUESTO book (non ereditato).""" + S = L.build_all() + names = [ANCHOR_NAME[k] for k in weights] + wmap = {ANCHOR_NAME[k]: v for k, v in weights.items()} + canon = combine_outer({n: S[n][L.SPEC[n][2]] for n in names}, wmap) + d_can = float(canon.dropna().values.mean() * DPY) + rng = np.random.default_rng(L.SEED) + ds = [] + for _ in range(draws): + cfg = {n: int(rng.choice(L.SPEC[n][1])) for n in names} + s = combine_outer({n: S[n][cfg[n]] for n in names}, wmap) + ds.append(float(s.dropna().values.mean() * DPY)) + return float(np.median(ds) / d_can) if d_can else float("nan") + + +_SKH = None +_XS = None + + +def realistic_book(capital: float, weights: dict) -> pd.Series: + """Book DEPLOYABLE a quel capitale, coi costi reali di ciascun venue.""" + global _SKH, _XS + if _SKH is None: + _SKH = _daily(skl.skh_book(0, "hourly")) + if _XS is None: + _XS = _daily(_xsec_returns()).resample("1D").apply(lambda x: (1 + x).prod() - 1) + cols = {} + for k, w in weights.items(): + alloc = capital * w + if k == "TP01": + cols[k] = CC.tp01_realistic(alloc) + elif k == "SKH01": + cols[k] = _SKH + elif k == "GTAA01": + # ⚠️ `CC.gtaa_banded` ritorna la storia PIENA dal 1996; lo sleeve di produzione + # (`sleeves._gtaa_daily_returns`) tronca a GTAA_BOOK_ACTIVATION. Senza questo + # troncamento il 75% del campione (1996-2019) e' GTAA01 DA SOLO al 100% dopo la + # rinormalizzazione per-riga di combine_outer -> il "book a 4 sleeve" diventa uno + # sleeve a basso CAGR e il muro esce grottescamente alto. Bug catturato in sessione. + g = _daily(CC.gtaa_banded(alloc, CC.GTAA_BAND_USD, CC.GTAA_EVERY)) + cols[k] = g[g.index >= GTAA_BOOK_ACTIVATION] + elif k == "XS01": + cols[k] = _XS + return combine_outer(cols, weights) + + +def wall_of(capital: float, deluck: float, k: float = 1.0) -> tuple[float, float, dict]: + """(muro, tasso di rendita perpetua, metriche) del book deployable a quel capitale.""" + w = included_at(capital) + b = realistic_book(capital, w) + r = (b - (1.0 - deluck) * float(b.mean())).values.astype(float) + gross = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE) + _swr, perp, cap = CW.perp_and_wall(r, k, gross) + return cap, perp, dict(sharpe=metrics(b)["sharpe"], cagr=metrics(b)["cagr"], + vol=float(b.std() * np.sqrt(DPY)), sleeves=w) + + +def main() -> None: + print("=" * 100) + print(" r0726 — IL MURO COME PUNTO FISSO (book giusto alla taglia). Chiude il follow-up.") + print("=" * 100) + print(" Difetto corretto: il muro pubblicato ($272k) usa il book a 2 sleeve da $600") + print(" estrapolato fino a $272k. A quella taglia gireresti un book diverso.") + + # --- fattore d'ancora del book deployable, misurato su se stesso ----------- + w_full = included_at(1_000_000.0) + f = anchor_factor(w_full) + print(f"\n Fattore d'ancora sul drift, MISURATO su questo book " + f"({'+'.join(w_full)}): ×{f:.3f}") + print(f" (per confronto: ×0.874 book 5-sleeve, ×0.890 book live 2-sleeve)") + + # --- soglie --------------------------------------------------------------- + print("\n SOGLIE DI SBLOCCO (peso deployable × capitale >= minimo dello sleeve)") + for k, w in sorted(W_DEPLOY.items(), key=lambda x: -MIN_ALLOC[x[0]]): + need = MIN_ALLOC[k] / w if MIN_ALLOC[k] > 0 else 0.0 + print(f" {k:<8} peso {w:.1%} min alloc ${MIN_ALLOC[k]:>9,.0f} " + f"-> serve capitale ${need:>10,.0f}" if need else + f" {k:<8} peso {w:.1%} nessuna soglia") + + # --- punto fisso ---------------------------------------------------------- + print("\n" + "-" * 100) + print(" ITERAZIONE AL PUNTO FISSO C_{n+1} = muro( book(C_n) )") + print("-" * 100) + print(f" {'iter':>5}{'capitale ipotesi':>20}{'sleeve':>34}{'Sharpe':>8}" + f"{'rendita':>9}{'-> muro':>14}") + C = 272_061.0 + for i in range(6): + cap, perp, m = wall_of(C, f) + names = "+".join(m["sleeves"]) + print(f" {i:>5}{'$'+format(C, ',.0f'):>20}{names:>34}{m['sharpe']:>8.2f}" + f"{perp:>9.2%}{'$'+format(cap, ',.0f'):>14}") + if abs(cap - C) / max(C, 1) < 0.02: + C = cap + print(f"\n CONVERGE a ${C:,.0f} (variazione < 2%)") + break + C = cap + else: + print(f"\n non converge in 6 iterazioni, ultimo valore ${C:,.0f}") + + # --- confronto ------------------------------------------------------------ + print("\n" + "=" * 100) + print(" CONFRONTO COI NUMERI PUBBLICATI") + print("=" * 100) + cap2, perp2, m2 = 272_061.0, 0.1091, None + print(f" {'lente':<52}{'rendita':>10}{'muro':>14}") + print(f" {'25/07: 2 sleeve, de-luck ×0.60 (a occhio)':<52}{6.00:>9.2f}%" + f"{'$494,758':>14}") + print(f" {'26/07: 2 sleeve, de-luck ×0.89 (misurato)':<52}{perp2*100:>9.2f}%" + f"{'$'+format(cap2, ',.0f'):>14}") + capf, perpf, mf = wall_of(C, f) + print(f" {'26/07-bis: book DEPLOYABLE alla taglia, punto fisso':<52}" + f"{perpf*100:>9.2f}%{'$'+format(capf, ',.0f'):>14}") + print(f"\n Sharpe del book al punto fisso: {mf['sharpe']:.2f} " + f"(vol {mf['vol']*100:.1f}%, CAGR {mf['cagr']*100:.1f}%)") + print(f" sleeve attivi: {', '.join(f'{k} {v:.0%}' for k, v in mf['sleeves'].items())}") + delta = capf / cap2 - 1.0 + print(f"\n >>> il muro si sposta di {delta*100:+.0f}% rispetto al numero pubblicato") + + # ---------------- perche': diagnostica per sleeve -------------------------- + print("\n" + "-" * 100) + print(" PERCHE' — contributo di ciascuno sleeve al punto fisso") + print("-" * 100) + w = included_at(C) + print(f" {'sleeve':<9}{'peso':>7}{'drift/a':>10}{'vol/a':>9}{'Sharpe':>9}") + for k, wk in w.items(): + s = _daily(realistic_book(C, {k: 1.0})) + print(f" {k:<9}{wk:>6.0%}{s.mean()*DPY*100:>9.1f}%{s.std()*np.sqrt(DPY)*100:>8.1f}%" + f"{metrics(s)['sharpe']:>9.2f}") + b2 = _daily(CC.book_series("hourly")) + print(f"\n book 2-sleeve (riferimento) drift {b2.mean()*DPY*100:.1f}% " + f"vol {b2.std()*np.sqrt(DPY)*100:.1f}% Sharpe {metrics(b2)['sharpe']:.2f}") + print(" -> diluire in sleeve a BASSO CAGR abbassa drift E vol insieme: a pari") + print(" nozionale il muro SALE. E' il fatto gia' misurato il 25/07 (§3).") + + # ---------------- il confronto ONESTO: iso-rischio ------------------------- + print("\n" + "=" * 100) + print(" CONFRONTO A ISO-RISCHIO — la regola del progetto (25/07 §3)") + print("=" * 100) + print(" 'Un diversificatore a basso CAGR si giudica a ISO-RISCHIO, mai a iso-nozionale") + print(" (e' il null de-levering al contrario).' Il confronto sopra e' iso-nozionale:") + print(" secondo la regola del progetto e' quello SBAGLIATO. Qui quello giusto.") + bd = _daily(realistic_book(C, w)) + vol_d = float(bd.std() * np.sqrt(DPY)) + vol_2 = float(b2.std() * np.sqrt(DPY)) + k_iso = vol_2 / vol_d if vol_d > 0 else 1.0 + print(f"\n vol book deployable {vol_d*100:.1f}% | vol book 2-sleeve {vol_2*100:.1f}%" + f" -> leva iso-rischio k = {k_iso:.2f}x") + cap_iso, perp_iso, _ = wall_of(C, f, k=k_iso) + print(f"\n {'lente':<52}{'rendita':>10}{'muro':>14}") + print(f" {'2 sleeve, ×0.89 (pubblicato)':<52}{perp2*100:>9.2f}%" + f"{'$'+format(cap2, ',.0f'):>14}") + print(f" {'deployable, ISO-NOZIONALE (leva 1.0x)':<52}{perpf*100:>9.2f}%" + f"{'$'+format(capf, ',.0f'):>14}") + print(f" {f'deployable, ISO-RISCHIO (leva {k_iso:.2f}x)':<52}{perp_iso*100:>9.2f}%" + f"{'$'+format(cap_iso, ',.0f'):>14}") + print("\n ⚠️ L'iso-rischio vale SOLO se la leva e' (a) disponibile e (b) a costo minore") + print(" dell'uplift — lezione del 25/07 (addendum IB 10k): su ETF a IB c'e' Reg-T 2x") + print(" (portfolio margin da ~$110k) e il margine costa ~5.5%/anno. Il numero sopra") + print(" NON sconta il costo del margine: e' un TETTO, non una stima.") + print("\n ⚠️ VRP01 e XSR01 restano ESCLUSI per regola, non per risultato: includerli") + print(" abbasserebbe ancora il muro ma violerebbe 'niente short-vol da modello in") + print(" deploy' e il gate pre-registrato di XSR01.") + + +if __name__ == "__main__": + main() diff --git a/tests/test_wall_fixedpoint.py b/tests/test_wall_fixedpoint.py new file mode 100644 index 0000000..a707fb1 --- /dev/null +++ b/tests/test_wall_fixedpoint.py @@ -0,0 +1,102 @@ +"""Test per r0726_wall_fixedpoint.py — il muro come punto fisso col book giusto alla taglia. + +Il test che conta e' `test_gtaa_e_troncato_all_era_book`: e' il bug catturato in sessione. +`CC.gtaa_banded` ritorna la storia GTAA dal **1996**, mentre lo sleeve di produzione tronca a +`GTAA_BOOK_ACTIVATION`. Senza troncamento la rinormalizzazione per-riga di `combine_outer` +rende il 75% del campione (1996-2019) **GTAA01 da solo al 100%** — e il "book a 4 sleeve" +diventa uno sleeve a basso CAGR: Sharpe 0.95 invece di 1.94, muro $854k invece di $274k. +Un numero grottescamente sbagliato e perfettamente plausibile a prima vista. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import r0726_wall_fixedpoint as WF # noqa: E402 + + +# =========================================================================== +# il bug: troncamento di GTAA01 +# =========================================================================== +def test_gtaa_e_troncato_all_era_book(): + from src.portfolio.sleeves import GTAA_BOOK_ACTIVATION + + g = WF.realistic_book(300_000.0, {"GTAA01": 1.0}) + assert g.index[0] >= GTAA_BOOK_ACTIVATION, ( + f"GTAA01 parte da {g.index[0]}: storia pre-book inclusa, il book combinato sarebbe " + "GTAA01 da solo per la maggior parte del campione") + + +def test_il_book_deployable_non_parte_dal_1996(): + """Guardia sull'effetto, non solo sulla causa: se una gamba futura reintroducesse storia + antecedente, questo test la prende anche se il test sopra non la copre.""" + b = WF.realistic_book(300_000.0, WF.included_at(300_000.0)) + assert b.index[0] >= pd.Timestamp("2019-01-01", tz="UTC"), ( + f"il book parte da {b.index[0]}: qualche gamba porta storia pre-era-crypto") + + +def test_il_book_a_4_sleeve_ha_sharpe_superiore_al_singolo_gtaa(): + """Controllo di plausibilita': un book diversificato non puo' avere lo Sharpe di GTAA01 + da solo. Con il bug del troncamento faceva esattamente quello.""" + from src.portfolio.portfolio import metrics + + C = 300_000.0 + b = WF.realistic_book(C, WF.included_at(C)) + g = WF.realistic_book(C, {"GTAA01": 1.0}) + assert metrics(b)["sharpe"] > metrics(g)["sharpe"] + 0.3 + + +# =========================================================================== +# soglie e pesi +# =========================================================================== +def test_soglie_di_sblocco_degli_sleeve(): + assert set(WF.included_at(1_000.0)) == {"TP01", "SKH01"} + assert set(WF.included_at(50_000.0)) == {"TP01", "SKH01", "GTAA01"} + assert set(WF.included_at(300_000.0)) == {"TP01", "SKH01", "GTAA01", "XS01"} + + +@pytest.mark.parametrize("cap", [600.0, 5_000.0, 50_000.0, 300_000.0]) +def test_i_pesi_si_rinormalizzano_a_uno(cap): + w = WF.included_at(cap) + assert sum(w.values()) == pytest.approx(1.0) + assert all(v > 0 for v in w.values()) + + +def test_vrp01_e_xsr01_sono_esclusi_per_regola(): + """VRP01: 'niente short-vol da modello in deploy'. XSR01: gate pre-registrato 2026-10-23. + Se qualcuno li aggiunge qui senza passare dai gate, il test lo dice.""" + assert "VRP01" not in WF.W_DEPLOY + assert "XSR01" not in WF.W_DEPLOY + assert set(WF.W_DEPLOY) == {"TP01", "SKH01", "GTAA01", "XS01"} + + +def test_i_pesi_deployable_derivano_dal_book_attivo(): + """W_DEPLOY deve essere il book attivo meno VRP01, rinormalizzato — non pesi inventati.""" + from src.portfolio.sleeves import active_sleeves + + live = {s.name: s.weight for s in active_sleeves()} + atteso = { + "TP01": live["TP01_trend_1d"], "SKH01": live["SKH01_skyhook"], + "GTAA01": live["GTAA01_eq_trend"], "XS01": live["XS01_xsec_hl"]} + tot = sum(atteso.values()) + for k, v in atteso.items(): + assert WF.W_DEPLOY[k] == pytest.approx(v / tot, rel=1e-6), ( + f"{k}: peso disallineato da active_sleeves()") + + +# =========================================================================== +# fattore d'ancora +# =========================================================================== +def test_anchor_factor_e_un_haircut_plausibile(): + """Deve essere <1 (l'ancora canonica e' un punto alto) ma non assurdo.""" + f = WF.anchor_factor(WF.included_at(300_000.0), draws=40) + assert 0.6 < f < 1.05, f"fattore d'ancora implausibile: {f}"