cblib causale: spot +1h e DVOL +1 giorno (debito §5.18); §11 rimisurato 0,714 → 0,712, verdetti invariati

- cblib.causale(s, cadenza): spot_series e dvol_series escono causali (asof = ultima chiusura NOTA);
  il DVOL giornaliero era una seconda serie con lo stesso difetto (fino a 24h avanti), verificato contro
  l'API pubblica a 1h. Regolamento ST alle 08:00 invece delle 09:00.
- §11 riprodotto al millesimo su worktree HEAD (0,714 [0,690-0,779], n=19, flag --al) e rimisurato:
  0,712 [0,664-0,732]; solo spot 0,721, solo DVOL 0,693, DVOL orario di controllo 0,717. Mediana onesta
  BTC 1,45 → 2,12, fee 1,51x/1,87x. r0730: 0,73 → 0,71. §75: ETH pre 0,76, BTC durante 0,89 (orario 0,83).
  skew panel ≤0,005. vrp_f_watch: f canonico 0,732 → 0,706, differenza +0,097 [+0,042, +0,140].
- r0909 senza doppio shift, contesto sul DVOL del giorno; r0822 regime su date di calendario; r0901
  join riallineato (bit-exact). Aperti con costo: DVOL orario (−0,04/+0,06 nel crollo), spot 5m (≤25 min).
