Files
PythagorasGoal/tests/test_lumpsum_split.py
T
Adriano Dal Pastro fd05307c96 research(capitale): il lump-sum vale 2.45x, e la protezione dalla rovina non passa da GTAA01
Quattro filoni chiesti dall'operatore ("proposte"). Book, pesi, config: INVARIATI.

1. LUMP-SUM + VENUE (r0727_lumpsum_split.py). Tutte le traiettorie del 25-26/07 avevano
   START=600 cablato: mai misurato un versamento iniziale, mentre ~10k EUR stanno fermi
   altrove. Macchineria validata: con lump 0 riproduce IDENTICI i numeri del 26/07.
   - 10k EUR oggi e mai piu' nulla -> traguardo 17.2a, P 62%, rendita 61.58 EUR/g
   - equivalenza onesta: +154 EUR/mese per 13 anni = 24.523 EUR, cioe' 2.45x
     (la prima stesura misurava i versamenti risparmiati: numero giusto, domanda sbagliata)
   - col rischio venue: a 11.500$ lo split e' possibile (quota IB 26%, non 25%) e taglia
     P(perso tutto) da 18.4% a 3.5% a p=1%, costando 1.9-2.6pp di P(arrivare)
   - SPLIT-CASSA: seconda gamba ferma costa altri 0.6-0.8pp e protegge IDENTICO
     -> la protezione non e' bloccata dal PRIIPs: serve un CONTO, non uno sleeve

2. FEE WATCH (scripts/live/fee_watch.py). Nuovo schema Deribit dal 1 agosto senza numeri
   pubblicati -> sorvegliante invece di promemoria. Legge il tier base dall'endpoint
   pubblico (oggi taker 5.00 bps), applica la regola congelata e allerta sui cambiamenti.

3. MONITOR HEALTH (src/live/monitor_health.py). Tre gate pre-registrati si decidono su
   serie forward di cui una sola era sorvegliata. Misura coda E buchi interni: una serie
   bucata ma fresca passa qualunque guardia di freschezza.

4. BANDA GTAA01 25% VALIDATA (r0727_gtaa_band_gate.py). 30 celle, 29.9 anni, dpy=252.
   Non e' selection-on-holdout (4/30 IS, 5/30 OOS), DSR 0.999, tracking OK ma AL BORDO.
   Il modo di fallire non e' il de-levering (la vol non scende) ma la perdita di tracking.
   Impatto sul book: zero -> REBAL_BAND_USD non toccato, si applica al deploy.

Aggiunto anche il bullet edge_watch, cablato il 26/07 e mai finito in CLAUDE.md.

Test: 56 nuovi, 504/504 verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 14:14:39 +00:00

154 lines
6.6 KiB
Python

