4cb82dc474
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>
138 lines
5.8 KiB
Python
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)
|