6f64845aa9
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>
228 lines
10 KiB
Python
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
|