Files
PythagorasGoal/scripts/research/r0822_vrp_real_quotes.py
T

858 lines
48 KiB
Python

"""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.")
print("\n QUANDO TORNERA' MISURABILE — il gate non e' chiuso per sempre, e la soglia e' un numero.")
print(" Regola del 19/06: «rivalutare quando la catena cattura un crash». Un criterio senza\n"
" numero non e' un criterio, quindi eccolo (IV-rank espandente causale, come nel sleeve):")
for a in ASSETS:
V = CB.dvol_series(a)
vals = V.to_numpy(float)
ivr = np.full(len(vals), np.nan)
for i in range(1, len(vals)):
ivr[i] = (vals[:i] < vals[i]).mean()
ser = pd.Series(ivr, index=V.index).dropna()
per_year = ser.groupby(ser.index.year).apply(lambda x: float((x > 0.30).mean()))
thr = float(np.quantile(vals[:-1], 0.30))
last_open = ser[ser > 0.30]
since = (V.index[-1] - last_open.index[-1]).days if len(last_open) else None
inwin = ser[(ser.index >= puts["ts"].min()) & (ser.index <= puts["ts"].max())]
near = inwin.max() if len(inwin) else float("nan")
print(f" {a}: quota di giorni col gate APERTO per anno — "
+ " ".join(f"{y}:{v:.0%}" for y, v in per_year.items()))
print(f" DVOL oggi {vals[-1]:.1f} · serve DVOL > {thr:.1f} "
f"(+{thr / vals[-1] - 1:.0%}) per riaprire il gate · ultimo giorno aperto "
f"{last_open.index[-1]:%Y-%m-%d} ({since} giorni fa)")
print(f" quanto vicino c'e' andato DENTRO la finestra: IV-rank max "
f"{near:.4f} il {inwin.idxmax():%Y-%m-%d} — "
f"{'mancato per ' + format(0.30 - near, '.4f') if near <= 0.30 else 'aperto'}")
print(" → il campione non e' 'sfortunato': cade dentro un regime di vol bassa lungo e\n"
" persistente. Ma il margine NON e' comodo: il gate ha sfiorato l'apertura, e\n"
" bastano ~+10% di DVOL per riaprirlo. La misura giusta si potra' rifare quando quel numero sara' superato,\n"
" e da allora servira' comunque un anno di settimane per avere un 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%}")
wmin = W.loc[W['frac'].idxmin()]
print(f" Denominatore, per non fabbricare una discrepanza: qui il rischio e' "
f"`width - credito`.\n"
f" CLAUDE.md cita «-81,7% del capitale» per la STESSA settimana "
f"({wmin['asset']} {wmin['exp']:%Y-%m-%d}):\n"
f" e' lo stesso mark (${wmin['worst']:.2f}) diviso per la width nuda. Due "
f"denominatori, un solo fatto.")
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}")
# DOVE vive l'instabilita': nel numeratore (il guadagno) o nel denominatore (la vol)?
det = []
for o in offsets:
w = by_off(o)
if len(w) > 2 and w.std(ddof=1) > 0:
det.append((o, float(w.mean()), float(w.std(ddof=1)), len(w)))
if det:
mm = np.array([d[1] for d in det])
ss = np.array([d[2] for d in det])
# il rapporto si stampa SOLO se il minimo e' positivo: max/min con min<0
# e' un numero senza significato (prima stesura: '112.240.864x').
rm = (f"{mm.max() / mm.min():.1f}x" if mm.min() > 0 else "SEGNO CHE CAMBIA")
print(f" media settimanale fra le ancore: [{mm.min():+.2%}, {mm.max():+.2%}] "
f"({rm}) · deviazione standard: [{ss.min():.2%}, {ss.max():.2%}] "
f"({ss.max() / ss.min():.1f}x)")
print(f" → si muovono ENTRAMBI, numeratore e denominatore"
f"{' — e sulla media il segno CAMBIA' if mm.min() <= 0 else ''}.\n"
f" Il motivo e' meccanico e vale la pena dirlo: spostare l'ora d'ingresso\n"
f" cambia lo SNAPSHOT, quindi quali strike combaciano col delta bersaglio\n"
f" — non e' lo stesso trade un'ora dopo, e' un'altra struttura. Con\n"
f" {det[0][3]} settimane questo parametro di disturbo domina tutto il resto.")
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())
def _f(k):
v = rep.get(k)
return f"{v:.3f}" if isinstance(v, (int, float)) and v is not None else str(v)
print(f" verdetto={rep.get('verdict')} corr={_f('corr')} "
f"insample_edge={rep.get('has_insample_edge')} robust_oos={rep.get('robust_oos')} "
f"is_hedge={rep.get('is_hedge')}")
print(f" barre ATTIVE {act}/{len(dly)} = {act / len(dly):.0%} — un `None` qui non e'\n"
f" un bug: le sotto-misure che richiedono un hold-out non hanno dati per\n"
f" esistere, e la funzione lo dichiara invece di inventare un numero.")
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. Quattro motivi, indipendenti
l'uno dall'altro: ciascuno da solo basterebbe.
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 sleeve sarebbe stato FLAT per l'intero campione. La DVOL
mediana sta al 7 (BTC) / 11 (ETH) percentile della storia 2021+, e il sottostante e'
salito del +25% / +44%: vol implicita bassa E rialzo forte = il regime MIGLIORE
possibile per vendere put, non uno neutro. Le settimane sono 10 per asset, non 16
(prima del 2026-06-09 l'archivio seguiva solo la finestra 13-20 DTE: quella 4-10 DTE
non esiste). Esito: 10/10 vincenti su BTC, e la lente giusta non e' lo Sharpe ma la
regola del tre — 0 perdite su 10 lascia un tasso vero fino al 30%, su un payoff che
perde ~7x il credito.
⚠️ Sfumatura che va detta: il gate non e' lontano. Ha mancato l'apertura per 0.0002 di
IV-rank il 2026-06-07, e bastano ~+10% di DVOL per riaprirlo. Il campione e' dentro un
regime di vol bassa persistente, non davanti a un muro.
2. IL TITOLO NON SOPRAVVIVE ALL'ORA D'INGRESSO. E' un max-of-k mai dichiarato, misurato
qui per la prima volta su questa struttura: su 9 ancore lo Sharpe canonico BTC 32.58
sta all'89 percentile, la MEDIANA ONESTA e' 1.45 e la banda tocca il NEGATIVO (-0.35).
Su ETH 3.90 -> 2.41. Il numero da citare per BTC e' 1.45, non 32.58.
⚠️ E il meccanismo non e' quello che avevo scritto prima di guardarlo. Non e' solo la
vol campionaria a ballare: fra le ancore la MEDIA settimanale BTC va da -1.35% a
+11.22% del rischio — CAMBIA SEGNO — e la deviazione standard di 11x (ETH: media 2.9x,
sd 2.4x). Spostare l'ora d'ingresso cambia lo snapshot e quindi quali strike combaciano
col delta bersaglio: non e' lo stesso trade un'ora dopo, e' UN'ALTRA STRUTTURA. A 10
settimane questo parametro di disturbo domina il segnale che si vorrebbe misurare.
3. 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 e il book
esegue sui perpetual lineari USDC. Esiste una famiglia USDC-lineare per BTC ed ETH col
lotto minimo 10x piu' piccolo (BTC 0.01 = $773 di nozionale invece di 0.1 = $7.728),
quindi il muro «BTC fuori a $3.000» del 30/07 — congelato in
tests/test_vrp_profit_take.py — e' misurato sulla famiglia che il conto non margina.
E' la 3a occorrenza della stessa forma dopo fee_watch (21/08) e la taratura di
venue_watch: un numero puntato su una configurazione diversa da quella che gira.
Ma di quella famiglia abbiamo ZERO ore di storia.
4. LO SPREAD E' IL COSTO DOMINANTE, E SULLA FAMIGLIA ESEGUIBILE E' PEGGIO. Attraversare
il bid/ask costa ~10% del credito sugli INVERSE; nella regione di strike che serve lo
spread relativo mediano e' 24-37% sugli inverse e 29-59% sulle USDC, con open interest
~0. Il f del credito netto si replica indipendentemente a 0.714 (IC95 [0.690, 0.779],
0/19 osservazioni >= 1.0) — conferma dello 0.73 del 30/07 su un campione piu' lungo e
con un percorso diverso.
CIO' CHE NON SI CONCLUDE: che la struttura non funzioni. Non e' stata osservata nel regime
in cui lavora. E un risultato a favore c'e', va detto: su BTC il null «e' solo crypto
travestito» e' REFUTATO (beta +0.019, t=0.18) — il PnL non viene dal rialzo. Non basta,
perche' il punto 2 toglie il titolo comunque.
LA REGOLA NON CAMBIA. «Niente short-vol da MODELLO in deploy» non diventa «short-vol da
QUOTE»: le quote adesso ci sono, ma sono quote della famiglia sbagliata, in un regime solo,
e con l'ancora che spiega il risultato. La sostituzione della regola resta NON DIMOSTRATA.
DUE AZIONI, NESSUNA ESEGUITA (toccano la produzione o il conto: decide l'operatore).
(A) DATO — far raccogliere a `collect_chain.py` anche `currency=USDC`. Costo MISURATO col
filtro OI>=100 gia' in vigore: ETH_USDC porterebbe 119 strumenti per giro (+21% di
chiamate, da ~570 a ~690), BTC_USDC ne porterebbe 5 su 686 (0.7%) = nulla di
utilizzabile. Cioe': l'unica struttura USDC-regolata con un libro vero e' su ETH,
lotto minimo 0.1 ETH = $242. Senza questa raccolta, fra sei mesi avremo un anno di
storia della famiglia che non possiamo marginare e zero di quella che potremmo — che
e' esattamente la posizione di oggi, un'ondata dopo l'altra.
(B) CONTO — una domanda che nessun dato pubblico chiude e che viene PRIMA di ogni
backtest: un saldo in USDC puo' marginare un'opzione INVERSE? Se no, la famiglia con
3 mesi di storia non e' negoziabile da questo conto a nessun capitale, e il punto 3
smette di essere un dettaglio di lotto per diventare un muro. Si risolve come si
risolse il PRIIPs il 26/07: guardando il conto, non i dati.
SE E QUANDO RIAPRIRLO. Non «quando ci sara' un crash» (criterio senza numero) ma: DVOL BTC
> 48.3 o ETH > 62.8 in modo persistente, PIU' ~un anno di settimane raccolte in quel
regime sulla famiglia eseguibile. Prima di allora ogni misura su questa struttura
descrivera' la sua astensione.""")
hr("COSA MI SMENTIREBBE", "-")
print(""" Tre osservazioni, tutte ottenibili, che ribalterebbero questo verdetto — e una che NON
lo ribalterebbe, messa qui perche' e' quella che arrivera' per prima.
1. IL CONTO. Se un saldo USDC puo' marginare le opzioni INVERSE, il punto 3 si sgonfia da
muro a dettaglio: la struttura ETH inverse (1 lotto, rischio ~$88, credito ~$12 la
settimana) e' gia' eseguibile a $600 oggi. Resterebbero i punti 1, 2 e 4 — sufficienti
a tenere il verdetto, ma il filone tornerebbe misurabile SUBITO invece che fra un anno.
Costo della verifica: un ordine di prova, come per il PRIIPs.
2. IL REGIME. Se, raccolte ~un anno di settimane col gate IV-rank APERTO, la media
settimanale restasse positiva CON la banda d'ancora che non contiene lo zero, allora
«short-vol da QUOTE» sarebbe dimostrato e la regola del 19/06 andrebbe riscritta.
E' la sola prova che conta: la banda d'ancora, non la media.
3. IL LIBRO. Se l'open interest di ETH_USDC salisse stabilmente sopra ~100 sugli strike a
δ -0.28/-0.10 con spread relativo sotto ~15%, esisterebbe una struttura regolata in
USDC ed eseguibile a $242 per lotto: il filone si riaprirebbe sul DATO invece che
sulla teoria. Ma senza raccoglierla adesso non lo sapremo mai — vedi azione (A).
⚠️ COSA NON MI SMENTIREBBE, ed e' quello che succedera' per primo: un'altra finestra di
10 settimane di vol bassa con 10/10 vincenti e uno Sharpe a due cifre. Sarebbe lo
STESSO campione, piu' lungo — non evidenza nuova. Un campione che non contiene il modo
di perdere non diventa informativo aggiungendo settimane in cui non si perde.""")
if __name__ == "__main__":
main()