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:
@@ -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 D−1."""
|
||||
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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user