research: leave-one-out del book DE-LUCKATO sulle ancore — la classifica si ribalta
Domanda: "quale sleeve terrei?". Il leave-one-out ovvio e' misurato all'ancora canonica di tutti e cinque gli sleeve, quindi NON e' credibile: un LOO e' un Delta, ed eredita la fortuna d'ancora come ogni Delta (lezione 26/07, altlib.anchor_luck_delta). Metodo: 2000 estrazioni uniformi indipendenti sullo spazio congiunto 24x10x7x23x5 = 193.200 configurazioni; book completo + 5 LOO alla STESSA configurazione; statistica = mediana delle differenze appaiate. Sanity bit-exact 5/5 (max|dif| = 0.0) contro gli sleeve di produzione. Quattro repliche ancorate riusate dagli audit 02/07-03/07, solo la fase di GTAA01 e' nuova (ed e' l'unica coperta da test dedicato). RISULTATI - Livello del book: la stima a occhio del 02/07 era ottimista su 3/3 metriche. FULL 2.222 (97.0 pctl) -> 1.95 | HOLD 2.364 (99.6 pctl) -> 1.54 [1.11, 1.91] | maxDD 6.07% (12.0 pctl) -> 6.85%. Solo 9 estrazioni su 2000 battono l'hold-out canonico. La somma delle fortune marginali NON e' la mediana congiunta: sbagliava di +0.82 di Sharpe. - SKH01 non e' il motore del book: l'ancora regala 2/3 del FULL, 70% dell'hold-out, 80% della protezione DD -> de-luckato e' il meno affidabile dei cinque. - GTAA01 e' l'unico positivo nel 100% delle estrazioni su tutte e tre le metriche e il miglior protettore di DD. Ma attribuzione != eseguibilita': sotto $3k resta non deployabile. - TP01 e' il maggior contributore (+0.390 FULL, 2000/2000) e il canonico lo SOTTOSTIMAVA. Il suo hold-out negativo non e' artefatto d'ancora (negativo nel 99.1%): e' la firma dell'assicurazione. Uno sleeve difensivo si giudica sul sinistro, non sul premio. - XS01: protezione DD esattamente zero (positiva nel 43% = moneta) -> diversificatore di rendimento, non di rischio. VRP01: 2a conferma di zero fortuna (canonico all'1.8 pctl). Corretto in sessione: un "pctl 100%" era arrotondamento di 99.55% con %.0f — un percentile a 0 decimali mente esattamente agli estremi, che sono l'unico posto dove lo si legge. Book, pesi, cron, config INVARIATI. Non e' un gate sui pesi (resta weights_tilt_null) e non e' evidenza out-of-sample: e' attribuzione, de-luckata. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
"""Test per r0726_loo_deluck.py — leave-one-out del book de-luckato sulle ancore.
|
||||
|
||||
Copre i due punti dove questo studio puo' rompersi in silenzio:
|
||||
(a) la REPLICA di fase di GTAA01 — l'unico generatore ancorato scritto nuovo (gli altri
|
||||
quattro sono riusati dagli audit 02/07 e 03/07, gia' verificati bit-exact li');
|
||||
(b) la STATISTICA APPAIATA — l'invariante che il 26/07 ha ribaltato un verdetto: la mediana
|
||||
delle differenze appaiate NON e' la differenza delle mediane.
|
||||
|
||||
I test su (b) girano su strutture sintetiche: sono logica, non dati, e devono restare veloci.
|
||||
"""
|
||||
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 r0726_loo_deluck as R # noqa: E402
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# (a) replica di fase di GTAA01
|
||||
# ===========================================================================
|
||||
def test_gtaa_fase_canonica_replica_lo_sleeve_di_produzione():
|
||||
"""La fase 0 deve riprodurre BIT-EXACT _gtaa_daily_returns(). Senza questo tutto lo
|
||||
studio misura una strategia diversa da quella del book."""
|
||||
from src.portfolio.portfolio import to_daily
|
||||
from src.portfolio.sleeves import _gtaa_daily_returns
|
||||
|
||||
mine = R.gtaa01_at(0)
|
||||
ref = to_daily(_gtaa_daily_returns())
|
||||
assert len(mine) == len(ref)
|
||||
assert mine.index.equals(ref.index)
|
||||
assert float(np.max(np.abs(mine.values - ref.values))) == 0.0
|
||||
|
||||
|
||||
def test_gtaa_le_fasi_sono_davvero_diverse():
|
||||
"""Controllo POSITIVO: se `ph` non entrasse nel calcolo, il test sopra passerebbe lo stesso
|
||||
e la banda d'ancora di GTAA01 sarebbe cinque copie della stessa serie (fortuna misurata 0
|
||||
per costruzione). Almeno una fase deve differire dalla canonica."""
|
||||
base = R.gtaa01_at(0)
|
||||
diffs = []
|
||||
for ph in (1, 2, 3, 4):
|
||||
other = R.gtaa01_at(ph)
|
||||
J = pd.concat({"a": base, "b": other}, axis=1, join="inner")
|
||||
diffs.append(float(np.max(np.abs(J["a"].values - J["b"].values))))
|
||||
assert max(diffs) > 1e-9, f"le 5 fasi di GTAA01 sono identiche: {diffs}"
|
||||
|
||||
|
||||
def test_gtaa_fase_fuori_range_non_ribilancia_mai():
|
||||
"""`i % 5 == 7` non e' mai vero -> la gamba resta a esposizione 0 per sempre. Serve a
|
||||
dimostrare che e' davvero la CADENZA a essere parametrizzata (e non un no-op)."""
|
||||
close = R.G._close("SPY")
|
||||
leg = R._gtaa_leg_phase("SPY", 1000.0, R.G.REBAL_BAND_USD, R.G.REBAL_EVERY, 7)
|
||||
assert len(leg) == len(close)
|
||||
assert float(np.max(np.abs(leg.values))) == 0.0
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# (b) statistica appaiata
|
||||
# ===========================================================================
|
||||
def _fake_draws(book_vals, loo_vals, name="TP01_trend_1d"):
|
||||
"""Costruisce la struttura che one_config() produce, con Δ = book - loo per estrazione."""
|
||||
return [{"book": (b, 0.0, 0.0), name: (b - l, 0.0, 0.0)}
|
||||
for b, l in zip(book_vals, loo_vals)]
|
||||
|
||||
|
||||
def test_mediana_appaiata_non_e_differenza_delle_mediane():
|
||||
"""L'INVARIANTE del 26/07, con numeri costruiti perche' i due metodi abbiano segno OPPOSTO.
|
||||
|
||||
book : [1.0, 2.0, 3.0] -> mediana 2.0
|
||||
senza : [0.5, 2.5, 2.9] -> mediana 2.5
|
||||
differenza delle mediane = -0.5 (direbbe: lo sleeve fa MALE)
|
||||
mediana delle differenze appaiate = mediana([+0.5, -0.5, +0.1]) = +0.1 (fa BENE)
|
||||
"""
|
||||
book = [1.0, 2.0, 3.0]
|
||||
loo = [0.5, 2.5, 2.9]
|
||||
draws = _fake_draws(book, loo)
|
||||
|
||||
appaiata = float(np.median(R.paired_deltas(draws, "TP01_trend_1d", 0)))
|
||||
ingenua = float(np.median(book) - np.median(loo))
|
||||
|
||||
assert appaiata == pytest.approx(0.1)
|
||||
assert ingenua == pytest.approx(-0.5)
|
||||
assert np.sign(appaiata) != np.sign(ingenua), "il caso di test non dimostra piu' il ribaltamento"
|
||||
|
||||
|
||||
def test_paired_deltas_preserva_ordine_e_metrica():
|
||||
draws = [{"book": (0, 0, 0), "X": (1.0, 10.0, 100.0)},
|
||||
{"book": (0, 0, 0), "X": (2.0, 20.0, 200.0)}]
|
||||
assert list(R.paired_deltas(draws, "X", 0)) == [1.0, 2.0]
|
||||
assert list(R.paired_deltas(draws, "X", 1)) == [10.0, 20.0]
|
||||
assert list(R.paired_deltas(draws, "X", 2)) == [100.0, 200.0]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# (c) semantica del leave-one-out sul book
|
||||
# ===========================================================================
|
||||
def _flat_series(vals, start="2020-01-01"):
|
||||
idx = pd.date_range(start, periods=len(vals), freq="1D", tz="UTC")
|
||||
return pd.Series(np.asarray(vals, float), index=idx)
|
||||
|
||||
|
||||
def test_one_config_e_appaiato_per_costruzione():
|
||||
"""res[n] deve essere il Δ calcolato alla STESSA configurazione, non a due configurazioni
|
||||
diverse. Si verifica ricalcolando a mano il book completo e quello senza uno sleeve."""
|
||||
rng = np.random.default_rng(7)
|
||||
n = 400
|
||||
series = {name: {0: _flat_series(rng.normal(0.0004, 0.01, n)), 1: _flat_series(rng.normal(0.0004, 0.01, n))}
|
||||
for name in R.SPEC}
|
||||
cfg = {name: (i % 2) for i, name in enumerate(R.SPEC)}
|
||||
res = R.one_config(series, cfg)
|
||||
|
||||
cols = {n_: series[n_][cfg[n_]] for n_ in R.SPEC}
|
||||
bf, bh, bd = R.book_stats(cols)
|
||||
assert res["book"] == (bf, bh, bd)
|
||||
|
||||
for name in R.SPEC:
|
||||
sub = {k: v for k, v in cols.items() if k != name}
|
||||
f, h, d = R.book_stats(sub)
|
||||
assert res[name][0] == pytest.approx(bf - f)
|
||||
assert res[name][1] == pytest.approx(bh - h)
|
||||
# sul maxDD il segno e' INVERTITO apposta: >0 = togliendolo il drawdown PEGGIORA
|
||||
assert res[name][2] == pytest.approx(d - bd)
|
||||
|
||||
|
||||
def test_sleeve_costante_a_zero_non_contribuisce_al_sharpe():
|
||||
"""Controllo positivo sul segno: uno sleeve che non fa nulla deve dare Δ ~0 sul Sharpe,
|
||||
non un contributo positivo per magia di rinormalizzazione."""
|
||||
rng = np.random.default_rng(11)
|
||||
n = 500
|
||||
names = list(R.SPEC)
|
||||
series = {nm: {0: _flat_series(rng.normal(0.0005, 0.008, n))} for nm in names}
|
||||
dead = names[-1]
|
||||
series[dead] = {0: _flat_series(np.zeros(n))}
|
||||
cfg = {nm: 0 for nm in names}
|
||||
res = R.one_config(series, cfg)
|
||||
# lo sleeve morto abbassa il Sharpe del book (diluisce): il suo Δ dev'essere <= 0
|
||||
assert res[dead][0] <= 1e-9
|
||||
|
||||
|
||||
def test_book_stats_usa_i_pesi_di_produzione():
|
||||
"""I pesi devono essere quelli del book attivo, e la somma dei cinque deve fare 1.0:
|
||||
se qualcuno cambia active_sleeves() e non questo modulo, il test lo dice."""
|
||||
from src.portfolio.sleeves import active_sleeves
|
||||
|
||||
live = {s.name: round(s.weight, 6) for s in active_sleeves()}
|
||||
assert live == R.WEIGHTS, f"pesi disallineati da active_sleeves(): {live} vs {R.WEIGHTS}"
|
||||
assert sum(R.WEIGHTS.values()) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_spec_copre_tutti_gli_sleeve_attivi_con_ancora_canonica_valida():
|
||||
from src.portfolio.sleeves import active_sleeves
|
||||
|
||||
assert set(R.SPEC) == {s.name for s in active_sleeves()}
|
||||
for name, (_gen, anchors, canon, _prod) in R.SPEC.items():
|
||||
assert canon in anchors, f"{name}: ancora canonica {canon} fuori dalla griglia"
|
||||
assert len(anchors) >= 5, f"{name}: griglia d'ancora troppo piccola per una banda"
|
||||
Reference in New Issue
Block a user