Files
PythagorasGoal/tests/test_prop_coupled.py
Adriano Dal Pastro 4cb82dc474 research(prop): lente wick ACCOPPIATA — chiude il follow-up dichiarato del 25/07
La scala di conti funded era giudicata con due lenti a un ordine di grandezza di
distanza su P(>=50 EUR/g): 20.7% close-only vs 1.6-2.5% wick indipendente. Il
difetto era dichiarato (gap estratto indipendente dal rendimento del giorno) ma
il recon accoppiato esisteva solo per il book 75/25.

Qui si generalizza: sleeve TP01/SKH01 separati a risoluzione oraria (minimo
intraday esatto per qualsiasi vettore di pesi) + XS01 accoppiato dagli OHLC
giornalieri HL (recon = sleeve ufficiale a max|delta|=0.0).

FINDING: la calibrazione del wick era giusta, l'errore era l'INDIPENDENZA. Le
marginali coincidono (p50 -0.17pp identico) ma il gap e' ~3x piu' profondo nei
giorni che finiscono BENE (-1.58pp vs -0.48pp), perche' un giorno brutto chiude
sul minimo (m==R nel 26% dei giorni). Il breach si valuta sul minimo -> la lente
indipendente raddoppia i breach da daily-loss (2.0-2.9x, misurato sui giorni
storici). close-only non e' conservativa ma CIECA: 0.00% di breach ovunque.

Verifiche: (1) 1h vs 5m identici (p99 -3.06 vs -3.14pp) -> il caveat di
risoluzione del 24/07 e' trascurabile; (2) riconciliazione col 24/07 RISOLTA
(63.2% vs 58% sulla stessa finestra: non era la finestra, era la lente);
(3) bound severo su XS01 non ribalta config B.

Decisioni: funded 0.75x (non 0.50x — massimizza il payout a 3 anni, $2674 vs
$1230); scala P(>=50/g) ~6% con P(zero) 52-65%; ordine fra politiche invariato
a tutte e 3 le lenti. Book/pesi/cron INVARIATI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 19:30:03 +00:00

138 lines
5.8 KiB
Python

