38c8cdf25b
Harness onesto research_lab.py (serie di posizione causale, fee-aware, null model a rotazione circolare, hold-out 2025+ bloccato; self-test cheat/noise che valida il banco). - Fase 1: triage superstiti (DIP, shape-ML) -> morti net-fee. - Fase 2: esplorazione famiglie (reversal morta; solo trend long-only/MA-cross passa i gate base). - Fase 3: conferma avversariale del trend -> regime-luck del toro, bocciato sul hold-out 2025-26. - Ricerca frattale multi-agente (Workflow, 63 agenti, 52 ipotesi dai due documenti) con guard anti-look-ahead (eval_signal.py) + hold-out + test cross-asset -> 0 edge robusto (l'unico "confermato" su ETH fallisce su BTC con lo stesso codice). - Analisi options: VRP reale +10/+14 vol pt ma finestra 6 sett. regime unico -> non validabile; ruolo solo overlay tail-cap, tenere cerbero-bite ad accumulare. Quinta conferma indipendente: su BTC/ETH-solo-prezzo non c'e' un edge facile. Il processo disciplinato ha evitato un falso "+49% vs -49%" che sul vecchio feed contaminato sarebbe finito in produzione. Diari docs/diary/2026-06-19-research-phase0-1 / -phase2-options / -phase3-confirm / -fractal-multiagent-search. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
122 lines
5.9 KiB
Python
122 lines
5.9 KiB
Python
"""ANALISI OPTIONS per BTC/ETH — onesta sui dati REALI disponibili (cerbero-bite mainnet).
|
||
|
||
Dati: Old/data/options (chain per-strike + dvol + market_snapshots). Finestra ~2026-05-01→06-11
|
||
(~6 settimane, REGIME UNICO calmo). NON si può validare OOS un edge su opzioni qui; si possono
|
||
MISURARE i livelli reali (VRP, premi put, skew, liquidità) e ragionare sull'USO delle opzioni
|
||
per il book BTC/ETH certificato. cerbero-bite è ancora vivo -> la fonte continua ad accumulare.
|
||
|
||
uv run python scripts/analysis/options_analysis.py
|
||
"""
|
||
from __future__ import annotations
|
||
import sys
|
||
from pathlib import Path
|
||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||
sys.path.insert(0, str(PROJECT_ROOT))
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
OPT = PROJECT_ROOT / "Old" / "data" / "options"
|
||
|
||
|
||
def load(name):
|
||
return pd.read_parquet(OPT / name)
|
||
|
||
|
||
def market_snapshots_analysis():
|
||
print("=" * 90)
|
||
print(" (1) MARKET SNAPSHOTS — VRP, DVOL, funding, dealer-gamma (livelli reali)")
|
||
print("=" * 90)
|
||
ms = load("market_snapshots.parquet")
|
||
t = pd.to_datetime(ms["timestamp"], utc=True, errors="coerce")
|
||
print(f" copertura: {t.min()} -> {t.max()} ({len(ms)} righe)")
|
||
for a in ("BTC", "ETH"):
|
||
d = ms[ms["asset"] == a].dropna(subset=["iv_minus_rv"])
|
||
if len(d) == 0:
|
||
print(f" {a}: nessun dato"); continue
|
||
vrp = d["iv_minus_rv"].astype(float)
|
||
dvol = d["dvol"].astype(float)
|
||
rv = d["realized_vol_30d"].astype(float)
|
||
fund = d["funding_perp_annualized"].astype(float) if "funding_perp_annualized" in d else pd.Series([np.nan])
|
||
gam = d["dealer_net_gamma"].astype(float) if "dealer_net_gamma" in d else pd.Series([np.nan])
|
||
print(f"\n {a} (n={len(d)})")
|
||
print(f" VRP (IV-RV): media {vrp.mean():+.1f} mediana {vrp.median():+.1f} "
|
||
f">0 nel {100*(vrp>0).mean():.0f}% del tempo [IV-RV in punti di vol annua]")
|
||
print(f" DVOL: media {dvol.mean():.1f} range [{dvol.min():.1f}, {dvol.max():.1f}]")
|
||
print(f" Realized30d: media {rv.mean():.1f}")
|
||
print(f" Funding perp: media {fund.mean():+.1f}% annuo")
|
||
if gam.notna().any():
|
||
print(f" Dealer net-γ: >0 nel {100*(gam>0).mean():.0f}% del tempo (>0 = dealer long gamma = mean-rev)")
|
||
|
||
|
||
def chain_analysis(asset):
|
||
print("\n" + "=" * 90)
|
||
print(f" (2) CHAIN {asset} — premi put protettivi, skew, liquidità (livelli reali)")
|
||
print("=" * 90)
|
||
ch = load(f"{asset.lower()}_chain.parquet")
|
||
for col in ("strike", "bid", "ask", "mid", "iv", "delta", "gamma"):
|
||
if col in ch:
|
||
ch[col] = pd.to_numeric(ch[col], errors="coerce")
|
||
ch["option_type"] = ch["option_type"].astype(str)
|
||
dv = load("dvol_history.parquet")
|
||
dv = dv[dv["asset"] == asset][["timestamp", "spot"]].copy()
|
||
dv["spot"] = pd.to_numeric(dv["spot"], errors="coerce")
|
||
# timestamp -> datetime UTC nativo (sono datetime64[tz], NON ms int: to_numeric li romperebbe)
|
||
ch["t"] = pd.to_datetime(ch["timestamp"], utc=True, errors="coerce")
|
||
dv["t"] = pd.to_datetime(dv["timestamp"], utc=True, errors="coerce")
|
||
ch = ch.dropna(subset=["t"]).sort_values("t").reset_index(drop=True)
|
||
dv = dv.dropna(subset=["t", "spot"]).sort_values("t").reset_index(drop=True)
|
||
# spot causale per timestamp della chain (merge_asof nearest, tolleranza 1h)
|
||
ch = pd.merge_asof(ch, dv[["t", "spot"]], on="t", direction="nearest",
|
||
tolerance=pd.Timedelta("1h"))
|
||
ch = ch.dropna(subset=["spot", "mid", "strike"])
|
||
# days-to-expiry
|
||
exp = pd.to_datetime(ch["expiry"], utc=True, errors="coerce")
|
||
ch["dte"] = (exp - ch["t"]).dt.total_seconds() / 86_400.0
|
||
ch = ch[(ch["dte"] > 0.5) & (ch["dte"] < 90)]
|
||
ch["money"] = ch["strike"] / ch["spot"]
|
||
ch["prem_pct"] = ch["mid"] * 100 # mid è in COIN (frazione del sottostante) -> %-del-notional
|
||
# NB: iv è GIÀ in percento (35.94 = 35.94%, coerente col DVOL ~40) -> non riscalare
|
||
ch["spread_pct"] = (ch["ask"] - ch["bid"]) / ch["mid"].replace(0, np.nan) * 100
|
||
|
||
puts = ch[ch["option_type"].str.lower().str.startswith("p")]
|
||
calls = ch[ch["option_type"].str.lower().str.startswith("c")]
|
||
|
||
def band(df, mlo, mhi, dlo, dhi):
|
||
s = df[(df["money"] >= mlo) & (df["money"] <= mhi) & (df["dte"] >= dlo) & (df["dte"] <= dhi)]
|
||
return s
|
||
|
||
print(" PUT protettive — premio reale (mid/spot) e liquidità per tenor/moneyness:")
|
||
print(f" {'tenor':<10s}{'moneyness':<14s}{'premio%':>9s}{'/mese%':>9s}{'spread%':>9s}{'n':>7s}{'strike?':>9s}")
|
||
for dlo, dhi, tn in [(5, 12, "settim."), (18, 45, "mensile")]:
|
||
for mlo, mhi, ml in [(0.97, 1.03, "ATM"), (0.88, 0.93, "~10% OTM"), (0.83, 0.88, "~15% OTM")]:
|
||
s = band(puts, mlo, mhi, dlo, dhi)
|
||
if len(s) == 0:
|
||
print(f" {tn:<10s}{ml:<14s}{'—':>9s}{'—':>9s}{'—':>9s}{0:>7d}{'NO':>9s}")
|
||
continue
|
||
prem = s["prem_pct"].median()
|
||
permonth = prem * 30.0 / s["dte"].median()
|
||
print(f" {tn:<10s}{ml:<14s}{prem:>8.2f}%{permonth:>8.2f}%{s['spread_pct'].median():>8.1f}%"
|
||
f"{len(s):>7d}{'SI':>9s}")
|
||
|
||
# skew: IV put 10% OTM vs IV call 10% OTM (stesso tenor mensile)
|
||
pv = band(puts, 0.88, 0.93, 12, 50)["iv"].median()
|
||
cv = band(calls, 1.07, 1.12, 12, 50)["iv"].median()
|
||
atmv = band(ch, 0.98, 1.02, 12, 50)["iv"].median()
|
||
if pd.notna(pv) and pd.notna(cv):
|
||
print(f" SKEW: IV put 10%OTM {pv:.0f}% vs call 10%OTM {cv:.0f}% vs ATM {atmv:.0f}%"
|
||
f" -> skew put {pv-cv:+.0f} pt vol (>0 = put care = paura del crash prezzata)")
|
||
|
||
|
||
def main():
|
||
market_snapshots_analysis()
|
||
for a in ("BTC", "ETH"):
|
||
chain_analysis(a)
|
||
print("\n" + "=" * 90)
|
||
print(" NB: finestra ~6 settimane, REGIME UNICO calmo -> livelli REALI misurabili, ma NESSUN")
|
||
print(" edge su opzioni è validabile OOS qui. Vedi commento finale.")
|
||
print("=" * 90)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|