"""R0822 VRP-QUOTE-VERE — esiste una struttura di reddito in opzioni ESEGUIBILE a $600-3.000 che sopravvive a fee reali, spread reali e lotto minimo? NON e' "rifare VRP01". La domanda e' se la regola dura del progetto — *niente short-vol da MODELLO in deploy* (19/06, riconfermata 3 volte) — si possa sostituire con *short-vol da QUOTE*, che e' un'altra cosa: il 30/07 il premio era ancora prezzato Black-Scholes su DVOL ATM e la misura sulle quote reali dava f = 0.73 del modello, con il difetto NON nella gamba venduta (f_short 1.01) ma nell'ALA che si compra (f_long 2.23). Da allora la condizione e' cambiata: la catena vera la raccogliamo noi, oraria, per strike, con bid/ask. ORDINE DELLE DOMANDE (deliberato). L'eseguibilita' viene PRIMA del rendimento — regola del 30/07: "prima di misurare il rendimento a un capitale dato, misurare il lotto minimo del venue". Se non si esegue, il resto e' accademia. §1 LIMITE DI REGIME — risultato di prima riga, non nota a pie' di pagina §2 IL CAMPIONE — quante settimane ci sono DAVVERO, e perche' §3 ESEGUIBILITA' — lotti, famiglie, e su quale delle due si esegue §4 BACKTEST SU QUOTE REALI — griglia dichiarata, fill al bid/ask, fee di listino §5 IL RISCHIO STA NEL MINIMO — non nelle chiusure settimanali §6 GATE — inclusi quelli che NON si possono far girare §7 VERDETTO CONVENZIONI CHE DECIDONO IL RISULTATO (dichiarate qui, non sepolte nel codice) (a) FILL: si vende al BID e si compra all'ASK, sempre. Il mid e' il modello sotto mentite spoglie e compare solo come diagnostica, mai come cella selezionabile. (b) FEE: listino Deribit vero — `min(0.03% del sottostante, 12.5% del premio della SINGOLA opzione)` per gamba, piu' la fee di consegna sulle gambe ITM a scadenza. Lo sleeve usa invece un forfait del 12.5% del credito NETTO: qui si misura di quanto sbaglia. (c) SETTLEMENT: indice reale alla scadenza dal feed certificato. Deribit regola sulla media dell'indice nei 30 minuti prima delle 08:00 UTC; qui si usa la barra 1h di chiusura — approssimazione dichiarata, non nascosta. (d) CAPITALE IMPEGNATO: il denominatore e' il rischio massimo della struttura `(K_short - K_long) x lotto - credito`. E' il limite INFERIORE del capitale richiesto; quello vero dipende dal regime di margine del conto e NON e' leggibile da dati pubblici. Riportato anche il limite superiore (cash-secured, la convenzione del sleeve). RAM: la catena si legge filtrata in LETTURA (solo put, solo le colonne che servono) con pyarrow.dataset — `bite_archive.parquet` intero esplode in memoria e ucciderebbe gli altri agenti. nice -n 19 timeout 900 uv run python scripts/research/r0822_vrp_real_quotes.py ... --no-live # salta le letture dal venue (usa la cache se c'e') """ from __future__ import annotations import argparse import json import sys import time import urllib.request from pathlib import Path import numpy as np import pandas as pd ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "research")) sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) import altlib as A # noqa: E402 import cblib as CB # noqa: E402 CHAIN_DIR = ROOT / "data" / "raw" / "cb_chain" SNAP_JSONL = ROOT / "data" / "options_daily" / "snapshots.jsonl" CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/r0822_venue.json") # --- listino Deribit (fonte: campo `taker_commission` letto dal venue + schema fee pubblico) --- FEE_UNDERLYING = 0.0003 # 0.03% del sottostante per gamba, per contratto FEE_PREMIUM_CAP = 0.125 # cap: 12.5% del premio della SINGOLA opzione FEE_DELIVERY = 0.00015 # 0.015% del sottostante sulle gambe ITM a scadenza SLEEVE_FEE_FRAC = 0.125 # forfait dello sleeve: 12.5% del credito NETTO DTE_LO, DTE_HI, DTE_TARGET = 4.0, 10.0, 7.0 WEEKS_PER_YEAR = 52.0 # ---------------------------------------------------------------- griglia DICHIARATA # Si conta al RIALZO: ogni variante provata e' un trial, anche quelle scartate. SHORT_DELTAS = (-0.35, -0.28, -0.20, -0.12) LONG_DELTAS = (-0.20, -0.10, -0.05) CELLS = [(s, l) for s in SHORT_DELTAS for l in LONG_DELTAS if abs(l) < abs(s)] FILLS = ("bid_ask", "mid") # `mid` = diagnostica, NON selezionabile ASSETS = ("BTC", "ETH") CANON = (-0.28, -0.10) # la cella di VRP01 def hr(t: str = "", ch: str = "=") -> None: print("\n" + ch * 100) if t: print(t) print(ch * 100) # ================================================================== dati def load_puts() -> pd.DataFrame: """Solo le PUT, solo le colonne che servono, filtrate in LETTURA.""" import pyarrow.dataset as ds cols = ["ts", "asset", "instrument_name", "strike", "option_type", "exp", "bid", "ask", "mid", "iv", "delta", "open_interest", "quote_status"] d = ds.dataset(str(CHAIN_DIR), format="parquet") df = d.to_table(columns=cols, filter=(ds.field("option_type") == "P")).to_pandas() df["ts"] = pd.to_datetime(df["ts"], utc=True).dt.as_unit("ns") df["exp"] = pd.to_datetime(df["exp"], utc=True) df["dte"] = (df["exp"] - df["ts"]).dt.total_seconds() / 86400.0 return df.drop_duplicates(subset=["ts", "instrument_name"], keep="last").reset_index(drop=True) def venue_read(live: bool) -> dict: """Specifiche e liquidita' lette DAL VENUE. Un fallimento si dichiara, non si inventa.""" if not live and CACHE.exists(): return json.loads(CACHE.read_text()) def api(path, **kw): q = "&".join(f"{k}={v}" for k, v in kw.items()) with urllib.request.urlopen( f"https://www.deribit.com/api/v2/public/{path}?{q}", timeout=25) as r: return json.load(r)["result"] out = {"ok": False, "ts": pd.Timestamp.now(tz="UTC").isoformat()} try: out["index"] = {c: api("get_index_price", index_name=f"{c}_usd")["index_price"] for c in ("btc", "eth")} specs, summ = {}, [] for cur in ("BTC", "ETH", "USDC"): for i in api("get_instruments", currency=cur, kind="option", expired="false"): fam = i["instrument_name"].split("-")[0] if fam not in specs: specs[fam] = {k: i.get(k) for k in ("min_trade_amount", "contract_size", "settlement_currency", "quote_currency", "taker_commission", "base_currency")} summ += api("get_book_summary_by_currency", currency=cur, kind="option") time.sleep(0.4) out["specs"] = specs out["summary"] = [{k: r.get(k) for k in ("instrument_name", "bid_price", "ask_price", "mid_price", "mark_price", "mark_iv", "open_interest", "volume", "underlying_price")} for r in summ] out["ok"] = True CACHE.parent.mkdir(parents=True, exist_ok=True) CACHE.write_text(json.dumps(out)) except Exception as exc: # noqa: BLE001 out["error"] = f"{type(exc).__name__}: {exc}" if CACHE.exists(): cached = json.loads(CACHE.read_text()) cached["stale"] = out["error"] return cached return out def specs_from_disk() -> dict: """Le specifiche gia' registrate dal progetto (`data/options_daily/snapshots.jsonl`). Serve come controllo indipendente della lettura live: due percorsi, stesso numero.""" mins, fees, last_ts = {}, {}, {} if not SNAP_JSONL.exists(): return {} with SNAP_JSONL.open() as f: for line in f: try: r = json.loads(line) except Exception: # noqa: BLE001, S112 continue if r.get("rec") != "chain": continue c = r["currency"] mins.setdefault(c, set()).add(r.get("min_trade_amount")) fees.setdefault(c, set()).add(r.get("taker_comm")) last_ts[c] = r["snap_ts"] return {c: {"min": sorted(x for x in mins[c] if x is not None), "taker": sorted(x for x in fees[c] if x is not None), "ts": pd.Timestamp(last_ts[c], unit="ms", tz="UTC")} for c in mins} # ================================================================== struttura def leg_fee_usd(prem_base: float, spot: float) -> float: """Fee di listino per UNA gamba, per contratto (1 unita' di sottostante), in USD.""" return min(FEE_UNDERLYING * spot, FEE_PREMIUM_CAP * prem_base * spot) def entry_snapshots(puts: pd.DataFrame, asset: str, offset_h: float = 0.0) -> dict: """Per ogni scadenza settimanale: lo snapshot piu' vicino a (7 giorni + offset) dalla scadenza. L'offset e' l'ANCORA: entrare alle 08:00 del venerdi' precedente e' UNA delle scelte possibili, e questo progetto ha misurato 4 volte che l'ancora e' un max-of-k non dichiarato. """ p = puts[(puts["asset"] == asset) & (puts["dte"] >= DTE_LO) & (puts["dte"] <= DTE_HI)] tgt = DTE_TARGET + offset_h / 24.0 out = {} for exp, g in p.groupby("exp"): ts_all = np.array(sorted(g["ts"].unique())) if not len(ts_all): continue dte = (exp - pd.DatetimeIndex(ts_all)).total_seconds() / 86400.0 ok = (dte >= DTE_LO) & (dte <= DTE_HI) if not ok.any(): continue cand = ts_all[ok][np.argsort(np.abs(dte[ok] - tgt))] for ts in cand[:6]: # al piu' 6 tentativi per scadenza snap = g[g["ts"] == ts] if snap["bid"].notna().sum() >= 2: out[exp] = (pd.Timestamp(ts), snap) break return out def build_trades(puts: pd.DataFrame, asset: str, short_d: float, long_d: float, fill: str = "bid_ask", offset_h: float = 0.0, lot: float = 1.0, today: pd.Timestamp | None = None) -> pd.DataFrame: """Un trade per scadenza: ingresso su quote REALI, tenuto a scadenza, regolato sull'indice.""" today = today or pd.Timestamp.now(tz="UTC") S = CB.spot_series(asset) V = CB.dvol_series(asset) rows, rejected = [], {"gambe_mancanti": 0, "credito_non_positivo": 0} for exp, (ts, snap) in entry_snapshots(puts, asset, offset_h).items(): if exp >= today: # non ancora regolata: non e' un trade continue legs = CB.pick_legs(snap, short_d, long_d) if legs is None: rejected["gambe_mancanti"] += 1 continue spot = float(S.asof(ts)) dte = (exp - ts).total_seconds() / 86400.0 dvol = float(V.asof(ts)) hist = V[V.index < ts] ivr = float((hist < dvol).mean()) if len(hist) else np.nan if fill == "bid_ask": ps, pl = legs["bid_short"], legs["ask_long"] # vendi al bid, compra all'ask else: ps, pl = legs["mid_short"], legs["mid_long"] if not (np.isfinite(ps) and np.isfinite(pl)): rejected["gambe_mancanti"] += 1 continue cred = (ps - pl) * spot # USD per 1 unita' di sottostante if cred <= 0: # premio non monotono nello strike (difetto certificato di `certify_cb_chain`): # una put piu' OTM quotata sopra una meno OTM. Non e' una struttura, e' una quota rotta. rejected["credito_non_positivo"] += 1 continue ST = float(S.asof(exp)) payoff = max(legs["k_short"] - ST, 0.0) - max(legs["k_long"] - ST, 0.0) fee = leg_fee_usd(ps, spot) + leg_fee_usd(pl, spot) for K in (legs["k_short"], legs["k_long"]): # consegna sulle gambe ITM intr = max(K - ST, 0.0) if intr > 0: fee += min(FEE_DELIVERY * ST, FEE_PREMIUM_CAP * intr) width = legs["k_short"] - legs["k_long"] mod = CB.f_factors(legs, spot, dvol / 100.0, dte) rows.append(dict( asset=asset, ts=ts, exp=exp, dte=dte, spot=spot, ST=ST, inst_short=legs["inst_short"], inst_long=legs["inst_long"], cred_real=cred, # per 1 unita': lo vuole CB.close_cost_path k_short=legs["k_short"], k_long=legs["k_long"], width=width, d_short=legs["d_short"], d_long=legs["d_long"], cred=cred * lot, payoff=payoff * lot, fee=fee * lot, pnl=(cred - payoff - fee) * lot, fee_sleeve=SLEEVE_FEE_FRAC * mod["cred_mod"] * lot, fee_sleeve_su_reale=SLEEVE_FEE_FRAC * cred * lot, cred_mod=mod["cred_mod"] * lot, risk=width * lot - cred * lot, dvol=dvol, ivrank=ivr, f_net=mod["f_net"], und_ret=ST / spot - 1.0, )) df = pd.DataFrame(rows) if not df.empty: df = df.sort_values("exp").reset_index(drop=True) df.attrs["rejected"] = rejected return df def stats(tr: pd.DataFrame) -> dict: """Metriche di una serie di trade settimanali. Il denominatore e' il RISCHIO impegnato.""" if tr.empty or len(tr) < 2: return dict(n=len(tr), sharpe=np.nan, mean=np.nan, t=np.nan) r = (tr["pnl"] / tr["risk"]).to_numpy(float) # ritorno sul capitale a rischio sd = float(np.std(r, ddof=1)) m = float(np.mean(r)) eq = np.cumprod(1.0 + r) dd = float(np.max((np.maximum.accumulate(eq) - eq) / np.maximum.accumulate(eq))) return dict(n=len(tr), mean=m, sd=sd, sharpe=(m / sd * np.sqrt(WEEKS_PER_YEAR)) if sd > 0 else np.nan, t=(m / (sd / np.sqrt(len(r)))) if sd > 0 else np.nan, maxdd_close=dd, wins=int((tr["pnl"] > 0).sum()), usd_week=float(tr["pnl"].mean()), usd_tot=float(tr["pnl"].sum())) def weekly_series(tr: pd.DataFrame) -> pd.Series: return pd.Series((tr["pnl"] / tr["risk"]).to_numpy(float), index=pd.DatetimeIndex(tr["exp"])).sort_index() def boot_ci(x: np.ndarray, n: int = 4000, seed: int = 822) -> tuple[float, float]: rng = np.random.default_rng(seed) if len(x) < 2: return (np.nan, np.nan) bs = rng.choice(x, size=(n, len(x)), replace=True).mean(axis=1) return float(np.percentile(bs, 2.5)), float(np.percentile(bs, 97.5)) # ================================================================== main def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--no-live", action="store_true", help="non interrogare il venue") args = ap.parse_args() print(__doc__.split("\n\n")[0]) puts = load_puts() today = pd.Timestamp.now(tz="UTC") venue = venue_read(not args.no_live) # ------------------------------------------------------------ §1 REGIME hr("§1 LIMITE DI REGIME — il campione descrive il regime in cui la strategia sta FLAT") print("Non e' una nota a pie' di pagina: VRP01 entra SOLO se IV-rank > 0.30 (il gate e' l'alpha,\n" "misurato 4 volte). Se nel campione quel gate non scatta mai, il campione non contiene\n" "la strategia — contiene la sua astensione.\n") for a in ASSETS: V = CB.dvol_series(a) win = V[(V.index >= puts["ts"].min()) & (V.index <= puts["ts"].max())] hist = V[V.index < win.index[0]].to_numpy(float) print(f" {a}: DVOL nel campione min {win.min().1f} / mediana {win.median().1f} / " f"max {win.max().1f}") print(f" vs storia 2021+ ({len(hist)} giorni) la MEDIANA del campione sta al " f"{float((hist < win.median()).mean()).0%} pctl, il suo MASSIMO al " f"{float((hist < win.max()).mean()).0%} pctl") print("\n \u26a0\ufe0f Due statistiche diverse, da tenere distinte: CLAUDE.md cita «DVOL max al 24\u00b0/28\u00b0\n" " pctl» dal campione del 30/07. Un MASSIMO cresce con la finestra — e' una statistica\n" " di ESTREMO, non di livello. Per dire in che regime siamo la lente e' la MEDIANA, ed\n" " e' bassissima.") # ------------------------------------------------------------ §2 CAMPIONE hr("§2 IL CAMPIONE — quante settimane ci sono DAVVERO") pre = puts[puts["ts"] < pd.Timestamp("2026-06-09", tz="UTC")] print(f" catena su disco: {puts['ts'].min():%Y-%m-%d} → {puts['ts'].max():%Y-%m-%d} " f"({(puts['ts'].max() - puts['ts'].min()).days} giorni, {len(puts):,} righe put)") if not pre.empty: print(f" ma PRIMA del 2026-06-09 l'archivio ereditato seguiva UNA sola scadenza, in " f"finestra {pre['dte'].min():.0f}-{pre['dte'].max():.0f} DTE:") print(f" → la finestra settimanale {DTE_LO:.0f}-{DTE_HI:.0f} DTE NON ESISTE prima " f"della raccolta full-chain.") print("\n Trade REGOLATI utilizzabili (cella canonica VRP01 δ-0.28/-0.10, fill bid/ask):") canon: dict[str, pd.DataFrame] = {} for a in ASSETS: lot = 1.0 tr = build_trades(puts, a, *CANON, lot=lot, today=today) canon[a] = tr rej = tr.attrs.get("rejected", {}) if not tr.empty else {} print(f" {a}: {len(tr)} settimane regolate ({tr['exp'].min():%Y-%m-%d} → " f"{tr['exp'].max():%Y-%m-%d}) scartate: {rej}") print("\n ⚠️ «3,7 mesi ≈ 16 scadenze settimanali» e' FALSO su questi dati: sono 10 per asset.") print("\n Gate IV-rank>0.30 di VRP01 sulle settimane utilizzabili:") tot = passed = 0 for a in ASSETS: tr = canon[a] n = int((tr["ivrank"] > 0.30).sum()) tot += len(tr) passed += n print(f" {a}: {n}/{len(tr)} passano (IV-rank min {tr['ivrank'].min():.3f} / " f"max {tr['ivrank'].max():.3f})") print(f" TOTALE: {passed}/{tot}. Il sleeve sarebbe stato FLAT per l'intero campione.") ret = {a: (canon[a]["ST"].iloc[-1] / canon[a]["spot"].iloc[0] - 1.0) for a in ASSETS} print(f"\n E il sottostante nel campione: BTC {ret['BTC']:+.1%}, ETH {ret['ETH']:+.1%} — " f"vol implicita bassa E rialzo forte\n = il regime IDEALE per vendere put. " f"Il campione e' il migliore possibile per questa struttura, non uno neutro.") # ------------------------------------------------------------ §3 ESEGUIBILITA' hr("§3 ESEGUIBILITA' — e qui il filone cambia domanda") fams = sorted({n.split("-")[0] for n in puts["instrument_name"].head(50_000)}) print(f" La catena che raccogliamo contiene le famiglie: {fams}") print(" → sono le opzioni INVERSE: quotate, marginate e regolate in BTC/ETH.") print(" Il conto e' in USDC ($635) e il book esegue sui perpetual LINEARI USDC.\n") disk = specs_from_disk() if disk: print(" Specifiche gia' registrate dal progetto (data/options_daily/snapshots.jsonl):") for c, v in sorted(disk.items()): print(f" {c:6s} min_trade_amount={v['min']} taker={v['taker']} ({v['ts']:%Y-%m-%d %H:%M})") if venue.get("ok"): idx = venue["index"] print(f"\n Lette dal venue ora — indice BTC ${idx['btc']:,.0f} / ETH ${idx['eth']:,.0f}:") print(f" {'famiglia':11s} {'min lotto':>10s} {'settle':>7s} {'quote':>7s} " f"{'$ nozionale/lotto':>18s}") for fam in ("BTC", "ETH", "BTC_USDC", "ETH_USDC"): s = venue["specs"].get(fam) if not s: continue base = (s.get("base_currency") or fam.split("_")[0]).lower() notion = float(s["min_trade_amount"]) * idx[base] print(f" {fam:11s} {s['min_trade_amount']:>10} {str(s['settlement_currency']):>7s} " f"{str(s['quote_currency']):>7s} {notion:>17,.0f}$") print("\n 📌 Esiste una famiglia USDC-lineare per BTC ed ETH, con lotto minimo 10x PIU'") print(" PICCOLO e regolata nella valuta del conto. Il muro «BTC fuori a $3.000» del") print(" 30/07 — congelato in tests/test_vrp_profit_take.py — e' stato misurato sulla") print(" famiglia che il conto NON margina.") print(" E' la 3a occorrenza della stessa forma dopo fee_watch (21/08, sorvegliava gli") print(" INVERSE mentre il book trada i LINEARI) e la taratura di venue_watch.") # --- ma la famiglia eseguibile e' quotata davvero? --- d = pd.DataFrame(venue["summary"]) parts = d["instrument_name"].str.split("-") d["fam"] = parts.str[0] d["typ"] = parts.str[3] d["strike"] = pd.to_numeric(parts.str[2].str.replace("d", ".", regex=False), errors="coerce") d["expd"] = pd.to_datetime(parts.str[1], format="%d%b%y", utc=True) + pd.Timedelta(hours=8) d["dte"] = (d["expd"] - today).dt.total_seconds() / 86400.0 d["base"] = d["fam"].str.replace("_USDC", "", regex=False).str.lower() d = d[d["typ"].eq("P") & d["base"].isin(["btc", "eth"])].copy() d["S"] = d["base"].map(idx) d["mny"] = d["strike"] / d["S"] q = d[(d["bid_price"] > 0) & (d["ask_price"] > 0)].copy() q["relsp"] = (q["ask_price"] - q["bid_price"]) / q["mid_price"] # regione delle put OTM usate dalla struttura (δ -0.35..-0.05 ≈ moneyness 0.78-0.97) w = q[(q["dte"] > 3) & (q["dte"] < 11) & q["mny"].between(0.78, 0.97)] print("\n Liquidita' REALE nella regione di strike che serve (scadenza settimanale, ora):") print(f" {'famiglia':11s} {'n quotate':>9s} {'spread rel. med':>16s} " f"{'p75':>7s} {'OI med':>9s} {'vol 24h':>10s}") for fam, g in w.groupby("fam"): allf = d[d["fam"] == fam] print(f" {fam:11s} {len(g):>9d} {g['relsp'].median():>15.0%} " f"{g['relsp'].quantile(.75):>6.0%} {g['open_interest'].median():>9.1f} " f"{allf['volume'].sum():>10,.0f}") print("\n Lo spread relativo e' ENORME in entrambe le famiglie (il mid non e' un prezzo\n" " eseguibile: e' la media di due prezzi lontani), e sulla famiglia USDC — l'unica\n" " marginabile dal conto — l'open interest e' ~0: quotata da un market maker,\n" " praticamente non tradata. Comprare l'ala li' dentro costa il doppio.") else: print(f"\n ⚠️ LETTURA DAL VENUE NON RIUSCITA: {venue.get('error', 'n/d')}") print(" Le specifiche delle famiglie USDC restano NON MISURATE in questo giro.") print("\n Capitale per UN lotto minimo, cella canonica (limiti dichiarati, vedi docstring):") print(f" {'asset':6s} {'lotto':>7s} {'width med':>10s} {'rischio max':>12s} " f"{'cash-secured':>13s} {'credito/sett':>13s}") minlot = {"BTC": 0.1, "ETH": 1.0} if venue.get("ok"): for fam in ("BTC", "ETH"): if fam in venue["specs"]: minlot[fam] = float(venue["specs"][fam]["min_trade_amount"]) for a in ASSETS: tr, lot = canon[a], minlot[a] wid = float(tr["width"].median()) print(f" {a:6s} {lot:>7.2f} {wid:>10,.0f} {wid * lot - tr['cred'].median() * lot:>11,.0f}$ " f"{tr['k_short'].median() * lot:>12,.0f}$ {tr['cred'].median() * lot:>12,.2f}$") print(" Sul rischio DEFINITO un lotto minimo inverse sta dentro $600. Sul cash-secured no.\n" " Quale dei due valga dipende dal regime di margine del conto (standard vs portfolio),\n" " che NON e' leggibile da dati pubblici.") print(" ⚠️ E c'e' una domanda PRIMA di quella, che nessun dato pubblico chiude: un saldo in\n" " USDC puo' marginare un'opzione INVERSE (collaterale in BTC/ETH)? Se no, la famiglia\n" " con 3 mesi di storia non e' negoziabile da questo conto a NESSUN capitale.\n" " Precedente esatto: il 26/07 GTAA01 aveva 5 settimane di misure sopra l'assunzione\n" " che i 6 ETF fossero comprabili — il blocco PRIIPs si scopri' con UN ordine di prova.\n" " La regola che ne usci' («la negoziabilita' sul conto REALE si verifica quando lo\n" " sleeve entra in RICERCA, non quando entra nel book») si applica qui identica, e\n" " questo script NON puo' eseguirla: richiede il conto, non i dati.") # ------------------------------------------------------------ §4 BACKTEST hr("§4 BACKTEST SU QUOTE REALI — griglia dichiarata") n_trials = len(CELLS) * len(FILLS) * (len(ASSETS) + 1) print(f" celle δ (short,long): {len(CELLS)} fill: {len(FILLS)} " f"asset: {len(ASSETS)} + combinato 50/50") print(f" → TRIAL DICHIARATI AL RIALZO: {n_trials}. Nessuna griglia ridotta per budget.") print(f" (Il fill `mid` non e' selezionabile — e' la diagnostica di quanto costa lo spread —\n" f" ma conta come trial: e' stato guardato.)\n") grid = [] for fill in FILLS: for (sd, ld) in CELLS: per, comb = {}, [] for a in ASSETS: tr = build_trades(puts, a, sd, ld, fill=fill, lot=1.0, today=today) per[a] = stats(tr) if not tr.empty: comb.append(weekly_series(tr)) row = dict(fill=fill, sd=sd, ld=ld) for a in ASSETS: row[f"{a}_sh"] = per[a]["sharpe"] row[f"{a}_n"] = per[a]["n"] if comb: c = pd.concat(comb, axis=1).mean(axis=1).dropna() row["comb_sh"] = (float(c.mean() / c.std(ddof=1) * np.sqrt(WEEKS_PER_YEAR)) if c.std(ddof=1) > 0 else np.nan) row["comb_n"] = len(c) grid.append(row) G = pd.DataFrame(grid) print(G.round(2).to_string(index=False)) print("\n ⚠️ COME SI LEGGE LA TABELLA SOPRA. Gli 'Sharpe' a due cifre NON sono un risultato:\n" " sono la firma del campione. Annualizzare (x√52) 10 osservazioni settimanali in cui\n" " la coda sinistra non si e' manifestata produce numeri privi di significato — e il\n" " fatto che l'INTERA griglia li produca e' esattamente il segnale, non un caso da\n" " selezionare. E' la 4a occorrenza della firma '0-perdite / Sharpe implausibile'\n" " dopo CC01, le celle deep-OTM Albimarini e meta' della griglia VRP del 03/07.\n" " Per lo stesso motivo NON viene riportato un CAGR: comporre 10 settimane a 52\n" " sarebbe una fabbricazione, non una stima.") print("\n Cella canonica di VRP01 (δ-0.28/-0.10), fill conservativo, per asset — numeri\n" " CONCRETI (dollari per lotto minimo), che a questa taglia dicono piu' di un rapporto:") print(f" {'asset':6s} {'n':>3s} {'vinte':>7s} {'$/sett':>9s} {'$ tot':>9s} " f"{'% rischio/sett':>15s} {'t':>6s} {'maxDD chiusure':>15s}") st = {} for a in ASSETS: tr = canon[a].copy() tr[["cred", "payoff", "fee", "pnl", "risk", "cred_mod", "fee_sleeve", "fee_sleeve_su_reale"]] *= minlot[a] st[a] = stats(tr) s = st[a] print(f" {a:6s} {s['n']:>3d} {s['wins']:>3d}/{s['n']:<3d} {s['usd_week']:>8.2f}$ " f"{s['usd_tot']:>8.2f}$ {s['mean']:>14.2%} {s['t']:>6.2f} {s['maxdd_close']:>14.1%}") print(" maxDD 0.0% su BTC significa che NON C'E' STATA una settimana in perdita: e' una\n" " proprieta' del campione, non della struttura.") print("\n Costo dello SPREAD, misurato (stesso ingresso, fill al mid vs al bid/ask):") for a in ASSETS: ba = build_trades(puts, a, *CANON, fill="bid_ask", today=today) md = build_trades(puts, a, *CANON, fill="mid", today=today) j = ba.merge(md, on="exp", suffixes=("_ba", "_md")) # appaiato per INGRESSO if j.empty: continue print(f" {a}: credito medio al mid ${j['cred_md'].mean():.2f} → al bid/ask " f"${j['cred_ba'].mean():.2f} = si perde il " f"{1 - j['cred_ba'].mean() / j['cred_md'].mean():.0%} del credito solo attraversando " f"lo spread") print("\n Il f del 30/07, ri-misurato su 10 settimane invece di 8 (fill conservativo):") for a in ASSETS: f = canon[a]["f_net"].dropna() lo, hi = boot_ci(f.to_numpy()) print(f" {a}: f_net mediana {f.median():.3f} media {f.mean():.3f} " f"IC95 [{lo:.3f}, {hi:.3f}] ≥1.0 in {int((f >= 1).sum())}/{len(f)}") allf = pd.concat([canon[a]["f_net"] for a in ASSETS]).dropna() lo, hi = boot_ci(allf.to_numpy()) print(f" pooled: {allf.median():.3f} (IC95 [{lo:.3f}, {hi:.3f}], n={len(allf)}) — " f"replica indipendente dello 0.73 del 30/07") print("\n Fee: listino vero contro il forfait dello sleeve (12.5% del credito NETTO).") print(" ⚠️ Il confronto giusto e' col credito MODELLATO — e' quello a cui il sleeve applica\n" " il forfait. Misurarlo sul credito reale (piu' piccolo di ~30%) sottostima l'errore:") for a in ASSETS: tr = canon[a] print(f" {a}: fee reale ${tr['fee'].mean():.2f}/settimana per contratto vs forfait sul\n" f" credito MODELLATO ${tr['fee_sleeve'].mean():.2f} (= {tr['fee_sleeve'].mean() / tr['fee'].mean():.2f}x)" f" · sul credito reale ${tr['fee_sleeve_su_reale'].mean():.2f} " f"(= {tr['fee_sleeve_su_reale'].mean() / tr['fee'].mean():.2f}x)") print(" Il '~2x' pubblicato il 30/07 non si replica qui: su queste 19 settimane il forfait\n" " sovrastima le fee di 1.1-1.9x a seconda dell'asset e della base di confronto.\n" " Resta vero il segno (il forfait e' CONSERVATIVO), non la taglia.") # ------------------------------------------------------------ §5 IL MINIMO hr("§5 IL RISCHIO STA NEL MINIMO — le chiusure settimanali non lo vedono") print(" Regola del 30/07: un rischio valutato sul minimo non si misura sulle chiusure.\n" " Qui il mark infra-settimana viene dalle standing quotes ORARIE delle STESSE due gambe\n" " (ricomprare la corta all'ask, rivendere la lunga al bid: di nuovo conservativo).\n") worst_rows = [] for a in ASSETS: print(f" {a} (lotto {minlot[a]})") for _, r in canon[a].iterrows(): path = CB.close_cost_path(r, puts) if path.empty: continue lot = minlot[a] risk = r["width"] * lot - r["cred"] * lot worst = float(path["pnl_aperto"].min()) * lot worst_rows.append(dict(asset=a, exp=r["exp"], worst=worst, risk=risk, frac=worst / risk, pnl=r["pnl"] * lot, n_marks=len(path))) flag = " ← chiusa in UTILE" if r["pnl"] > 0 and worst < 0 else "" print(f" {r['exp']:%Y-%m-%d} marks={len(path):3d} " f"peggior mark {worst:>9.2f}$ = {worst / risk:>7.1%} del rischio " f"esito {r['pnl'] * lot:>7.2f}${flag}") W = pd.DataFrame(worst_rows) if not W.empty: print(f"\n Peggior mark infra-settimana: mediana {W['frac'].median():.1%} del rischio, " f"minimo {W['frac'].min():.1%}") wl = W[W["pnl"] > 0] print(f" Fra le sole settimane chiuse in UTILE ({len(wl)}/{len(W)}): mediana " f"{wl['frac'].median():.1%}, minimo {wl['frac'].min():.1%}") print(" → il maxDD sulle chiusure e' una lente che non vede il rischio che si e' corso.") # ------------------------------------------------------------ §6 GATE hr("§6 GATE — compresi quelli che NON si possono far girare") print(" (a) implausible_sharpe — il gate del 26/07 sul rischio FUORI dal campione") for a in ASSETS: tr = canon[a] daily = pd.Series((tr["pnl"] / tr["risk"]).to_numpy(float), index=pd.DatetimeIndex(tr["exp"])) rep = A.implausible_sharpe(daily, n_trades=len(tr), n_losing_trades=int((tr["pnl"] <= 0).sum())) print(f" {a}: implausible={rep['implausible']} vinte {int((tr['pnl'] > 0).sum())}/{len(tr)}") for why in rep["reasons"]: print(f" · {why}") if int((tr["pnl"] <= 0).sum()) == 0: print(f" · regola del tre: 0 perdite su {len(tr)} lascia un tasso VERO fino a " f"{3 / len(tr):.0%}. Su un payoff che perde {tr['risk'].median() / tr['cred'].median():.1f}x " f"il credito, basta a ribaltare l'expectancy.") print("\n (b) deflated_sharpe — NON CALCOLABILE, e il perche' e' il risultato") for a in ASSETS: s = weekly_series(canon[a]) dsr, _ = A.deflated_sharpe(st[a]["sharpe"], [r["comb_sh"] for r in grid if np.isfinite(r.get("comb_sh", np.nan))], s, dpy=WEEKS_PER_YEAR) print(f" {a}: DSR = {dsr} (T={len(s)} osservazioni settimanali; la funzione " f"richiede T>=30)") print(" Costruire una serie GIORNALIERA per superare il T>=30 sarebbe barare: uno sleeve\n" " settimanale su griglia giornaliera e' ~94% di zeri (lezione 26/07 su VRP01), e il\n" " T gonfiato entra nel deflated-Sharpe come se fossero osservazioni indipendenti.\n" " Statistica onesta a questa taglia = il t con il suo intervallo:") for a in ASSETS: r = (canon[a]["pnl"] / canon[a]["risk"]).to_numpy(float) lo, hi = boot_ci(r) print(f" {a}: media settimanale {r.mean():+.2%} del rischio, IC95 " f"[{lo:+.2%}, {hi:+.2%}], t={st[a]['t']:.2f}, n={len(r)}") print("\n (c) anchor_luck_band — l'ora d'ingresso e' un'ancora (max-of-k mai dichiarato)") offsets = list(range(-24, 25, 6)) for a in ASSETS: def by_off(o, _a=a): return weekly_series(build_trades(puts, _a, *CANON, offset_h=float(o), today=today)) band = A.anchor_luck_band( by_off, offsets, canonical=0, metric=lambda s: (float(s.mean() / s.std(ddof=1) * np.sqrt(WEEKS_PER_YEAR)) if len(s) > 2 and s.std(ddof=1) > 0 else 0.0)) if band.get("reason"): print(f" {a}: {band['reason']}") continue print(f" {a}: canonica {band['canonical']:.2f} (pctl {band['canonical_pctl']:.0%}) · " f"MEDIANA ONESTA {band['median']:.2f} · banda [{band['lo']:.2f}, {band['hi']:.2f}] · " f"positiva in {band['frac_positive']:.0%} di {band['n_anchors']} ancore · " f"fortuna {band['luck']:+.2f}") print("\n (d) null del de-levering / beta — 'e' reddito da volatilita' o crypto travestito?'") print(" Lo spread ha delta positivo (≈ +0.18 all'ingresso): in un campione che sale del") print(" 20-50% guadagna PER COSTRUZIONE. Regressione del PnL settimanale sul ritorno") print(" del sottostante nella stessa settimana:") for a in ASSETS: tr = canon[a] x = tr["und_ret"].to_numpy(float) y = (tr["pnl"] / tr["risk"]).to_numpy(float) if len(x) < 4: continue b, alpha = np.polyfit(x, y, 1) resid = y - (b * x + alpha) se = float(np.std(resid, ddof=2) / (np.std(x) * np.sqrt(len(x)))) print(f" {a}: beta {b:+.3f} (t={b / se:.2f}) alpha {alpha:+.2%}/settimana " f"corr {np.corrcoef(x, y)[0, 1]:+.2f}") k = abs(b) lev = pd.Series(k * x, index=pd.DatetimeIndex(tr["exp"])) sh_lev = (float(lev.mean() / lev.std(ddof=1) * np.sqrt(WEEKS_PER_YEAR)) if lev.std(ddof=1) > 0 else np.nan) print(f" null: essere semplicemente LONG il sottostante a leva {k:.2f} " f"(stesso beta) da' Sharpe {sh_lev:.2f} contro {st[a]['sharpe']:.2f} della struttura") print("\n (e) marginal_vs_tp01 — girato, ma NON interpretabile a questa taglia") try: comb = pd.concat([weekly_series(canon[a]) for a in ASSETS], axis=1).mean(axis=1).dropna() dly = comb.resample("1D").sum().fillna(0.0) rep = A.marginal_vs_tp01(dly) act = int((dly != 0).sum()) print(f" verdetto={rep.get('verdict')} corr={rep.get('corr'):.3f} " f"(barre ATTIVE {act}/{len(dly)} = {act / len(dly):.0%})") print(f" ⚠️ {len(comb)} settimane su un hold-out che non esiste: il verdetto e' " f"rumore, si riporta per non ometterlo.") except Exception as exc: # noqa: BLE001 print(f" NON GIRATO: {type(exc).__name__}: {exc}") print("\n (f) causality_ok — non girato su questo candidato, e perche'") print(" `A.causality_ok` valuta target_fn su OHLCV BTC/ETH; qui il segnale non e' una") print(" serie di prezzo ma una selezione di strumenti da uno snapshot. Verifica") print(" equivalente eseguita per costruzione e controllata qui sotto:") bad = 0 for a in ASSETS: for _, r in canon[a].iterrows(): if r["ts"] >= r["exp"] or not (DTE_LO <= r["dte"] <= DTE_HI): bad += 1 print(f" ingressi con ts >= scadenza o fuori finestra DTE: {bad}/" f"{sum(len(canon[a]) for a in ASSETS)} (le gambe si scelgono da UNO snapshot allo\n" f" stesso ts; il regolamento usa l'indice alla scadenza, mai prima)") # ------------------------------------------------------------ §7 VERDETTO hr("§7 VERDETTO") print(""" SCARTATO — su questi dati la domanda non e' decidibile, e i tre motivi sono indipendenti l'uno dall'altro: 1. IL CAMPIONE NON CONTIENE LA STRATEGIA. 0 settimane su 19 passano il gate IV-rank>0.30 che E' l'alpha di VRP01. Il campione e' vol implicita al 24-28 percentile E un rialzo del +25%/+57%: il regime migliore possibile per vendere put, non uno neutro. Le 10 settimane per asset (non 16: prima del 2026-06-09 la finestra 4-10 DTE non esiste) danno 10/10 vincenti su BTC. La lente giusta non e' lo Sharpe, e' la regola del tre: 0 perdite su 10 lascia un tasso vero fino al 30%, su un payoff che perde ~7x il credito. 2. LA FAMIGLIA CHE HA STORIA NON E' QUELLA CHE SI PUO' ESEGUIRE. Le quote raccolte sono le opzioni INVERSE (margine e regolamento in BTC/ETH); il conto e' in USDC. La famiglia USDC-lineare esiste per BTC ed ETH ed ha un lotto minimo 10x piu' piccolo (BTC 0.01 invece di 0.1) — quindi il muro «BTC fuori a $3.000» del 30/07, congelato in un test, e' misurato sulla famiglia sbagliata. Ma di quella famiglia abbiamo ZERO ore di storia e l'open interest e' ~0. 3. LO SPREAD E' IL COSTO DOMINANTE E NON E' MODELLABILE DA UN MID. Attraversare il bid/ask costa da solo una quota grossa del credito, e l'ala che si COMPRA e' la gamba dove il libro e' piu' largo — lo stesso meccanismo che il 30/07 misurava come f=0.73. Cio' che NON si conclude: che la struttura non funzioni. Non e' stata osservata nel regime in cui lavora. La regola «niente short-vol da MODELLO in deploy» NON diventa «short-vol da QUOTE»: le quote ora ci sono, ma sono quote della famiglia sbagliata, in un regime solo. AZIONE RACCOMANDATA (non eseguita — tocca la produzione, e la decisione e' dell'operatore): far raccogliere a `collect_chain.py` anche `currency=USDC` (famiglie BTC_USDC/ETH_USDC). Costa ~2 chiamate in piu' per giro; senza, fra sei mesi ci ritroveremo con un anno di storia della famiglia che non possiamo tradare e zero di quella che potremmo.""") if __name__ == "__main__": main()