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