research(rendita): curva capitale->book eseguibile, muro della rendita, asimmetria funded

Ondata "rendita passiva" (2° filone del 2026-07-25). Book live, pesi, config e cron
INVARIATI: questa sessione non tocca nulla del path di esecuzione.

1) DIFETTO DI ESEGUIBILITA' su GTAA01 (5° sleeve del book attivo). gtaa.py modella 2bps
   proporzionali e lo sleeve e' documentato "switch mensile/basso turnover": entrambe false.
   Il vol-target ribilancia OGNI GIORNO su 6 gambe e IB ha un pavimento FISSO per ordine
   (min(max($0.35, $0.0035/az), 1% valore)) = ~$530/anno indipendenti dal capitale.
   A $600: CAGR -3.5% / Sharpe -0.55 (vs +5.5%/0.77 modellato); negativo fino a ~$3k.
   Con banda $50 + cadenza settimanale (plateau, non argmax) torna a Sh 0.52 / CAGR 3.0%.
   -> Sharpe onesto di GTAA01 ~0.55-0.64, non 0.77.
   REGOLA NUOVA: il costo di un venue va modellato nella sua FORMA (fisso vs proporzionale),
   non solo nel livello. Controprova su Deribit (proporzionale): TP01 realistico = modellato
   da ~$500 in su.

2) LA RENDITA NON E' capitale x CAGR. Il muro del 24/07 (EUR 122k) ignorava il rischio di
   sequenza. Prelievo perpetuo (P(cap a 20a >= cap iniziale) >= 90%) sul path live
   de-luckato: 5.98% -> $497k; banda onesta $232k-$497k (il de-luck x0.6 sopra il path live
   rischia di contare due volte la degradazione SKH01). Invariante di lente: la rendita
   perpetua vale ~45-55% del CAGR -> ogni muro calcolato come target/CAGR sbaglia di ~2x.

3) DIVERSIFICARE NON CREA REDDITO a pari nozionale (5.98% -> 6.35%); libera BUDGET DI
   RISCHIO: a iso-rischio (vol 15%) 7.51%/$395k -> 9.25%/$321k. L'ipotesi "piu' capitale ->
   piu' sleeve -> CAGR super-lineare" e' REFUTATA nella forma forte (curva Sharpe piatta da
   $600 a $200k).

4) ASIMMETRIA CAPITALE-PROPRIO vs FUNDED. Su conto funded il capitale e' $100k -> gli sleeve
   STAT-MODE per taglia (XS01) diventano eseguibili, e le regole prop passano sul DRAWDOWN,
   non sul CAGR. Aggiungendo XS01: P(pass) HYRO 44.4->54.7%, funded P(vivo 1a) 18.9->57.6%.

5) LA VIA: i 600 euro come BIGLIETTO. Scala di conti funded, 36 mesi. Problema mai studiato:
   N conti sullo STESSO book bustano INSIEME -> serve sleeve diversi su conti diversi.
   Vince il MISTO, non gli estremi; il book live attuale e' la politica PEGGIORE.
   Verdetto in banda: P(arrivare a 50 EUR/g in 3 anni) 2-21%, P(bruciare i 600) 33-78%
   -> scommessa a coda destra, non una rendita.

6) ADDENDUM "10k su IB": allocazione fra venue. 94% su IB piu' che dimezza il reddito
   (EUR 0.93/g vs 2.16/g). La leva che convertirebbe lo Sharpe in reddito e' chiusa dai
   costi (Reg-T 2x + margine 5.5%) -> vince tutto-Deribit a 1.33x. GTAA01 sui 10k rende
   EUR 0.50/g sopra la liquidita' ferma, pagati con un maxDD del 10%.

Bug catturato in sessione: base di prelievo funded aggiornata giornalmente come HWM mobile
-> payout $230/anno invece di ~$7.000; regole vere = max-loss STATICO dal saldo iniziale.
Test di regressione cablato.

