crolli: il libro perde nel giorno e chiude > 0 in 8/8 finestre dal 2022 (SKH01 short); crollo catturato a vol bassa, XRP diluisce come SOL

Quattro misure (r0909_*), nessun cambio a libro/pesi/config:
- libro nei crolli: −0,48%/g nei 160 giorni ≤ −5%, positivo per finestra dal 2022
  per la gamba short di SKH01 (108% dei guadagni); beta 0,0769 riprodotto (§46);
  il peso di SKH01 resta chiuso dal gate (in-sample).
- crollo catturato 1-5/06/2026 a quote vere: f_net 0,74 = rally; put δ−0,10
  1,92× il modello; a vol bassa e fuori dal gate di VRP01 → §3 non si riapre.
- XRP terza gamba (harness r0822_sol_leg): hold-out −0,169 in 0/24, un anno buono.
- universo Deribit: liquidi solo BTC/ETH/XRP/SOL; XS01 13/19; BTCDVOL future
  non negoziabile; PAXG non misurato.
Revisione fable: due conclusioni smontate (MTM 2,6× era un artefatto di quote;
«8/12 guadagna» era l'ordine delle classi). Debito §5.18: cblib.spot_series
+ asof guarda un'ora avanti (feed 1h etichettato all'apertura).
Test Opus: 92 nuovi, suite 1008/1008; due difetti del verdetto XRP corretti.
Docs: diario, RESULTS §74-77, CLAUDE.md, memoria 20/50, README; journal 07-08/09.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zJjHUS7mf4pnGE6pq9RTt
This commit is contained in:
Adriano Dal Pastro
2026-09-09 16:33:54 +00:00
parent 9f55652d99
commit bb40e87d13
16 changed files with 3138 additions and 10 deletions
+508
View File
@@ -0,0 +1,508 @@
"""Test di `scripts/research/r0909_crash_catturato.py` (2026-09-09) — il `f` di stress su quote vere.
Questo script porta l'UNICA condizione che riapre la decisione vincolante del 19/06 ("niente
short-vol da modello in deploy"): un `f` di stress reale misurato su un crash catturato. Un
numero con quel peso ha bisogno di guardie sul MECCANISMO, non solo sull'esito:
* il prezzatore vettorizzato dev'essere *lo stesso* di `cblib.bs_put`, elemento per elemento e
nei casi degeneri (T≤0, sig=0 → intrinseco): se diverge, il denominatore di ogni `f` e' un
altro modello e il rapporto non significa piu' niente;
* **lo spot dev'essere CAUSALE.** Il feed 1h e' etichettato all'APERTURA: la barra 20:00 chiude
alle 21:00, quindi `S.asof(ts)` guardava un'ora AVANTI. `spot_causale` sposta l'indice di
+1h. E' un look-ahead di un'ora dentro una misura di stress — piccolo e letale;
* **il costo di chiusura si TAGLIA alla larghezza.** Uno spread a rischio definito non costa
mai piu' della larghezza per chiudersi: si lascia regolare. Senza il taglio il "peggior MTM"
era un artefatto di quote (2,6× il modello; col taglio 1,08);
* la geometria fuori tolleranza va MARCATA, non aggiustata: `pick_legs` prende sempre la delta
piu' vicina, quindi senza `in_geom` una griglia rada produrrebbe silenziosamente un'altra
struttura (e' cio' che ha reso BTC non misurabile);
* un `f` fra un reale negativo e un modello positivo NON e' un `f`: dev'essere NaN (P12).
I test sintetici costruiscono quote a mano, dove ogni numero atteso si calcola a matita. Il test
d'integrazione (~11 s) riproduce la misura pubblicata su ETH.
"""
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"))
import cblib as CB # noqa: E402
import r0909_crash_catturato as C # noqa: E402
INS_S = "ETH-19JUN26-1900-P"
INS_L = "ETH-19JUN26-1600-P"
K_S, K_L = 1900.0, 1600.0
LARGHEZZA = K_S - K_L
TS0 = pd.Timestamp("2026-06-05 00:00", tz="UTC")
ORE = pd.date_range(TS0, periods=4, freq="h")
# ------------------------------------------------------------------ impalcature sintetiche
def _serie_spot(entrata: float = 2000.0, scadenza: float | None = None) -> pd.Series:
"""Spot orario dal 01/06 a EXP: `entrata` fino a 12h dopo l'ingresso, poi `scadenza`
(cosi' `S.asof(EXP)` e' pilotabile senza toccare le quote della finestra d'ingresso)."""
idx = pd.date_range(pd.Timestamp("2026-06-01", tz="UTC"), C.EXP, freq="h")
fine = TS0 + pd.Timedelta(hours=12)
v = np.where(idx <= fine, entrata, entrata if scadenza is None else scadenza)
return pd.Series(v.astype(float), index=idx)
def _serie_dvol(dvol: float = 50.0) -> pd.Series:
idx = pd.date_range(pd.Timestamp("2026-06-01", tz="UTC"), C.EXP, freq="h")
return pd.Series(float(dvol), index=idx)
def _seg(bid_s=0.030, ask_s=0.032, bid_l=0.010, ask_l=0.012,
ore=ORE, salta_lunga=()) -> pd.DataFrame:
"""Tabella di quote (ts × strumento). `salta_lunga` toglie la gamba LUNGA a quelle ore:
e' il caso "una gamba sola", che non e' meta' struttura ma un'altra strategia."""
righe = []
for k, t in enumerate(ore):
righe.append(dict(ts=t, instrument_name=INS_S, bid=bid_s, ask=ask_s, iv=48.0 + k))
if k not in salta_lunga:
righe.append(dict(ts=t, instrument_name=INS_L, bid=bid_l, ask=ask_l, iv=57.0 + k))
return pd.DataFrame(righe)
def _quote(bid_s=0.030, ask_s=0.032, bid_l=0.010, ask_l=0.012, salta_lunga=()) -> C.Quote:
return C.Quote(_seg(bid_s, ask_s, bid_l, ask_l, salta_lunga=salta_lunga),
_serie_spot(), _serie_dvol())
def _snap(d_short: float, d_long: float, bid_s=0.030, ask_s=0.032,
bid_l=0.010, ask_l=0.012) -> pd.DataFrame:
return pd.DataFrame([
dict(instrument_name=INS_S, strike=K_S, delta=d_short,
bid=bid_s, ask=ask_s, iv=48.0, mid=(bid_s + ask_s) / 2),
dict(instrument_name=INS_L, strike=K_L, delta=d_long,
bid=bid_l, ask=ask_l, iv=57.0, mid=(bid_l + ask_l) / 2),
])
def _trade(d_short: float, d_long: float, bid_s=0.030, ask_s=0.032, bid_l=0.010,
ask_l=0.012, spot=2000.0, ST=None) -> dict | None:
S, V = _serie_spot(spot, ST), _serie_dvol()
Q = C.Quote(_seg(bid_s, ask_s, bid_l, ask_l), S, V)
return C.trade("ETH", TS0, _snap(d_short, d_long, bid_s, ask_s, bid_l, ask_l), Q, S, V)
# ===========================================================================================
# spot_causale — il look-ahead di un'ora
# ===========================================================================================
def test_spot_causale_restituisce_l_ultima_chiusura_CONOSCIUTA():
"""🚨 Il feed 1h e' etichettato all'APERTURA: la barra 01:00 chiude alle 02:00. Senza lo
spostamento, `asof(02:00)` restituiva la chiusura delle 02:00 — cioe' un'ora di futuro
dentro un ingresso. DEVE FALLIRE SE: qualcuno "semplifica" togliendo lo shift.
⚠️ `cblib.spot_series` ha ancora il difetto (e quindi il f 0,714 di §11): dichiarato nel
docstring dello script, non toccato qui."""
idx = pd.date_range("2026-06-01", periods=5, freq="h", tz="UTC")
S = pd.Series([10.0, 11.0, 12.0, 13.0, 14.0], index=idx)
Sc = C.spot_causale(S)
assert list(Sc.index) == list(idx + pd.Timedelta(hours=1))
assert Sc.asof(idx[2]) == 11.0, "a 02:00 e' nota la chiusura della barra 01:00"
assert S.asof(idx[2]) == 12.0, "il difetto che la funzione ripara (controllo positivo)"
assert np.isnan(Sc.asof(idx[0])), "prima della prima chiusura nota non c'e' spot, non c'e' 10.0"
assert Sc.iloc[0] == 10.0 and S.iloc[0] == 10.0, "i VALORI non si toccano, solo le etichette"
# ===========================================================================================
# bs_put_vec == cblib.bs_put, elemento per elemento
# ===========================================================================================
def test_bs_put_vec_e_IDENTICA_a_cblib_bs_put_elemento_per_elemento():
"""DEVE FALLIRE SE: la vettorizzata devia dal prezzatore del sleeve — il denominatore di
ogni `f` smetterebbe di essere "lo stesso trade nel modello"."""
S = np.array([2400.0, 2000.0, 1900.0, 1800.0, 1500.0, 900.0])
T = np.array([0.10, 0.0383, 0.0192, 0.0027, 0.0027, 0.20])
sig = np.array([0.35, 0.50, 0.65, 1.10, 0.50, 0.05])
for K in (K_S, K_L, 2200.0):
v = C.bs_put_vec(S, K, T, sig)
atteso = [CB.bs_put(float(s), K, float(t), float(g)) for s, t, g in zip(S, T, sig)]
assert v == pytest.approx(atteso, rel=0, abs=1e-10), K
def test_bs_put_vec_a_T_o_sigma_nulli_da_l_INTRINSECO():
"""I casi degeneri entrano nel conto: all'ora di scadenza T=0 e il valore e' il payoff, non
un prezzo. DEVE FALLIRE SE: qualcuno toglie la maschera `ok` e lascia passare log/0 o NaN
dentro `mod_spread`."""
S = np.array([2400.0, 1900.0, 1500.0, 1000.0])
for T, sig in ((np.zeros(4), np.full(4, 0.5)),
(np.full(4, -1.0), np.full(4, 0.5)),
(np.full(4, 0.05), np.zeros(4))):
v = C.bs_put_vec(S, K_S, T, sig)
assert v == pytest.approx(np.maximum(K_S - S, 0.0), abs=1e-12)
assert np.isfinite(v).all()
# ===========================================================================================
# Quote + percorso su quote sintetiche
# ===========================================================================================
def test_percorso_costa_lo_spread_nel_VERSO_GIUSTO():
"""chiudi_short = (ASK corta BID lunga)·spot: si ricompra cio' che si e' venduto e si
rivende cio' che si e' comprato, cioe' si attraversa lo spread due volte.
liquida_long = (BID corta ASK lunga)·spot e' lo specchio.
DEVE FALLIRE SE: qualcuno "corregge" i lati — a mid il percorso costerebbe meno e il fatto
misurato si ammorbidirebbe da solo."""
Q = _quote(salta_lunga=(1,))
assert list(Q.bid.index) == list(ORE)
assert Q.spot.iloc[0] == 2000.0
assert Q.dvol.iloc[0] == pytest.approx(0.50), "il DVOL entra in frazione, non in punti"
p = C.percorso(Q, ORE[0] - pd.Timedelta(hours=1), INS_S, INS_L, K_S, K_L)
assert len(p) == 3, "l'ora con la sola gamba corta dev'essere ESCLUSA"
assert list(p["ts"]) == [ORE[0], ORE[2], ORE[3]]
assert p["ts"].is_monotonic_increasing
assert p["chiudi_grezzo"].tolist() == pytest.approx([(0.032 - 0.010) * 2000.0] * 3)
assert p["chiudi_short"].tolist() == pytest.approx([(0.032 - 0.010) * 2000.0] * 3), (
"dentro la larghezza il taglio non deve mordere: sarebbe un costo inventato")
assert p["liquida_long"].tolist() == pytest.approx([(0.030 - 0.012) * 2000.0] * 3)
assert p["liquida_put"].tolist() == pytest.approx([0.010 * 2000.0] * 3)
assert p["iv_s"].tolist() == pytest.approx([48.0, 50.0, 51.0])
assert p["iv_l"].tolist() == pytest.approx([57.0, 59.0, 60.0])
assert p["spot"].tolist() == pytest.approx([2000.0] * 3)
# il valore MODELLATO e' quello del sleeve, agli stessi strike e alla stessa ora
for i, t in enumerate([ORE[0], ORE[2], ORE[3]]):
T = max((C.EXP - t).total_seconds(), 0.0) / 86400.0 / 365.25
atteso = CB.bs_put(2000.0, K_S, T, 0.5) - CB.bs_put(2000.0, K_L, T, 0.5)
assert p["mod_spread"].iloc[i] == pytest.approx(atteso, abs=1e-10)
assert p["mod_put"].iloc[i] == pytest.approx(CB.bs_put(2000.0, K_L, T, 0.5), abs=1e-10)
def test_percorso_TAGLIA_il_costo_di_chiusura_alla_LARGHEZZA():
"""🚨 Uno spread a rischio definito non puo' costare piu' della larghezza per chiudersi: si
lascia regolare. A T→0 le quote arrivano al 158% di spread relativo, e senza il taglio il
"peggior MTM" era un artefatto di book (2,6× il modello, corretto in revisione a 1,08).
`chiudi_grezzo` resta accanto perche' il taglio va CONTATO, non nascosto (P12)."""
Q = _quote(bid_s=0.005, ask_s=0.200, bid_l=0.010, ask_l=0.012)
p = C.percorso(Q, ORE[0] - pd.Timedelta(hours=1), INS_S, INS_L, K_S, K_L)
assert p["chiudi_grezzo"].tolist() == pytest.approx([(0.200 - 0.010) * 2000.0] * 4) # 380
assert p["chiudi_short"].tolist() == pytest.approx([LARGHEZZA] * 4) # 300
assert (p["chiudi_short"] <= LARGHEZZA + 1e-12).all()
assert (p["chiudi_grezzo"] > p["chiudi_short"]).all(), "il grezzo deve restare visibile"
# la gamba lunga liquidata non puo' valere meno di zero
assert p["liquida_long"].tolist() == pytest.approx([0.0] * 4)
def test_percorso_taglia_liquida_long_anche_in_ALTO():
"""L'altro estremo della stessa regola: un debit spread non vale piu' della sua larghezza."""
Q = _quote(bid_s=0.200, ask_s=0.210, bid_l=0.005, ask_l=0.010)
p = C.percorso(Q, ORE[0] - pd.Timedelta(hours=1), INS_S, INS_L, K_S, K_L)
assert p["liquida_long"].tolist() == pytest.approx([LARGHEZZA] * 4)
def test_percorso_parte_DOPO_l_ingresso_e_non_lo_include():
"""Il mark-to-market comincia all'ora successiva: includere l'ora d'ingresso metterebbe
dentro un P&L noto a priori (il mezzo spread pagato entrando)."""
Q = _quote()
p = C.percorso(Q, ORE[0], INS_S, INS_L, K_S, K_L)
assert list(p["ts"]) == [ORE[1], ORE[2], ORE[3]]
assert C.percorso(Q, ORE[-1], INS_S, INS_L, K_S, K_L).empty
def test_percorso_e_vuoto_se_uno_strumento_non_esiste_nella_tabella():
"""P5: "non vedo" ha uno stato suo (DataFrame vuoto → `trade` ritorna None), non un valore
inventato."""
Q = _quote()
assert C.percorso(Q, ORE[0], INS_S, "ETH-19JUN26-9999-P", K_S, 9999.0).empty
assert C.percorso(Q, ORE[0], "ETH-19JUN26-9999-P", INS_L, 9999.0, K_L).empty
def test_Quote_scarta_le_quote_NON_quotate():
"""bid o ask a zero (o assenti) non sono prezzi: entrano come "non quotato", non come 0."""
seg = _seg()
seg.loc[seg["instrument_name"] == INS_L, "bid"] = 0.0 # gamba lunga senza bid
Q = C.Quote(seg, _serie_spot(), _serie_dvol())
assert INS_L not in Q.bid.columns
assert C.percorso(Q, ORE[0], INS_S, INS_L, K_S, K_L).empty
# ===========================================================================================
# la guardia di geometria in_geom (TOL_D)
# ===========================================================================================
def test_in_geom_e_FALSO_quando_le_delta_disponibili_sono_lontane():
"""`pick_legs` prende SEMPRE la delta piu' vicina: su una griglia rada restituisce una
struttura che non e' VRP01 e non lo dice. `in_geom` e' il pezzo che lo dice.
DEVE FALLIRE SE: qualcuno allarga TOL_D o toglie il flag — i 78 snapshot BTC scartati
il 09/09 sono esattamente quelli che quel flag ha fermato."""
t = _trade(-0.50, -0.01)
assert t is not None
assert t["in_geom"] is False
assert t["d_short"] == -0.50 and t["d_long"] == -0.01
def test_in_geom_e_VERO_sulla_geometria_di_VRP01():
"""Controllo POSITIVO (M15): un rilevatore tarato per non segnalare va provato su un caso
che DEVE passare, altrimenti "nessun trade fuori geometria" non significa niente."""
t = _trade(C.SHORT_D, C.LONG_D)
assert t["in_geom"] is True
assert t["d_short"] == pytest.approx(C.SHORT_D) and t["d_long"] == pytest.approx(C.LONG_D)
def test_in_geom_il_confine_e_esattamente_TOL_D():
"""I due confini si DERIVANO da `TOL_D`/`SHORT_D`/`LONG_D`, non si riscrivono (P1).
⚠️ Il confine ESATTO non e' asseribile in virgola mobile: `-0.28 - 0.08` vale
-0.36000000000000004, quindi la delta "sul confine" cade FUORI. Si prova un micron dentro
e un micron fuori — e' il confine che conta, non il suo ultimo bit."""
dentro_s, fuori_s = C.SHORT_D - C.TOL_D[0] + 1e-6, C.SHORT_D - C.TOL_D[0] - 1e-6
dentro_l, fuori_l = C.LONG_D - C.TOL_D[1] + 1e-6, C.LONG_D - C.TOL_D[1] - 1e-6
assert _trade(dentro_s, dentro_l)["in_geom"] is True, "un micron dentro deve passare"
assert _trade(fuori_s, dentro_l)["in_geom"] is False, "gamba corta appena fuori"
assert _trade(dentro_s, fuori_l)["in_geom"] is False, "gamba lunga appena fuori"
def test_P1_le_delta_bersaglio_vengono_dal_SLEEVE():
"""DEVE FALLIRE SE: qualcuno ridichiara 0,28/0,10 nello script — la misura punterebbe su
una geometria diversa da quella del sleeve che dovrebbe descrivere."""
from src.portfolio.sleeves import VRP_CFG
assert C.SHORT_D is VRP_CFG["short_delta"]
assert C.LONG_D is VRP_CFG["long_delta"]
assert C.FEE_FRAC is VRP_CFG["fee_frac"]
# ===========================================================================================
# trade() — i conti, e cio' che si RIFIUTA di calcolare
# ===========================================================================================
def test_f_exp_e_f_mtm_sono_NAN_a_SEGNI_DISCORDI():
"""Un rapporto fra un reale NEGATIVO e un modello POSITIVO non e' un fattore di stress:
e' due fatti diversi messi in frazione. Qui il credito reale (0,2 $) sta sotto le fee
d'ingresso (1,2 $) mentre il credito modellato e' pieno → pnl_real < 0 < pnl_mod.
DEVE FALLIRE SE: qualcuno toglie le condizioni di segno e pubblica un `f` negativo."""
t = _trade(C.SHORT_D, C.LONG_D, bid_s=0.0101, ask_s=0.0102, bid_l=0.009, ask_l=0.010)
assert t["pnl_real"] < 0 < t["pnl_mod"], (t["pnl_real"], t["pnl_mod"])
assert np.isnan(t["f_exp"])
assert t["mtm_real_min"] < 0 < t["mtm_mod_min"], (t["mtm_real_min"], t["mtm_mod_min"])
assert np.isnan(t["f_mtm"])
def test_f_exp_e_UN_NUMERO_quando_perdono_entrambi():
"""Controllo POSITIVO del test qui sopra: con lo spot a 1500 a scadenza perdono sia il
reale sia il modello, e allora `f_exp` esiste ed e' esattamente il rapporto delle perdite."""
t = _trade(C.SHORT_D, C.LONG_D, bid_s=0.0101, ask_s=0.0102, bid_l=0.009, ask_l=0.010,
ST=1500.0)
assert t["pnl_real"] < 0 and t["pnl_mod"] < 0
assert t["f_exp"] == pytest.approx(t["pnl_real"] / t["pnl_mod"], abs=1e-12)
assert np.isfinite(t["f_exp"]) and t["f_exp"] > 0
def test_itm_pieno_dichiara_quando_f_exp_e_TAUTOLOGICO():
"""Se ENTRAMBE le gambe finiscono ITM il payoff e' la larghezza per il reale e per il
modello: `f_exp` si riduce a una funzione di `f_net` e non e' piu' un'informazione nuova.
Il flag esiste per poterlo DIRE (l'83% dei trade ETH del 09/09), non per correggerlo."""
assert _trade(C.SHORT_D, C.LONG_D, ST=1500.0)["itm_pieno"] is True # 1500 <= K_L
assert _trade(C.SHORT_D, C.LONG_D, ST=2000.0)["itm_pieno"] is False
assert _trade(C.SHORT_D, C.LONG_D, ST=K_L)["itm_pieno"] is True # confine incluso
assert _trade(C.SHORT_D, C.LONG_D, ST=K_L + 1.0)["itm_pieno"] is False
def test_trade_CONTA_le_ore_tagliate_invece_di_nasconderle():
"""P12/D5: una riparazione silenziosa e' un'invenzione. Il taglio alla larghezza cambia il
peggior MTM, quindi le ore in cui morde vanno contate e stampate accanto al numero."""
normale = _trade(C.SHORT_D, C.LONG_D)
assert normale["n_ore"] == 3 and normale["n_tagli"] == 0
tagliato = _trade(C.SHORT_D, C.LONG_D, ask_s=0.200) # (0,200 0,010)·2000 = 380 > 300
assert tagliato["n_ore"] == 3 and tagliato["n_tagli"] == 3
assert tagliato["mtm_real_min"] == pytest.approx(tagliato["cred_real"] - LARGHEZZA)
def test_f_net_e_il_credito_reale_su_quello_modellato():
"""Il credito reale si incassa al BID della corta e si paga all'ASK della lunga: e' la
convenzione conservativa di `cblib.f_factors`, e vale 0,74 su ETH nel crollo."""
t = _trade(C.SHORT_D, C.LONG_D)
assert t["cred_real"] == pytest.approx((0.030 - 0.012) * 2000.0)
T = (C.EXP - TS0).total_seconds() / 86400.0 / 365.25
assert t["cred_mod"] == pytest.approx(
CB.bs_put(2000.0, K_S, T, 0.5) - CB.bs_put(2000.0, K_L, T, 0.5), abs=1e-10)
assert t["f_net"] == pytest.approx(t["cred_real"] / t["cred_mod"], abs=1e-12)
assert t["risk"] == pytest.approx(LARGHEZZA - t["cred_real"])
def test_trade_e_None_se_il_credito_non_e_positivo():
"""Un credit spread a credito ≤ 0 non e' la struttura che si sta studiando."""
assert _trade(C.SHORT_D, C.LONG_D, bid_s=0.010, ask_s=0.011, bid_l=0.020, ask_l=0.030) is None
# ===========================================================================================
# verdetto() — i rami
# ===========================================================================================
ESITO = ("IL CROLLO E' CATTURATO MA A VOL BASSA E FUORI DAL GATE DI VRP01: il f di stress che "
"la regola del 19/06 aspettava NON e' misurato nella sostanza — cio' che e' misurato "
"e' che il rapporto di credito f_net non cambia col regime")
def _r(f_exp=1.04, f_mtm=1.08, n=569, n_pre=286, n_fuori=2, f_net=0.74, q_itm=0.83,
n_strutture=4, n_tagli=8779, n_ore=213477, gate=0.0,
dur_f_net=0.71, dur_q_perde=0.46) -> dict:
return dict(n=n, n_pre=n_pre, n_fuori=n_fuori, f_net=f_net, f_exp=f_exp, f_mtm=f_mtm,
q_itm_pieno=q_itm, n_strutture=n_strutture, n_tagli=n_tagli, n_ore=n_ore,
gate_aperto=gate, durante=dict(f_net=dur_f_net, q_perde=dur_q_perde))
def test_verdetto_l_esito_e_una_CONCLUSIONE_non_un_esito_numerico():
"""La conclusione del 09/09 non e' "f vale x": e' che il crollo catturato sta a vol BASSA e
fuori dal gate, quindi il `f` di stress che la regola del 19/06 aspetta non e' misurato
nella sostanza. DEVE FALLIRE SE: qualcuno rimette un verdetto che dipende dal solo numero —
e' la lettura che il numero da solo autorizzava e che la revisione ha tolto."""
for s in ({"BTC": _r(), "ETH": _r()},
{"BTC": _r(f_exp=3.0), "ETH": _r(f_exp=0.2)},
{"BTC": None, "ETH": None}):
v = C.verdetto(s)
assert v.startswith(ESITO)
assert v.endswith("— n = 1 crollo, 1 scadenza per asset, famiglia INVERSE")
def test_verdetto_NON_MISURABILE_quando_un_asset_non_ha_snapshot_in_geometria():
"""P5: lo stato "non vedo" ha un ramo suo e nomina la causa (griglia di strike rada)."""
v = C.verdetto({"BTC": dict(n=0, n_fuori=78), "ETH": None})
assert "BTC: NON misurabile — 78 snapshot tutti FUORI geometria (griglia di strike rada)" in v
assert "ETH: NON misurabile — 0 snapshot tutti FUORI geometria" in v
def test_verdetto_distingue_ZERO_SNAPSHOT_da_NESSUN_INGRESSO_PRE_CROLLO():
"""Due guasti diversi con la stessa azione (P5): "tutti fuori geometria" e "in geometria ma
nessun ingresso PRIMA del crollo" sono cause diverse e vanno dette diverse — e' esattamente
il caso di BTC il 09/09, dove resta solo la lente DURANTE."""
v = C.verdetto({"BTC": _r(f_exp=float("nan"), n=247, n_pre=0, n_fuori=78,
dur_f_net=0.79, dur_q_perde=0.3724696356275304),
"ETH": _r()})
assert "BTC: nessun ingresso pre-crollo in geometria (solo DURANTE: f_net 0.79, perde il 37%)" in v
assert "FUORI geometria" not in v.split("BTC:")[1].split(";")[0]
def test_verdetto_dichiara_che_il_f_a_scadenza_e_TAUTOLOGICO():
"""M13/§2: un numero si cita con la sua risoluzione. 286 snapshot su 4 strutture non sono
286 osservazioni, e a gambe entrambe ITM il `f` a scadenza e' una funzione di `f_net`.
Senza queste due note la riga "f 1,04" verrebbe letta come una misura indipendente."""
v = C.verdetto({"BTC": _r(), "ETH": _r()})
assert "a scadenza f 1.04 ma TAUTOLOGICO nel 83% dei trade" in v
assert "4 strutture, non 286 osservazioni" in v
assert "peggior MTM tagliato alla larghezza f 1.08" in v
assert "quote oltre la larghezza nel 4.1% delle ore" in v
def test_verdetto_dice_se_il_gate_di_VRP01_era_APERTO():
"""E' il fatto che decide se la misura parla del sleeve o di un altro trade: col gate chiuso
VRP01 quel trade NON lo avrebbe fatto."""
assert "gate IV-rank CHIUSO nel 0% delle ore" in C.verdetto({"BTC": _r(), "ETH": _r()})
assert "gate IV-rank APERTO nel 60% delle ore" in C.verdetto(
{"BTC": _r(gate=0.6), "ETH": _r(gate=0.6)})
# confine: 0,5 esatto non e' "aperto" (il confronto e' `>`)
assert "gate IV-rank CHIUSO nel 50% delle ore" in C.verdetto(
{"BTC": _r(gate=0.5), "ETH": _r(gate=0.5)})
def test_verdetto_non_divide_per_zero_quando_non_ci_sono_ore():
"""Un campione vuoto non deve far esplodere la riga: la quota di ore tagliate va a 0,0%."""
v = C.verdetto({"BTC": _r(n_tagli=0, n_ore=0), "ETH": _r()})
assert "quote oltre la larghezza nel 0.0% delle ore" in v
def test_verdetto_riproduce_ALLA_LETTERA_la_riga_pubblicata_il_09_09():
"""M23: la macchina deve ristampare il numero vecchio. Sono i valori esatti usciti da
`sintesi()` il 09/09 — se il testo cambia, cambia anche cio' che il diario cita, e va
cambiato nei due posti insieme."""
s = {"BTC": _r(f_exp=float("nan"), f_mtm=float("nan"), n=247, n_pre=0, n_fuori=78,
f_net=0.7941019699391642, q_itm=float("nan"), n_strutture=0, n_tagli=0,
n_ore=0, gate=0.0, dur_f_net=0.7941019699391642,
dur_q_perde=0.3724696356275304),
"ETH": _r(f_exp=1.0383032287675409, f_mtm=1.0790935015712533, n=569, n_pre=286,
n_fuori=2, f_net=0.7405178198702741, q_itm=0.8286713286713286,
n_strutture=4, n_tagli=8779, n_ore=213477, gate=0.0)}
assert C.verdetto(s) == (
ESITO + ""
"BTC: nessun ingresso pre-crollo in geometria (solo DURANTE: f_net 0.79, perde il 37%); "
"ETH: f_net 0.74 (22/08 in rally 0,714); a scadenza f 1.04 ma TAUTOLOGICO nel 83% dei "
"trade (entrambe le gambe ITM ⇒ dipende solo da f_net; 4 strutture, non 286 "
"osservazioni); peggior MTM tagliato alla larghezza f 1.08 (quote oltre la larghezza "
"nel 4.1% delle ore); gate IV-rank CHIUSO nel 0% delle ore "
"— n = 1 crollo, 1 scadenza per asset, famiglia INVERSE")
# ===========================================================================================
# carica() e l'integrazione sull'archivio VERO
# ===========================================================================================
def test_carica_tiene_solo_put_una_scadenza_la_finestra_e_nessun_duplicato():
"""L'archivio ereditato copre molte scadenze e molti tipi: se il filtro si allentasse, il
`f` verrebbe misurato su una struttura diversa da quella dichiarata nel docstring."""
if not C.ARCHIVIO.exists():
pytest.skip(f"archivio ereditato assente: {C.ARCHIVIO} (data/raw e' gitignored)")
a = C.carica()
assert len(a) > 0
assert set(pd.unique(a["option_type"])) == {"P"}, "call rimaste dentro"
assert (a["exp"] == C.EXP).all(), "piu' di una scadenza"
assert a["ts"].min() >= C.INGRESSI[0], "quote prima della finestra dichiarata"
assert a["ts"].max() <= C.EXP, "quote dopo la scadenza"
assert not a.duplicated(subset=["ts", "instrument_name"]).any()
assert (a["dte"] >= 0).all()
assert a["dte"].max() <= (C.EXP - C.INGRESSI[0]).total_seconds() / 86400.0 + 1e-9
assert {"BTC", "ETH"} <= set(pd.unique(a["asset"]))
def test_integrazione_la_misura_del_09_09_si_riproduce():
"""~11 s. Riproduce cio' su cui poggia la (non) riapertura della decisione del 19/06.
I fatti che decidono, e che il test tiene fermi:
* il gate IV-rank di VRP01 e' CHIUSO nel 100% delle ore → il sleeve questo trade non lo
avrebbe fatto, quindi il `f` misurato non e' il suo `f` di stress;
* il `f` a scadenza (1,04) e' TAUTOLOGICO nella grande maggioranza dei trade;
* 286 snapshot stanno su POCHE strutture: la risoluzione non e' 286 (M13);
* BTC resta NON misurabile pre-crollo;
* col taglio alla larghezza il peggior MTM e' vicino a 1, non 2,6 (che era book, non
rischio) — qui si controlla la TAGLIA, non la terza cifra."""
if not C.ARCHIVIO.exists():
pytest.skip(f"archivio ereditato assente: {C.ARCHIVIO}")
s = C.sintesi(C.misura())
eth = s["ETH"]
assert eth["n_pre"] > 200, "il campione pre-crollo si e' svuotato"
assert eth["f_net"] == pytest.approx(0.74, abs=0.03)
assert eth["f_exp"] == pytest.approx(1.04, abs=0.03)
assert eth["q_itm_pieno"] == pytest.approx(0.83, abs=0.10), (
"la quota di trade a f_exp tautologico e' cambiata: cambia la lettura del 1,04")
assert eth["n_strutture"] <= 10, (
f"{eth['n_pre']} snapshot su {eth['n_strutture']} strutture: se le strutture "
"diventassero tante, la nota sulla risoluzione andrebbe riscritta")
assert eth["gate_aperto"] == pytest.approx(0.0, abs=1e-12), (
"il gate IV-rank di VRP01 risulta APERTO: cambia il senso della misura, che oggi dice "
"'lo sleeve questo trade non lo avrebbe fatto'")
assert eth["perdita_su_risk"] == pytest.approx(1.01, abs=0.05)
assert 0.9 < eth["f_mtm"] < 1.5, (
f"peggior MTM {eth['f_mtm']:.2f}: col taglio alla larghezza sta attorno a 1; sopra 1,5 "
"vuol dire che il taglio non morde piu' e il numero torna a essere book, non rischio")
assert 0 < eth["n_tagli"] < eth["n_ore"] * 0.20, (
"il taglio deve mordere su una minoranza di ore: se e' zero non serve, se e' la "
"maggioranza il campione e' un altro")
btc = s["BTC"]
assert btc["n_pre"] == 0 and np.isnan(btc["f_exp"]), (
"BTC era NON misurabile pre-crollo: se ora lo e', il verdetto cambia e va riscritto, "
"non aggiustato")
assert btc["durante"]["n"] > 0 and 0.0 < btc["durante"]["q_perde"] < 1.0
v = C.verdetto(s)
assert v.startswith(ESITO)
assert "BTC: nessun ingresso pre-crollo in geometria" in v
assert "gate IV-rank CHIUSO nel 0% delle ore" in v
assert v.endswith("famiglia INVERSE")