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>
This commit is contained in:
Adriano Dal Pastro
2026-07-27 14:14:39 +00:00
parent 3f0812c7fc
commit fd05307c96
13 changed files with 1963 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
"""Test del gate onesto sulla banda GTAA01 (scripts/research/r0727_gtaa_band_gate.py).
Congela i tre esiti del gate e — soprattutto — l'ANNUALIZZAZIONE. Una serie su giorni di borsa
passata a un annualizzatore a 365 esce con lo Sharpe ×1.20 e il CAGR ×1.45: e' l'errore del
25/07, ed e' l'unico difetto qui che produrrebbe numeri sbagliati ma plausibili.
`test_la_proposta_non_e_selezionata_sull_hold_out` e' il test di metodo: se un giorno la cella
proposta risultasse in rango MIGLIORE sull'hold-out che in-sample, sarebbe la firma della
selezione-sull'hold-out e la validazione andrebbe rifatta.
"""
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"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import r0727_gtaa_band_gate as BG # noqa: E402
@pytest.fixture(scope="module")
def grid():
"""Griglia ridotta alla sola cadenza della proposta: basta per i gate (B) e (C) e tiene
il test veloce. La griglia piena vive nello script."""
ref = BG.run_cell(1, 0.0, BG.CAP_REF)
out = {}
for frac in BG.FRAC_GRID:
s = BG.run_cell(BG.PROPOSTA[0], frac, BG.CAP_REF)
j = pd.concat([s, ref], axis=1, join="inner").dropna()
out[frac] = dict(series=s, met=BG.met(s),
corr=float(j.iloc[:, 0].corr(j.iloc[:, 1])))
return ref, out
# ===========================================================================
# annualizzazione — l'errore che produce numeri plausibili e sbagliati
# ===========================================================================
def test_annualizza_a_giorni_di_borsa():
assert BG.DPY == 252.0
def test_lo_sharpe_e_calcolato_a_252_non_a_365():
idx = pd.bdate_range("2020-01-01", periods=1000)
r = pd.Series(np.linspace(0.0004, 0.0006, 1000), index=idx)
v = r.values
atteso = v.mean() / v.std() * np.sqrt(252.0) # std di popolazione, come nel resto del progetto
assert BG.met(r)["sharpe"] == pytest.approx(atteso, rel=1e-12)
assert BG.met(r)["sharpe"] != pytest.approx(v.mean() / v.std() * np.sqrt(365.0), rel=1e-6)
# ===========================================================================
# la parametrizzazione: e' questa la proposta
# ===========================================================================
def test_la_banda_e_una_frazione_della_gamba_non_un_importo():
"""E' l'intera ragione della proposta: un importo assoluto ha effetti diversi a capitali
diversi, una frazione no."""
from src.portfolio.gtaa import EQ_UNIVERSE
for cap in (3_000.0, 10_000.0, 50_000.0):
assert 0.25 * cap / len(EQ_UNIVERSE) == pytest.approx(0.25 * cap / 6)
def test_la_produzione_usa_ancora_la_banda_ASSOLUTA():
"""Il gate valida una proposta che NON e' ancora in produzione. Se un giorno la produzione
passasse alla banda frazionaria, questo test si rompe e va riletto tutto il confronto."""
from src.portfolio.gtaa import REBAL_BAND_USD, REBAL_EVERY
assert (REBAL_EVERY, REBAL_BAND_USD) == BG.CANONICO == (5, 50.0)
# ===========================================================================
# (A) selezione — il gate di metodo
# ===========================================================================
def test_la_proposta_non_e_selezionata_sull_hold_out():
"""Rango in-sample <= rango hold-out: la proposta non migliora passando all'hold-out.
Il contrario sarebbe la firma della selezione-sull'hold-out."""
rows = []
for every in BG.EVERY_GRID:
for frac in BG.FRAC_GRID:
s = BG.run_cell(every, frac, BG.CAP_REF)
rows.append(dict(every=every, frac=frac,
sh_in=BG.met(s.loc[: BG.HOLDOUT])["sharpe"],
sh_oos=BG.met(s.loc[BG.HOLDOUT:])["sharpe"]))
G = pd.DataFrame(rows)
p = G[(G["every"] == BG.PROPOSTA[0]) & (G["frac"] == BG.PROPOSTA[1])].iloc[0]
rank_in = int((G["sh_in"] > p["sh_in"]).sum()) + 1
rank_oos = int((G["sh_oos"] > p["sh_oos"]).sum()) + 1
assert rank_in <= rank_oos, (
f"la proposta e' {rank_oos}a sull'hold-out ma {rank_in}a in-sample: "
"sta meglio dove non doveva essere guardata")
def test_l_hold_out_usato_e_quello_documentato_di_gtaa01():
"""2015+: l'OOS equity della validazione 22/06, indipendente dall'hold-out crypto."""
assert BG.HOLDOUT == "2015-01-01"
# ===========================================================================
# (B) deflated Sharpe
# ===========================================================================
def test_la_proposta_sopravvive_alla_deflazione(grid):
from altlib import deflated_sharpe
ref, cells = grid
all_sr = [c["met"]["sharpe"] for c in cells.values()]
s = cells[BG.PROPOSTA[1]]["series"]
dsr, _ = deflated_sharpe(BG.met(s)["sharpe"], all_sr, s.dropna().values, dpy=BG.DPY)
assert dsr >= 0.95
# ===========================================================================
# (C) tracking — e il modo di fallire, che NON e' il de-levering
# ===========================================================================
def test_al_25_percento_lo_sleeve_segue_ancora_se_stesso(grid):
_, cells = grid
assert cells[0.25]["corr"] >= BG.MIN_CORR
def test_oltre_il_25_percento_il_tracking_si_rompe(grid):
_, cells = grid
assert cells[0.40]["corr"] < BG.MIN_CORR
assert cells[0.60]["corr"] < cells[0.40]["corr"]
def test_la_banda_larga_non_de_leva(grid):
"""Il null de-levering classico NON morde qui: allargando la banda la vol non scende.
Se un giorno scendesse, il fallimento sarebbe di un altro tipo e il verdetto va riscritto."""
ref, cells = grid
vref = BG.met(ref)["vol"]
assert cells[0.60]["met"]["vol"] / vref > 0.95
def test_il_25_percento_e_al_bordo_non_al_centro(grid):
"""Onesta' sul margine: la proposta passa, ma di poco. Congelarlo evita che in futuro si
citi come un plateau largo."""
_, cells = grid
assert BG.MIN_CORR <= cells[0.25]["corr"] < BG.MIN_CORR + 0.02
# ===========================================================================
# perche' la proposta esiste: invarianza al capitale
# ===========================================================================
def test_la_banda_frazionaria_batte_quella_fissa_a_capitale_piccolo():
from src.portfolio.gtaa import gtaa_returns
cap = 3_000.0
frazionaria = BG.met(BG.run_cell(BG.PROPOSTA[0], BG.PROPOSTA[1], cap))["sharpe"]
fissa = BG.met(gtaa_returns(capital=cap, band_usd=50.0, every=5))["sharpe"]
assert frazionaria > fissa
def test_a_capitale_grande_le_due_configurazioni_convergono():
"""A $50k la banda fissa smette di essere una tassa: il vantaggio della proposta e' al
capitale PICCOLO, ed e' li' che va citato."""
from src.portfolio.gtaa import gtaa_returns
cap = 50_000.0
frazionaria = BG.met(BG.run_cell(BG.PROPOSTA[0], BG.PROPOSTA[1], cap))["sharpe"]
fissa = BG.met(gtaa_returns(capital=cap, band_usd=50.0, every=5))["sharpe"]
assert abs(frazionaria - fissa) < abs(
BG.met(BG.run_cell(BG.PROPOSTA[0], BG.PROPOSTA[1], 3_000.0))["sharpe"]
- BG.met(gtaa_returns(capital=3_000.0, band_usd=50.0, every=5))["sharpe"])