- test: +3 causalita' in test_cb_chain_vrp, anti-doppio-shift in test_r0909; suite 1011/1011.
- revisione fable: 16 segnalazioni, tutte applicate (fra cui un «−1,8% in un'ora» che era +3,9%).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjj5vPEBoAJrB23P6RjKzs
This commit is contained in:
Adriano Dal Pastro
2026-09-09 21:41:23 +00:00
parent bb40e87d13
commit 0a2f780d3c
12 changed files with 401 additions and 73 deletions
+60 -1
View File
@@ -23,7 +23,8 @@ sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
sys.path.insert(0, str(ROOT / "scripts" / "analysis"))
from cblib import bs_put, f_factors, pick_legs # noqa: E402
import cblib # noqa: E402
from cblib import bs_put, causale, f_factors, pick_legs # noqa: E402
from certify_cb_chain import ( # noqa: E402
crossed_rate, hollow_rate, monotonicity_violations, worst_day_hollow,
)
@@ -196,3 +197,61 @@ def test_nel_sleeve_f_moltiplica_il_credito_NETTO():
assert SL.VRP_CFG["f"] == 1.0, (
"il sleeve gira ancora a f=1.0: se cambia, aggiornare il diario 2026-07-30 e CLAUDE.md"
)
# ===========================================================================================
# C. CAUSALITA' delle serie di contesto (debito §5.18, 09/09): `asof(ts)` non guarda avanti
# ===========================================================================================
def test_causale_rietichetta_alla_chiusura_e_non_tocca_i_valori():
"""Il feed e' etichettato all'APERTURA: la chiusura della barra T e' nota a T+cadenza.
DEVE FALLIRE SE: qualcuno "semplifica" togliendo lo spostamento, o sposta i VALORI."""
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 = causale(S, "1h")
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"
assert list(Sc.values) == list(S.values), "i VALORI non si toccano, solo le etichette"
def test_spot_series_e_causale_sul_feed_1h(monkeypatch):
"""`cblib.spot_series(a).asof(ts)` = ultima chiusura ORARIA nota a ts. Controllo positivo:
la serie grezza del feed (etichettata all'apertura) a ts da' la chiusura di ts+1h."""
import scripts.analysis.research_lab as rl
idx = pd.date_range("2026-06-01", periods=6, freq="h", tz="UTC")
epoch = pd.Timestamp("1970-01-01", tz="UTC") # NON `asi8 // 10**6`: in pandas 3 e' in `us` (D6)
px = pd.DataFrame({"timestamp": ((idx - epoch) // pd.Timedelta(milliseconds=1)).astype("int64"),
"close": [100.0, 101.0, 102.0, 103.0, 104.0, 105.0]})
monkeypatch.setattr(rl, "load_tf", lambda asset, tf: px)
cblib.spot_series.cache_clear()
try:
S = cblib.spot_series("ZZZ")
ts = idx[3] + pd.Timedelta(minutes=17) # 03:17: la barra 03:00 e' ancora aperta
assert S.asof(ts) == 102.0, "a 03:17 l'ultima chiusura nota e' quella della barra 02:00"
assert S.asof(idx[3]) == 102.0, "a 03:00 esatte la barra 02:00 e' appena chiusa"
assert S.asof(idx[5] + pd.Timedelta(hours=1)) == 105.0, "l'ultima chiusura arriva un'ora dopo"
grezza = pd.Series(px["close"].values, index=idx)
assert grezza.asof(ts) == 103.0, "il difetto riparato: la serie del feed a 03:17 da' la chiusura delle 04:00"
finally:
cblib.spot_series.cache_clear()
def test_dvol_series_e_causale_sul_feed_giornaliero(monkeypatch, tmp_path):
"""La riga DVOL del giorno D e' la chiusura delle 23:00 di D: dentro D non e' ancora nota.
`cblib.dvol_series(a).asof(ts in D)` deve dare la chiusura di D1."""
days = pd.date_range("2026-06-01", periods=4, freq="D", tz="UTC")
epoch = pd.Timestamp("1970-01-01", tz="UTC")
pd.DataFrame({"timestamp": ((days - epoch) // pd.Timedelta(milliseconds=1)).astype("int64"),
"close": [36.0, 37.0, 43.0, 47.0]}).to_parquet(tmp_path / "dvol_zzz.parquet")
monkeypatch.setattr(cblib, "RAW", tmp_path)
cblib.dvol_series.cache_clear()
try:
V = cblib.dvol_series("ZZZ")
ts = days[2] + pd.Timedelta(hours=10) # 03/06 10:00
assert V.asof(ts) == 37.0, "dentro il 03/06 l'ultimo DVOL noto e' la chiusura del 02/06"
assert V.asof(days[3]) == 43.0, "alle 00:00 del 04/06 e' nota la chiusura del 03/06"
assert np.isnan(V.asof(days[0] + pd.Timedelta(hours=5))), "nel primo giorno non c'e' storia"
finally:
cblib.dvol_series.cache_clear()
+24 -20
View File
@@ -8,7 +8,7 @@ numero con quel peso ha bisogno di guardie sul MECCANISMO, non solo sull'esito:
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
alle 21:00, quindi `S.asof(ts)` guardava un'ora AVANTI. `cblib.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"
@@ -96,25 +96,28 @@ def _trade(d_short: float, d_long: float, bid_s=0.030, ask_s=0.032, bid_l=0.010,
# ===========================================================================================
# spot_causale — il look-ahead di un'ora
# lo spot e il DVOL arrivano da cblib GIA' causali: qui non si ri-spostano (doppio shift = un'ora
# di RITARDO invece di un'ora di anticipo, stesso errore col segno opposto)
# ===========================================================================================
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"
def test_lo_script_usa_le_serie_di_cblib_senza_ri_spostarle():
"""🚨 Il 09/09 mattina lo script riparava da solo il look-ahead con `spot_causale`; la sera la
riparazione e' entrata in `cblib.spot_series` / `dvol_series` (debito §5.18). Applicare lo
spostamento DUE volte darebbe la chiusura di due ore prima. DEVE FALLIRE SE: qualcuno
reintroduce uno shift locale, o `cblib` smette di essere causale."""
import inspect
import cblib
src = inspect.getsource(C.misura)
assert "CB.spot_series(asset)" in src and "CB.dvol_series(asset)" in src
assert "spot_causale" not in src and "Timedelta(hours=1)" not in src
# l'UNICO `causale(` ammesso in misura() e' quello del contesto, e toglie un giorno al DVOL per la
# tabella di calendario: un `causale(CB.spot_series(...), "1h")` sarebbe il doppio shift
assert src.count("causale(") == 1 and 'CB.causale(V, "-1D")' in src
assert not hasattr(C, "spot_causale"), "lo shift locale e' stato rimosso: vive in cblib.causale"
# cblib e' causale (controllo positivo sul difetto che ripara)
idx = pd.date_range("2026-06-01", periods=3, freq="h", tz="UTC")
S = pd.Series([1.0, 2.0, 3.0], index=idx)
assert cblib.causale(S, "1h").asof(idx[1]) == 1.0 and S.asof(idx[1]) == 2.0
# ===========================================================================================
@@ -327,7 +330,8 @@ def test_trade_CONTA_le_ore_tagliate_invece_di_nasconderle():
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."""
convenzione conservativa di `cblib.f_factors`, e vale 0,76 su ETH nel crollo (0,74 con lo spot
e il DVOL non causali della mattina del 09/09: i due numeri vanno cambiati insieme)."""
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
@@ -477,7 +481,7 @@ def test_integrazione_la_misura_del_09_09_si_riproduce():
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_net"] == pytest.approx(0.76, 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")