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:
@@ -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
|
||||
Reference in New Issue
Block a user