"""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")