"""Test del filone lump-sum + split di venue (scripts/research/r0727_lumpsum_split.py).
Il test piu' importante e' `test_riproduce_esattamente_i_numeri_del_26_07`: la macchineria e' una
GENERALIZZAZIONE di `r0726_venue_risk.simulate` (capitale iniziale e deposito diventano
parametri). Se la generalizzazione avesse cambiato anche solo l'ordine di consumo dell'RNG, i
numeri pubblicati e quelli nuovi non sarebbero piu' confrontabili — e la tabella nuova
sembrerebbe comunque plausibile.
Il secondo che conta e' `test_sotto_la_soglia_lo_split_non_esiste`: uno 'split' in cui la seconda
gamba e' sotto il minimo eseguibile non e' uno split, e' cash su un secondo conto — che protegge
dal fallimento del venue ma NON e' lo sleeve che i numeri assumono.
"""
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"))
import r0727_lumpsum_split as LS # noqa: E402
@pytest.fixture(scope="module")
def series():
"""Serie sintetiche a drift noto: i test sulla struttura non devono dipendere dal feed."""
idx = pd.date_range("2020-01-01", periods=1200, freq="D", tz="UTC")
rng = np.random.default_rng(7)
return {
"Deribit": pd.Series(rng.normal(0.0005, 0.008, len(idx)), index=idx),
"IB": pd.Series(rng.normal(0.0002, 0.004, len(idx)), index=idx),
"Hyperliquid": pd.Series(rng.normal(0.0003, 0.010, len(idx)), index=idx),
}
# ===========================================================================
# la replica — senza questa, la tabella nuova non e' confrontabile con quella vecchia
# ===========================================================================
def test_riproduce_esattamente_i_numeri_del_26_07(series):
import r0726_venue_risk as VR
cfg = {"Deribit": 1.0}
kw = dict(n_paths=200, years=3, seed=LS.SEED)
mine = LS.simulate(cfg, series, 0.01, lump_usd=0.0, dep_eur=VR.DEP_EUR, **kw)
theirs = VR.simulate(cfg, series, 0.01, **kw)
assert mine["p_reach"] == theirs["p_reach"]
assert mine["p_all_dead"] == theirs["p_all_dead"]
assert mine["median_end"] == theirs["median_end"]
def test_il_deposito_di_default_e_quello_delle_traiettorie_pubblicate(series):
"""€250/mese: la colonna con cui si confronta tutto il piano."""
import inspect
import r0726_venue_risk as VR
assert inspect.signature(LS.simulate).parameters["dep_eur"].default == VR.DEP_EUR
# ===========================================================================
# il lump-sum
# ===========================================================================
def test_un_lump_sum_non_puo_peggiorare_il_traguardo(series):
kw = dict(n_paths=300, years=5, seed=1)
senza = LS.simulate({"Deribit": 1.0}, series, 0.0, lump_usd=0.0, **kw)
con = LS.simulate({"Deribit": 1.0}, series, 0.0, lump_usd=10_000.0, **kw)
assert con["p_reach"] >= senza["p_reach"]
assert con["median_end"] > senza["median_end"]
def test_con_un_conto_solo_la_rovina_non_dipende_dal_capitale(series):
"""Fatto strutturale, non aneddoto: con UN venue 'almeno un fallimento' COINCIDE con
'perso tutto', e quella probabilita' e' una proprieta' del TEMPO di esposizione. Se un
giorno questa uguaglianza si rompesse, il modello di rovina e' cambiato."""
kw = dict(n_paths=400, years=5, seed=3)
a = LS.simulate({"Deribit": 1.0}, series, 0.02, lump_usd=0.0, **kw)
b = LS.simulate({"Deribit": 1.0}, series, 0.02, lump_usd=20_000.0, **kw)
assert a["p_all_dead"] == b["p_all_dead"]
def test_senza_deposito_e_senza_lump_non_si_arriva(series):
r = LS.simulate({"Deribit": 1.0}, series, 0.0, lump_usd=0.0, dep_eur=0.0,
n_paths=200, years=5, seed=2)
assert r["p_reach"] == 0.0
# ===========================================================================
# lo split, e la soglia che lo rende reale
# ===========================================================================
def test_sotto_la_soglia_lo_split_non_esiste():
"""A $6.050 servirebbe il 50% su IB per fare $3.000: oltre il tetto, quindi None.
Dichiararlo 'possibile' vorrebbe dire modellare uno sleeve che non e' eseguibile."""
assert LS.split_config(600.0) is None
assert LS.split_config(6_050.0) is None
def test_sopra_la_soglia_e_il_capitale_a_scegliere_la_quota():
cfg = LS.split_config(11_500.0)
assert cfg is not None
assert cfg["IB"] == pytest.approx(3_000.0 / 11_500.0, rel=1e-9) # ~26%, non il 25% preferito
assert cfg["Deribit"] + cfg["IB"] == pytest.approx(1.0)
def test_a_capitale_ampio_si_torna_alla_quota_raccomandata():
cfg = LS.split_config(50_000.0)
assert cfg["IB"] == LS.IB_SHARE
def test_la_quota_ib_non_supera_mai_il_tetto():
for tot in (8_600.0, 10_000.0, 12_000.0, 100_000.0):
cfg = LS.split_config(tot)
if cfg:
assert cfg["IB"] <= LS.IB_SHARE_MAX + 1e-12
def test_due_conti_riducono_la_rovina(series):
kw = dict(n_paths=600, years=6, seed=5, lump_usd=10_900.0)
conc = LS.simulate({"Deribit": 1.0}, series, 0.05, **kw)
split = LS.simulate(LS.split_config(11_500.0), series, 0.05, **kw)
assert split["p_all_dead"] < conc["p_all_dead"]
def test_la_rovina_non_dipende_da_cosa_sta_sulla_seconda_gamba(series):
"""SPLIT-GTAA e SPLIT-CASSA devono avere la stessa P(perso tutto): la protezione viene dal
numero di conti, non dai loro ritorni. E' la lettura centrale della sezione 2-bis."""
kw = dict(n_paths=500, years=6, seed=11, lump_usd=10_900.0)
cfg = LS.split_config(11_500.0)
zero = {**series, "IB": series["IB"] * 0.0}
assert (LS.simulate(cfg, series, 0.03, **kw)["p_all_dead"]
== LS.simulate(cfg, zero, 0.03, **kw)["p_all_dead"])
# ===========================================================================
# costi dichiarati
# ===========================================================================
def test_haircut_toglie_il_drift_dichiarato(series):
s = LS.haircut(series["IB"], 0.009)
drift = (series["IB"].mean() - s.mean()) * 365
assert drift == pytest.approx(0.009, rel=1e-9)
def test_il_haircut_ib_somma_taglia_piccola_e_veicolo_ucits():
"""0.8pp (CAGR 3.8%->3.0% a $3k, misurato 25/07) + 0.1pp (drag UCITS, misurato 26/07)."""
assert LS.IB_HAIRCUT_YR == pytest.approx(0.009)
def test_il_bersaglio_e_lo_stesso_per_entrambi_i_bracci():
"""Conservativo CONTRO lo split (che avrebbe un muro leggermente diverso): se un giorno si
usassero due bersagli, il confronto smetterebbe di essere leggibile."""
import r0726_venue_risk as VR
assert LS.TARGET == VR.TARGET