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