Test: 214 pass (200 preesistenti + 14 nuovi in tests/test_capcurve.py).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-25 17:05:16 +00:00
parent 7b567c8435
commit ea6743d6b0
7 changed files with 1975 additions and 0 deletions
+182
View File
@@ -0,0 +1,182 @@
"""Test della macchineria RENDITA / COST-FLOOR (ondata 2026-07-25).
Blocca tre cose che, se si rompono in silenzio, producono numeri credibili e sbagliati:
1. il simulatore di prelievo (survival) — validato contro casi con risposta NOTA;
2. il modello di costo IB a pavimento fisso — deve penalizzare il capitale piccolo e
deve essere salvato dalla banda (altrimenti GTAA01 sembra eseguibile quando non lo e');
3. la regressione del bug HWM nel modello funded (base di prelievo aggiornata ogni giorno
-> payout ~0): e' l'errore realmente commesso in sessione.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
import numpy as np
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
def _load(name: str, rel: str):
spec = importlib.util.spec_from_file_location(name, ROOT / rel)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
@pytest.fixture(scope="module")
def cc():
return _load("r0725_capcurve", "scripts/research/r0725_capcurve.py")
# --------------------------------------------------------------- 1) survival
def test_survival_asset_deterministico(cc):
"""Un asset che rende esattamente 10%/anno senza vol sostiene un prelievo perpetuo
appena sotto il 10% e NON sostiene il 15%."""
r = np.full(2000, 1.10 ** (1 / 365) - 1.0)
assert cc.survival(r, 1e6, 1e6 * 0.05, 20, 1.0, n_paths=100)["p10_end"] > 1e6
assert cc.survival(r, 1e6, 1e6 * 0.09, 20, 1.0, n_paths=100)["p10_end"] > 1e6
# sopra il rendimento dell'asset il capitale si erode, e molto sopra si va in rovina
assert cc.survival(r, 1e6, 1e6 * 0.11, 20, 1.0, n_paths=100)["p10_end"] < 1e6
assert cc.survival(r, 1e6, 1e6 * 0.15, 20, 1.0, n_paths=100)["p_survive"] < 0.05
def test_survival_drift_zero_non_sostiene_rendita(cc):
"""Random walk a drift ZERO: nessun prelievo perpetuo e' sostenibile (il criterio
p10 >= capitale iniziale deve fallire gia' a prelievo ~0)."""
rng = np.random.default_rng(0)
r = rng.normal(0.0, 0.15 / np.sqrt(365), 4000)
assert cc.survival(r, 1e6, 0.0, 20, 1.0, n_paths=500)["p10_end"] < 1e6
assert cc.survival(r, 1e6, 1e6 * 0.03, 20, 1.0, n_paths=500)["p_survive"] < 0.95
def test_survival_prelievo_nullo_non_fallisce_mai(cc):
"""Senza prelievi un book a rendimenti limitati non puo' andare in rovina."""
rng = np.random.default_rng(1)
r = rng.normal(0.0005, 0.01, 3000)
assert cc.survival(r, 1e6, 0.0, 20, 1.0, n_paths=300)["p_survive"] == 1.0
def test_leva_aumenta_la_vol_dei_path(cc):
"""La leva k scala i ritorni: e' l'assunzione dichiarata del confronto iso-rischio."""
rng = np.random.default_rng(2)
r = rng.normal(0.0003, 0.01, 2000)
p1 = cc._boot_paths(r, 200, 365, 20, np.random.default_rng(3))
p2 = cc._boot_paths(r, 200, 365, 20, np.random.default_rng(3)) * 2.0
assert np.isclose(p2.std(), 2.0 * p1.std(), rtol=1e-9)
# ------------------------------------------------------ 2) pavimento IB / banda
def test_ib_pavimento_penalizza_il_capitale_piccolo(cc):
"""Il costo IB e' FISSO per ordine: a capitale piccolo il drag esplode. Se questo test
fallisce, il modello e' tornato proporzionale e GTAA01 sembrera' eseguibile a $600."""
small = cc.metrics(cc.gtaa_realistic(600.0))
big = cc.metrics(cc.gtaa_realistic(50_000.0))
assert small["cagr"] < big["cagr"], "il pavimento fisso deve penalizzare il piccolo"
assert small["cagr"] < 0.0, "a $600 il ribilanciamento continuo su IB deve essere negativo"
def test_la_banda_salva_gtaa_a_capitale_piccolo(cc):
"""Con banda + cadenza settimanale, GTAA01 a $600 torna positivo. E' la condizione che
rende il 3o sleeve eseguibile a basso capitale."""
nudo = cc.metrics(cc.gtaa_realistic(600.0))
banda = cc.metrics(cc.gtaa_banded(600.0, cc.GTAA_BAND_USD, cc.GTAA_EVERY))
assert banda["sharpe"] > nudo["sharpe"] + 0.5
assert banda["cagr"] > 0.0
def test_tp01_deribit_insensibile_al_capitale_sopra_soglia(cc):
"""Su Deribit la fee e' proporzionale e non c'e' pavimento: sopra ~$500 lo Sharpe
realistico coincide col modellato (conferma della lezione 'min-order = banda ottimale')."""
a = cc.metrics(cc.tp01_realistic(1_000.0))["sharpe"]
b = cc.metrics(cc.tp01_realistic(50_000.0))["sharpe"]
assert abs(a - b) < 0.02
# --------------------------------------------------- 3) regressione bug HWM funded
@pytest.fixture(scope="module")
def pc():
return _load("r0725_prop_config", "scripts/research/r0725_prop_config.py")
def test_funded_payout_non_e_zero_regressione_hwm(pc):
"""REGRESSIONE: aggiornare la base di prelievo ogni giorno (come un HWM mobile) azzera
per costruzione il guadagno misurato al checkpoint mensile -> payout ~0. Con un book
che rende ~20%/anno e split 80% il payout atteso su $100k e' dell'ordine di $10k, NON
di $200. Questo test e' il guardiano di quell'errore."""
r = np.full(3000, 1.20 ** (1 / 365) - 1.0) # +20%/anno deterministico, mai in perdita
f = pc.sim_funded(r, max_loss=0.10, daily_loss=0.05, k=1.0, years=1, n_paths=200)
assert f["p_alive"] == 1.0, "un asset che sale ogni giorno non puo' bustare"
assert f["e_payout"] > 8_000.0, f"payout implausibilmente basso: {f['e_payout']:.0f}"
assert f["e_payout"] < 20_000.0
def test_funded_bust_su_max_loss_statico(pc):
"""Il max-loss e' STATICO dal saldo iniziale: un book che perde costantemente deve
bustare, e il payout deve essere zero."""
r = np.full(3000, -0.002)
f = pc.sim_funded(r, max_loss=0.06, daily_loss=0.05, k=1.0, years=1, n_paths=200)
assert f["p_alive"] == 0.0
assert f["e_payout"] == 0.0
def test_eval_target_irraggiungibile_non_passa(pc):
"""Sanity dell'eval: un book piatto non raggiunge mai il target di profitto."""
r = np.zeros(3000)
e = pc.sim_eval(r, target=0.10, max_dd=0.06, daily_loss=0.04, k=1.0, n_paths=200)
assert e["p_pass"] == 0.0
# ------------------------------------------------ 4) scala di conti funded (ladder)
@pytest.fixture(scope="module")
def pl():
return _load("r0725_prop_ladder", "scripts/research/r0725_prop_ladder.py")
def test_wick_calibrato_sui_percentili_del_recon(pl):
"""Il gap intraday deve riprodurre l'ordine di grandezza misurato dal recon MTM 1h del
24/07 (p50 ~-0.17pp, p90 ~-1pp, p99 ~-3pp). Se la calibrazione scivola, i breach diventano
finzione in una direzione o nell'altra."""
q = pl.wick_percentiles()
assert 0.0010 < q[0.50] < 0.0025
assert 0.005 < q[0.90] < 0.015
assert 0.015 < q[0.99] < 0.050
def test_lente_intraday_e_sempre_piu_severa_del_close_only(pl):
"""I wick possono solo AGGIUNGERE breach: la lente intraday non puo' produrre piu' reddito
di quella close-only. Se questo si inverte, il gap sta entrando col segno sbagliato."""
M, names = pl.sleeve_panel()
slots = list(pl.policies(names).values())[1]
close = pl.simulate(M, slots, n_paths=400, lev=1.0, seed=1, intraday=False)
intra = pl.simulate(M, slots, n_paths=400, lev=1.0, seed=1, intraday=True)
assert intra["p_target"] <= close["p_target"]
assert intra["p_zero"] >= close["p_zero"]
def test_conti_sullo_stesso_book_sono_perfettamente_correlati(pl):
"""Il punto centrale dello studio: N conti che girano lo STESSO book non diversificano.
Verifica strutturale sui pesi, non sull'output del MC."""
M, names = pl.sleeve_panel()
pols = pl.policies(names)
conc = pols["CONC-DIV ogni conto = book diversificato"]
assert all(np.allclose(s, conc[0]) for s in conc), "CONC deve avere slot identici"
sparso = pols["SPARSO 1 sleeve per conto (a rotazione)"]
assert not np.allclose(sparso[0], sparso[1]), "SPARSO deve avere slot diversi"
def test_senza_cassa_nessun_conto_e_nessun_reddito(pl):
"""Controllo di sanita' del meccanismo cassa->biglietto: a cassa zero non si compra nulla."""
M, names = pl.sleeve_panel()
slots = list(pl.policies(names).values())[0]
old = pl.START_EUR
try:
pl.START_EUR = 0.0
r = pl.simulate(M, slots, n_paths=200, lev=1.0, seed=2)
assert r["p_zero"] == 1.0
finally:
pl.START_EUR = old