Files
PythagorasGoal/tests/test_capcurve.py
T
Adriano Dal Pastro 6f64845aa9 research(hyro): HyroTrader nello specifico — consistency 40% refutata, DD 6% e' il vincolo
Addendum all'ondata rendita, su richiesta dell'operatore ("e hyrotrader").

CONSISTENCY 40%: ipotesi REFUTATA. Avevo previsto una tagliola (SKH01 ha P&L a scalino,
TP01 fa strappi di trend). Misurato: costo ~0pp a ogni leva e in entrambe le lenti,
perche' il book accumula il 10% in 130-300 giorni -> nessun giorno si avvicina al 40%
del cumulato. Prima di dichiararlo ho dovuto PROVARE che il controllo funziona (un
"costo 0" puo' essere un bug): su un book con profitto concentrato in 1-2 giorni la
regola porta P(pass) da >90% a <5%. Due iterazioni di test prima di avere un
discriminante valido (strappi ripetuti si diluiscono; sim_eval fa block-bootstrap,
non replay).

IL VINCOLO VERO E' IL maxDD 6% STATICO. Config A (book live TP01/SKH01) ha maxDD 9.5%
> 6% = strutturalmente incompatibile a leva piena: intraday a 1.0x sopravvive l'1.4%.
Config B (+XS01) ha maxDD 6.2% e Sharpe 1.13.

RACCOMANDAZIONE: config B a leva funded 0.50x (intraday P(vivo) 71% vs 32% a 0.75x;
E[payout] €7.69 vs €8.66/g — la sopravvivenza COMPONE su piu' anni, un conto bustato
smette di produrre per sempre). EV del biglietto $100k positivo in entrambe le lenti
(+$6.789 close-only, +$1.166 intraday), con ~69% di perdere la fee nella lente
pessimista. Cap $200k/trader => ~€15-30/g max: per €50/g servono piu' firm.

DISCREPANZA DICHIARATA col 24/07 (P(vivo) A@0.75x: 58% loro, 10% mio): il mio wick e' a
estrazione indipendente (piu' severo) e la finestra 2024+ e' quella dove A e' debole.
La direzione concorda (meno leva e' meglio); sul LIVELLO il numero del 24/07 (recon MTM
vero) e' piu' affidabile del mio.

Piu' l'addendum "10k su IB" al diario (analisi gia' committata in ea6743d).

Test: 217 pass (+3: consistency morde/non-morde, DD statico binding).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 17:10:26 +00:00

228 lines
10 KiB
Python

"""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
# ------------------------------------------- 5) regola di CONSISTENCY HyroTrader
@pytest.fixture(scope="module")
def hy():
return _load("r0725_hyro", "scripts/research/r0725_hyro.py")
def test_consistency_MORDE_su_un_book_grumoso(hy):
"""PROVA CHE IL CONTROLLO FUNZIONA. Un UNICO giorno da +12% porta da solo tutto il profitto
e non arriva mai altro: con la regola 40% quel path non deve passare MAI (0.12 > 0.40*0.12),
senza regola passa subito. Se questo test non discrimina, il 'costo ~0pp' misurato sul book
vero sarebbe un bug, non un risultato.
NB sul probe: sim_eval fa BLOCK BOOTSTRAP (blocchi di 20g), non replay -> uno strappo isolato
finirebbe solo in alcuni path. Qui gli strappi sono spaziati di 20 giorni = esattamente uno per
blocco, cosi' OGNI path ne vede uno ogni 20 giorni in modo deterministico. In 40 giorni:
senza regola si passa al 1o strappo (+12% >= target 10%); con la regola servono 3 strappi
perche' il giorno migliore scenda sotto il 40% del profitto cumulato -> oltre la finestra.
"""
r = np.zeros(200)
r[::20] = 0.12
senza = hy.sim_eval(r, 1.0, consistency=False, intraday=False, max_days=40, n_paths=200)
con = hy.sim_eval(r, 1.0, consistency=True, intraday=False, max_days=40, n_paths=200)
assert senza["p_pass"] > 0.9, "senza regola un +12% secco deve passare il target 10%"
assert con["p_pass"] < 0.05, "la regola 40% deve bloccare un profitto concentrato in 1-2 giorni"
def test_consistency_non_morde_su_un_book_liscio(hy):
"""Controprova: un book che accumula il target in tanti giorni piccoli non e' toccato
dalla regola. E' il motivo per cui sul book vero il costo misurato e' ~0."""
r = np.full(400, 0.0008) # ~+0.08%/g: il 10% arriva in ~120 giorni
senza = hy.sim_eval(r, 1.0, consistency=False, intraday=False, n_paths=200)
con = hy.sim_eval(r, 1.0, consistency=True, intraday=False, n_paths=200)
assert abs(con["p_pass"] - senza["p_pass"]) < 0.02
def test_dd_statico_6pct_e_binding(hy):
"""Il maxDD 6% STATICO di HYRO e' il vincolo vero: un book con DD storico sopra il 6%
non puo' girare a leva piena. Sanity: alzando la leva la sopravvivenza funded scende."""
cfg = hy.configs()
r = list(cfg.values())[0].values.astype(float)
lo = hy.sim_funded(r, 0.5, False, 100_000.0, n_paths=400)["p_alive"]
hi = hy.sim_funded(r, 1.0, False, 100_000.0, n_paths=400)["p_alive"]
assert lo > hi