"""Test della lente WICK ACCOPPIATA (scripts/research/r0725_prop_coupled.py).
Blindano le proprieta' che rendono la lente *onesta* — sono le stesse che, se si rompessero,
farebbero tornare silenziosamente il difetto del 25/07 (gap indipendente dal giorno):
* il recon XS01 deve riprodurre ESATTAMENTE lo sleeve ufficiale (se diverge, si sta
misurando il wick di una strategia che non e' quella in produzione);
* il gap dev'essere <= 0 per costruzione (il minimo intraday non puo' stare sopra la chiusura);
* la dipendenza dal giorno dev'esserci (gap piu' profondo nei giorni buoni) — e' il fatto
su cui poggia tutta la conclusione;
* le tre lenti devono ordinarsi close >= coupled >= indip sui breach;
* il bound severo su XS01 dev'essere effettivamente piu' severo di quello condiviso.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
pytest.importorskip("pandas")
import r0725_prop_coupled as pc # noqa: E402
from src.portfolio.sleeves import _xsec_returns # noqa: E402
# --------------------------------------------------------------- fedelta' del recon
def test_xsec_recon_riproduce_lo_sleeve_ufficiale():
"""Se il recon XS01 divergesse dallo sleeve, misureremmo il wick di un'altra strategia."""
off = _xsec_returns().dropna().sort_index()
if off.index.tz is None:
off.index = off.index.tz_localize("UTC")
rec = pc.xsec_daily_tuples()
common = off.index.intersection(rec.index)
assert len(common) > 500
assert np.abs(off.reindex(common).values - rec["R"].reindex(common).values).max() < 1e-12
def test_crypto_recon_riproduce_il_book_7525_del_2407():
"""La decomposizione per-sleeve, ricombinata a 75/25, deve dare il book di r0724.
Tolleranza larga: r0724 fa il recon in un colpo solo, qui si somma dopo aver diviso
per i pesi — devono coincidere a meno dell'aritmetica in virgola mobile."""
R, G, I = pc.book_tuples(*pc.CFG_A)
assert len(R) > 2000
vol = R.std() * np.sqrt(365.0)
assert 0.08 < vol < 0.16, f"vol book live fuori scala: {vol:.3f}"
# i percentili del gap devono ritrovare quelli dichiarati dal 24/07 (-0.17 / -1.03 / -2.70)
p50, p90 = -np.percentile(-G, 50), -np.percentile(-G, 90)
assert -0.004 < p50 < -0.0005, p50
assert -0.020 < p90 < -0.004, p90
# --------------------------------------------------------------- proprieta' del gap
@pytest.mark.parametrize("cfg", [pc.CFG_A, pc.CFG_B, pc.CFG_XS])
def test_gap_sempre_non_positivo(cfg):
"""m <= R per costruzione: il minimo intraday non puo' stare sopra la chiusura."""
_, G, _ = pc.book_tuples(*cfg)
assert G.max() <= 1e-15
def test_gap_piu_profondo_nei_giorni_buoni():
"""IL fatto su cui poggia la conclusione. Se questo si invertisse, la lente indipendente
non sarebbe piu' sbilanciata e il follow-up perderebbe senso."""
R, G, _ = pc.book_tuples(*pc.CFG_A)
q = np.quantile(R, [0.1, 0.9])
gap_bad = G[R <= q[0]].mean()
gap_good = G[R >= q[1]].mean()
assert gap_good < gap_bad, (gap_good, gap_bad)
assert gap_good < 2.0 * gap_bad, "il gap nei giorni buoni dev'essere nettamente piu' profondo"
def test_una_quota_di_giorni_chiude_sul_minimo():
"""Nei giorni brutti la chiusura E' il minimo: e' il motivo per cui il gap indipendente
(che non lo sa) aggiunge un tuffo che non c'e'."""
_, G, _ = pc.book_tuples(*pc.CFG_A)
frac = float((G > -1e-12).mean())
assert 0.10 < frac < 0.60, frac
# --------------------------------------------------------------- ordinamento delle lenti
def test_le_lenti_si_ordinano_sui_breach():
"""close-only non vede mai il minimo -> meno breach di tutti; l'indipendente ne inventa
-> piu' della accoppiata. L'ordine e' la ragion d'essere dell'esercizio."""
R, G, _ = pc.book_tuples(*pc.CFG_A, start=pc.START_WIN)
rng = np.random.default_rng(0)
vol = R.std() * np.sqrt(365.0)
Gi = -np.exp(rng.normal(pc.pl.WICK_MU, pc.pl.WICK_SIGMA, size=(200, len(R)))) * np.clip(
vol / pc.pl.WICK_REF_VOL, 0.0, 3.0)
thr = 0.04
b_close = float((R < -thr).mean())
b_coup = float((R + G < -thr).mean())
b_indip = float((R[None, :] + Gi < -thr).mean())
assert b_close <= b_coup <= b_indip, (b_close, b_coup, b_indip)
assert b_indip > 1.3 * max(b_coup, 1e-9), "l'indipendente deve gonfiare in modo misurabile"
def test_bound_severo_xs01_e_davvero_piu_severo():
_, g_sh, _ = pc.book_tuples(*pc.CFG_B, xs_mode="shared")
_, g_pl, _ = pc.book_tuples(*pc.CFG_B, xs_mode="perleg")
assert g_pl.mean() < g_sh.mean()
assert g_pl.min() < g_sh.min()
def test_wick_mode_sconosciuto_alza_errore():
with pytest.raises(ValueError):
pc.xsec_daily_tuples("nonesiste")
# --------------------------------------------------------------- simulazione
def test_single_account_close_domina_coupled_domina_indip():
"""A parita' di tutto il resto, una lente piu' severa non puo' far sopravvivere di piu'."""
out = {ln: pc.single_account(pc.CFG_A, 1.0, ln, pc.START_WIN, n_paths=800)
for ln in ("close", "coupled", "indip")}
assert out["close"]["p_alive"] >= out["coupled"]["p_alive"] >= out["indip"]["p_alive"]
def test_leva_piu_alta_non_aumenta_la_sopravvivenza():
lo = pc.single_account(pc.CFG_B, 0.50, "coupled", pc.START_WIN, n_paths=800)
hi = pc.single_account(pc.CFG_B, 1.00, "coupled", pc.START_WIN, n_paths=800)
assert lo["p_alive"] > hi["p_alive"]
def test_book_tuples_non_condivide_lo_stato_fra_chiamate():
"""La cache restituisce copie: un chiamante che modifica l'array non deve avvelenare
il successivo (stesso guardrail messo su r0725_capcurve)."""
R1, _, _ = pc.book_tuples(*pc.CFG_A)
R1[:] = 999.0
R2, _, _ = pc.book_tuples(*pc.CFG_A)
assert not np.allclose(R2, 999.0)