research(cases): 3 agenti live sui 3 case — PM<->Deribit SKIP (gap=spec non alpha), HLP SKIP/WATCH (carry ex-evento 0.2%/a), 0DTE capture avviata
PM: 34 binarie riconciliate vs daily options; il gap 11pp si decompone in basis USDT (+4-5pp ATM), tempo 8h (-+5pp) e replica su book morti; residuo tail 2-3pp sotto costi; trade coperto reale: lock /bin/bash.1-5, P(conflitto gambe) 2-22%, margine SM domina -> SKIP. HLP: serie daily trovata (wHLP): Sharpe 0.69 non ~2, +12% 2026 = 1 evento, ex-evento +0.2%/a, coda JELLY = inventario ereditato troncato da voto discrezionale, Kelly f*=0 -> SKIP/WATCH con trigger meccanici. 0DTE: fee cap binding = drag 2-3x weekly, IV<RV al front stanotte; snapshot pipeline scritta e primo capture su disco; decisione pre-registrata a 90g di serie. Book INVARIATO. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
"""SNAPSHOT catena opzioni DAILY (0-1DTE) Deribit BTC/ETH — cattura quote REALI per il dossier 0DTE.
|
||||
|
||||
CONTESTO (2026-07-24). VRP01 (put credit spread settimanale) e' deploy-bloccato dalla regola
|
||||
"niente short-vol da modello": serve una serie di premi REALI, non BS-su-DVOL. Le scadenze DAILY
|
||||
accumulano 365 expiry/anno (7x le weekly): in ~6 mesi di cattura si ottiene una serie di ~180
|
||||
premi reali a tenor giornaliero + ~26 weekly, abbastanza per stimare il VRP daily NETTO di
|
||||
bid/ask e fee (misura live 2026-07-24: haircut mid->netto ~30-50% a tenor daily, fee cap 12.5%
|
||||
quasi sempre binding perche' i premi daily sono < 0.0024 base ccy).
|
||||
|
||||
COSA FA (sola lettura, API pubblica, nessun ordine, nessun token):
|
||||
- per BTC e ETH: public/get_instruments (kind=option) -> tutte le scadenze entro --max-dte-h
|
||||
(default 50h = la daily 0DTE + la daily 1DTE appena listata);
|
||||
- per ogni strumento nel ladder di moneyness (default 75-125%): public/ticker ->
|
||||
bid/ask/depth, mark, mark_iv, bid_iv/ask_iv, greche, OI, volume, underlying;
|
||||
- 1 riga JSON per strumento ("rec":"chain") + 1 riga meta per valuta ("rec":"meta", con
|
||||
index price e DVOL corrente) APPESE a data/options_daily/snapshots.jsonl.
|
||||
|
||||
CADENZA RACCOMANDATA (quando/se si decide di cablarla — NON e' in cron adesso):
|
||||
- 08:05 UTC: subito dopo il listing della nuova daily (~24h DTE) = il premio "vendibile";
|
||||
- 07:55 UTC: subito prima del settle (08:00 UTC) = chiude il ciclo (payoff realizzato).
|
||||
Con 2 run/giorno: ~160 strumenti/run, ~350 byte/riga -> ~120 KB/giorno, ~40 MB/anno. Banale.
|
||||
Ogni run extra (es. 12:00/20:00) aggiunge la dimensione intraday dello spread: opzionale.
|
||||
|
||||
USO:
|
||||
uv run python scripts/research/r0724_daily_opt_snapshot.py
|
||||
uv run python scripts/research/r0724_daily_opt_snapshot.py --currencies BTC --max-dte-h 30
|
||||
|
||||
NB ONESTO: questo script MISURA, non decide. La serie che produce serve a rispondere fra ~6 mesi
|
||||
a: (1) IV daily vs RV daily netto haircut, (2) quanto spesso il gate IV-rank aprirebbe a tenor
|
||||
daily, (3) f di stress reale quando capita un crash dentro la finestra di cattura.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
OUT_DIR = PROJECT_ROOT / "data" / "options_daily"
|
||||
OUT_FILE = OUT_DIR / "snapshots.jsonl"
|
||||
API = "https://www.deribit.com/api/v2/public"
|
||||
|
||||
SESSION = requests.Session()
|
||||
|
||||
|
||||
def api(endpoint: str, **params):
|
||||
"""GET pubblico con retry breve. Ritorna result o solleva."""
|
||||
last = None
|
||||
for _ in range(4):
|
||||
try:
|
||||
r = SESSION.get(f"{API}/{endpoint}", params=params, timeout=20)
|
||||
j = r.json()
|
||||
if "result" in j:
|
||||
return j["result"]
|
||||
last = j.get("error")
|
||||
except Exception as e: # rete/JSON: ritenta
|
||||
last = str(e)
|
||||
time.sleep(0.7)
|
||||
raise RuntimeError(f"Deribit API fail {endpoint} {params}: {last}")
|
||||
|
||||
|
||||
def snapshot_currency(cur: str, max_dte_h: float, mny_lo: float, mny_hi: float) -> list[dict]:
|
||||
now_ms = int(time.time() * 1000)
|
||||
snap_ts = now_ms
|
||||
rows: list[dict] = []
|
||||
|
||||
instruments = api("get_instruments", currency=cur, kind="option", expired="false")
|
||||
spot = api("get_index_price", index_name=f"{cur.lower()}_usd")["index_price"]
|
||||
try:
|
||||
dvol_data = api("get_volatility_index_data", currency=cur,
|
||||
start_timestamp=now_ms - 3_600_000, end_timestamp=now_ms,
|
||||
resolution=3600).get("data", [])
|
||||
dvol = float(dvol_data[-1][4]) if dvol_data else None
|
||||
except Exception:
|
||||
dvol = None
|
||||
|
||||
chain = [i for i in instruments
|
||||
if (i["expiration_timestamp"] - now_ms) / 3.6e6 <= max_dte_h
|
||||
and mny_lo * spot <= i["strike"] <= mny_hi * spot]
|
||||
chain.sort(key=lambda i: (i["expiration_timestamp"], i["strike"], i["option_type"]))
|
||||
|
||||
expiries = sorted({i["expiration_timestamp"] for i in chain})
|
||||
rows.append({
|
||||
"rec": "meta", "snap_ts": snap_ts, "currency": cur, "index_price": spot,
|
||||
"dvol": dvol, "n_instruments": len(chain), "expiries": expiries,
|
||||
"max_dte_h": max_dte_h, "moneyness": [mny_lo, mny_hi],
|
||||
})
|
||||
|
||||
for inst in chain:
|
||||
name = inst["instrument_name"]
|
||||
try:
|
||||
t = api("ticker", instrument_name=name)
|
||||
except RuntimeError as e:
|
||||
print(f" WARN ticker {name}: {e}", file=sys.stderr)
|
||||
continue
|
||||
g = t.get("greeks") or {}
|
||||
st = t.get("stats") or {}
|
||||
rows.append({
|
||||
"rec": "chain", "snap_ts": snap_ts, "currency": cur, "instrument": name,
|
||||
"expiry_ts": inst["expiration_timestamp"],
|
||||
"dte_h": round((inst["expiration_timestamp"] - snap_ts) / 3.6e6, 3),
|
||||
"strike": inst["strike"], "type": inst["option_type"],
|
||||
"settlement_period": inst.get("settlement_period"),
|
||||
"min_trade_amount": inst.get("min_trade_amount"),
|
||||
"taker_comm": inst.get("taker_commission"),
|
||||
"bid": t.get("best_bid_price"), "ask": t.get("best_ask_price"),
|
||||
"bid_amount": t.get("best_bid_amount"), "ask_amount": t.get("best_ask_amount"),
|
||||
"mark": t.get("mark_price"), "mark_iv": t.get("mark_iv"),
|
||||
"bid_iv": t.get("bid_iv"), "ask_iv": t.get("ask_iv"),
|
||||
"delta": g.get("delta"), "gamma": g.get("gamma"),
|
||||
"vega": g.get("vega"), "theta": g.get("theta"),
|
||||
"oi": t.get("open_interest"), "volume_24h": st.get("volume"),
|
||||
"underlying": t.get("underlying_price"), "index_price": t.get("index_price"),
|
||||
})
|
||||
time.sleep(0.05) # rate-limit gentile (pubblico: 20 req/s, stiamo larghi)
|
||||
return rows
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Snapshot catena daily-expiry Deribit -> JSONL")
|
||||
ap.add_argument("--currencies", nargs="+", default=["BTC", "ETH"])
|
||||
ap.add_argument("--max-dte-h", type=float, default=50.0,
|
||||
help="cattura tutte le scadenze entro N ore (default 50 = 0DTE+1DTE)")
|
||||
ap.add_argument("--moneyness", nargs=2, type=float, default=[0.75, 1.25],
|
||||
metavar=("LO", "HI"), help="ladder strike in frazione dello spot")
|
||||
args = ap.parse_args()
|
||||
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
total = 0
|
||||
with OUT_FILE.open("a") as f:
|
||||
for cur in args.currencies:
|
||||
try:
|
||||
rows = snapshot_currency(cur, args.max_dte_h, *args.moneyness)
|
||||
except Exception as e:
|
||||
print(f"ERRORE {cur}: {e}", file=sys.stderr)
|
||||
continue
|
||||
for r in rows:
|
||||
f.write(json.dumps(r, separators=(",", ":")) + "\n")
|
||||
total += len(rows)
|
||||
n_chain = sum(1 for r in rows if r["rec"] == "chain")
|
||||
print(f"{cur}: {n_chain} strumenti (+1 meta) appesi")
|
||||
print(f"OK: {total} righe -> {OUT_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,291 @@
|
||||
"""r0724_hlp_deepdive — Deep-dive ALLOCATION sul vault HLP di Hyperliquid.
|
||||
|
||||
Contesto (2026-07-24): il probe HLP (89-95 punti ~14g dall'API nativa, vedi
|
||||
`data/external/hlp_vault.json`) ha mostrato l'unico stream crash-long mai visto dal
|
||||
progetto (corr col book -0.19, +10.4% nel cascade ott-2025). Questo script risponde
|
||||
alle domande aperte con dati reali:
|
||||
|
||||
1. GRANULARITA' DAILY — l'API nativa (`vaultDetails`/`portfolio`) e' downsampled:
|
||||
allTime ~14g (95 pti), month ~10h ma SOLO ultimi 30g, week ~2.4h ultimi 7g
|
||||
→ nessun daily storico ufficiale. Fonti esterne trovate:
|
||||
- CoinGecko `wrapped-hlp` (wHLP di Hyperbeat, redimibile a NAV): prezzo DAILY
|
||||
dal 2025-07-25 → oggi. E' un prezzo di mercato (puo' fare sconto sotto stress),
|
||||
ma e' l'unico mark giornaliero di lungo periodo liberamente accessibile.
|
||||
- DefiLlama `protocol/hyperliquid-hlp`: TVL DAILY dal 2024-12 (per il modello
|
||||
di diluizione return-vs-TVL).
|
||||
- Thunderhead (cloudfront d2v1fiwobg9w6): `hlp_liquidator_pnl` DAILY, ma solo
|
||||
2025-03-05 → 2025-07-12 (stantio; copre pero' il 12-mar e JELLY 26-mar-2025)
|
||||
e `hlp_positions` (esposizione daily per coin dal 2023-06, per la leva).
|
||||
- MORTI: ASXN api-hyperliquid.asxn.xyz `/hlp_pnl` (daily completo, dietro
|
||||
Turnstile anti-bot); DefiLlama yields (HLP non ha token → non e' un pool);
|
||||
stats-data.hyperliquid.xyz espone solo `Mainnet/vaults` (stessi dati downsampled).
|
||||
|
||||
2. CODA — ricostruzione numerica 12-mar-2025 (whale ETH, -$4.28M realizzati in
|
||||
1 giorno dal liquidator) e JELLY 26-mar-2025 (unrealized peak ~-$13.5M su TVL
|
||||
~$240M ≈ -5.6%, salvato da delist+settlement dei validator a $0.0095 → +$703k
|
||||
realizzati). Vedi diario per il worst-case ragionato.
|
||||
|
||||
3-5. Fisco/accesso e allocation math: nel diario. Qui i numeri.
|
||||
|
||||
Uso:
|
||||
uv run python scripts/research/r0724_hlp_deepdive.py # usa cache se c'e'
|
||||
uv run python scripts/research/r0724_hlp_deepdive.py --refresh # ri-scarica
|
||||
|
||||
I fetch sono salvati in data/external/hlp_deepdive/ (gitignored di fatto: non
|
||||
committare i dati). Nessuna azione live: SOLO analisi.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
EXT = ROOT / "data" / "external"
|
||||
CACHE = EXT / "hlp_deepdive"
|
||||
CACHE.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
VAULT = "0xdfc24b077bc1425ad1dea75bcb6f8158e10df303"
|
||||
|
||||
SOURCES = {
|
||||
# prezzo daily wHLP (Hyperbeat Wrapped HLP) — mark di mercato del NAV HLP
|
||||
"whlp_coingecko.json": (
|
||||
"https://api.coingecko.com/api/v3/coins/wrapped-hlp/market_chart"
|
||||
"?vs_currency=usd&days=365&interval=daily"
|
||||
),
|
||||
# TVL daily del vault HLP
|
||||
"llama_hlp_tvl.json": "https://api.llama.fi/protocol/hyperliquid-hlp",
|
||||
# PnL daily del liquidator HLP (stantio: 2025-03-05 → 2025-07-12, copre JELLY)
|
||||
"thunderhead_liq_pnl.json": "https://d2v1fiwobg9w6.cloudfront.net/hlp_liquidator_pnl",
|
||||
}
|
||||
|
||||
|
||||
def fetch(name: str, url: str, refresh: bool) -> dict | list | None:
|
||||
p = CACHE / name
|
||||
if p.exists() and not refresh:
|
||||
return json.loads(p.read_text())
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "pythagoras-research/1.0"})
|
||||
raw = urllib.request.urlopen(req, timeout=60).read()
|
||||
p.write_bytes(raw)
|
||||
return json.loads(raw)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f" [WARN] fetch {name} fallito ({e}); uso cache se esiste")
|
||||
return json.loads(p.read_text()) if p.exists() else None
|
||||
|
||||
|
||||
def refresh_vault_details(refresh: bool) -> dict:
|
||||
"""Snapshot vaultDetails dall'API nativa (stesso formato del probe originale)."""
|
||||
p = EXT / "hlp_vault.json"
|
||||
if p.exists() and not refresh:
|
||||
return json.loads(p.read_text())
|
||||
body = json.dumps({"type": "vaultDetails", "vaultAddress": VAULT}).encode()
|
||||
req = urllib.request.Request(
|
||||
"https://api.hyperliquid.xyz/info", data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
raw = urllib.request.urlopen(req, timeout=60).read()
|
||||
p.write_bytes(raw)
|
||||
return json.loads(raw)
|
||||
|
||||
|
||||
def series_from_period(d: dict, period: str) -> pd.DataFrame:
|
||||
for per, pdata in d["portfolio"]:
|
||||
if per == period:
|
||||
av = pd.DataFrame(pdata["accountValueHistory"], columns=["ts", "av"])
|
||||
pnl = pd.DataFrame(pdata["pnlHistory"], columns=["ts", "pnl"])
|
||||
df = av.merge(pnl, on="ts")
|
||||
df["t"] = pd.to_datetime(df["ts"], unit="ms", utc=True)
|
||||
df["av"] = df["av"].astype(float)
|
||||
df["pnl"] = df["pnl"].astype(float) # PnL CUMULATIVO
|
||||
return df.set_index("t")[["av", "pnl"]]
|
||||
raise KeyError(period)
|
||||
|
||||
|
||||
def ret_stats(r: pd.Series, periods_per_year: float, label: str) -> dict:
|
||||
r = r.replace([np.inf, -np.inf], np.nan).dropna()
|
||||
mu, sd = r.mean(), r.std()
|
||||
sharpe = mu / sd * np.sqrt(periods_per_year) if sd > 0 else np.nan
|
||||
eq = (1 + r).cumprod()
|
||||
dd = (eq / eq.cummax() - 1).min()
|
||||
ann = (1 + mu) ** periods_per_year - 1
|
||||
out = dict(label=label, n=len(r), ann_ret=ann, sharpe=sharpe, maxdd=dd,
|
||||
worst=r.min(), worst_t=str(r.idxmin())[:10], best=r.max())
|
||||
print(f" {label:28s} n={out['n']:4d} ann~{ann*100:6.1f}% Sh={sharpe:5.2f} "
|
||||
f"maxDD={dd*100:5.1f}% worst={r.min()*100:6.2f}% ({out['worst_t']})")
|
||||
return out
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--refresh", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
print("=" * 78)
|
||||
print("HLP DEEP-DIVE — dati al", datetime.now(timezone.utc).isoformat()[:16])
|
||||
print("=" * 78)
|
||||
|
||||
d = refresh_vault_details(args.refresh)
|
||||
print(f"\nVault: {d['name']} APR corrente dichiarato: {d['apr']*100:.3f}%")
|
||||
print(f"isClosed={d['isClosed']} allowDeposits={d['allowDeposits']} "
|
||||
f"maxDistributable=${d['maxDistributable']/1e6:.0f}M")
|
||||
|
||||
# ---------------- A. serie nativa 14g (allTime) ----------------
|
||||
print("\n[A] Serie NATIVA allTime (~14g/punto) — return su AUM di inizio periodo")
|
||||
at = series_from_period(d, "allTime")
|
||||
at["dpnl"] = at["pnl"].diff()
|
||||
at["ret"] = at["dpnl"] / at["av"].shift(1)
|
||||
at.loc[at["av"].shift(1) < 5e6, "ret"] = np.nan # AUM<5M: return non significativo
|
||||
ret14 = at["ret"].iloc[1:]
|
||||
ret_stats(ret14, 365.25 / 14, "HLP 14g FULL (AUM>5M)")
|
||||
for y in (2023, 2024, 2025, 2026):
|
||||
sub = ret14[ret14.index.year == y]
|
||||
if len(sub) > 3:
|
||||
ret_stats(sub, 365.25 / 14, f" anno {y}")
|
||||
print(" Traiettoria 2026 (per periodo 14g):",
|
||||
" ".join(f"{x*100:+.1f}" for x in ret14[ret14.index.year == 2026].dropna()))
|
||||
worst5 = ret14.dropna().nsmallest(5)
|
||||
print(" 5 peggiori periodi 14g:", [(str(i.date()), f"{v*100:+.2f}%") for i, v in worst5.items()])
|
||||
|
||||
# month nativo (~10h granularita', ultimi 30g)
|
||||
mo = series_from_period(d, "month")
|
||||
mo_ret = mo["pnl"].diff() / mo["av"].shift(1)
|
||||
tot30 = (1 + mo_ret.dropna()).prod() - 1
|
||||
print(f" Ultimi 30g (periodo 'month', {len(mo)} pti ~10h): tot {tot30*100:+.2f}%")
|
||||
|
||||
# ---------------- B. wHLP daily (CoinGecko) ----------------
|
||||
print("\n[B] wHLP (CoinGecko, prezzo DAILY di mercato ~ NAV; sconto possibile)")
|
||||
cg = fetch("whlp_coingecko.json", SOURCES["whlp_coingecko.json"], args.refresh)
|
||||
if cg and cg.get("prices"):
|
||||
pr = pd.DataFrame(cg["prices"], columns=["ts", "px"])
|
||||
pr["t"] = pd.to_datetime(pr["ts"], unit="ms", utc=True).dt.normalize()
|
||||
pr = pr.drop_duplicates("t").set_index("t")["px"].astype(float)
|
||||
r1d = pr.pct_change()
|
||||
ret_stats(r1d, 365.25, "wHLP daily FULL (12 mesi)")
|
||||
for lbl, a, b in [
|
||||
("cascade 2025-10-09→13", "2025-10-09", "2025-10-13"),
|
||||
("feb-2026 (liq event)", "2026-02-01", "2026-02-28"),
|
||||
("ultimi 45g", str(pr.index[-1] - pd.Timedelta(days=45))[:10], None),
|
||||
]:
|
||||
w = pr.loc[a:b] if b else pr.loc[a:]
|
||||
if len(w) > 1:
|
||||
print(f" {lbl:26s} {w.iloc[0]:.4f} → {w.iloc[-1]:.4f} "
|
||||
f"({(w.iloc[-1]/w.iloc[0]-1)*100:+.2f}%) min {w.min():.4f}")
|
||||
peak = pr.cummax()
|
||||
cur_dd = pr.iloc[-1] / peak.iloc[-1] - 1
|
||||
print(f" Drawdown CORRENTE dal max ({pr.idxmax().date()} {pr.max():.4f}): "
|
||||
f"{cur_dd*100:+.2f}%")
|
||||
|
||||
# ---------------- C. TVL daily (DefiLlama) → diluizione ----------------
|
||||
print("\n[C] TVL daily (DefiLlama hyperliquid-hlp) + fit return-vs-TVL")
|
||||
ll = fetch("llama_hlp_tvl.json", SOURCES["llama_hlp_tvl.json"], args.refresh)
|
||||
if ll and ll.get("tvl"):
|
||||
tvl = pd.DataFrame(ll["tvl"])
|
||||
tvl["t"] = pd.to_datetime(tvl["date"], unit="s", utc=True).dt.normalize()
|
||||
tvl = tvl.drop_duplicates("t").set_index("t")["totalLiquidityUSD"].astype(float)
|
||||
print(f" TVL {tvl.index[0].date()} ${tvl.iloc[0]/1e6:.0f}M → picco "
|
||||
f"{tvl.idxmax().date()} ${tvl.max()/1e6:.0f}M → oggi ${tvl.iloc[-1]/1e6:.0f}M "
|
||||
f"({(tvl.iloc[-1]/tvl.max()-1)*100:+.0f}% dal picco)")
|
||||
# fit sul nativo: PnL$ 14g vs AUM inizio periodo (tutta la storia)
|
||||
x = at["av"].shift(1).iloc[1:] / 1e6
|
||||
y = at["dpnl"].iloc[1:] / 1e6
|
||||
ok = x.notna() & y.notna()
|
||||
b1, b0 = np.polyfit(x[ok], y[ok], 1)
|
||||
corr_xy = np.corrcoef(x[ok], y[ok])[0, 1]
|
||||
print(f" Fit PnL$_14g = {b0:+.2f}M {b1:+.4f}·TVL(M) corr={corr_xy:+.2f} "
|
||||
f"(b1≈0 ⇒ PnL$ NON scala col TVL ⇒ return-on-AUM ∝ 1/TVL = diluizione)")
|
||||
for lo, hi in [(0, 150), (150, 300), (300, 450), (450, 700)]:
|
||||
m = (x >= lo) & (x < hi)
|
||||
if m.sum() >= 4:
|
||||
impl = (y[m].mean() / x[m].mean()) * (365.25 / 14) * 100
|
||||
print(f" TVL {lo:3d}-{hi:3d}M: n={m.sum():3d} PnL medio "
|
||||
f"${y[m].mean()*1000:+7.0f}k/14g → ~{impl:+.1f}%/anno su AUM")
|
||||
|
||||
# ---------------- D. liquidator daily (thunderhead) — la coda ----------------
|
||||
print("\n[D] Liquidator PnL DAILY (thunderhead, 2025-03-05→2025-07-12 — copre JELLY)")
|
||||
th = fetch("thunderhead_liq_pnl.json", SOURCES["thunderhead_liq_pnl.json"], args.refresh)
|
||||
if th and th.get("chart_data"):
|
||||
liq = pd.DataFrame(th["chart_data"]).dropna()
|
||||
liq["t"] = pd.to_datetime(liq["time"], utc=True)
|
||||
liq = liq.set_index("t")["total_pnl"].astype(float)
|
||||
w = liq.sort_values()
|
||||
print(" 5 peggiori giorni:", [(str(i.date()), f"{v/1e6:+.2f}M") for i, v in w.head(5).items()])
|
||||
print(" 5 migliori giorni:", [(str(i.date()), f"{v/1e6:+.2f}M") for i, v in w.tail(5).items()])
|
||||
for day, note in [("2025-03-12", "whale ETH 50x"), ("2025-03-26", "JELLY settle")]:
|
||||
if day in liq.index.strftime("%Y-%m-%d").tolist():
|
||||
v = liq[liq.index.strftime("%Y-%m-%d") == day].iloc[0]
|
||||
print(f" {day} ({note}): {v/1e6:+.2f}M")
|
||||
|
||||
# ---------------- F. decomposizioni oneste ----------------
|
||||
print("\n[F] Decomposizioni oneste")
|
||||
# carry 2026 ex-evento: togli il singolo periodo +7% (evento liquidazione feb-2026)
|
||||
r26 = ret14[ret14.index.year == 2026].dropna()
|
||||
if len(r26):
|
||||
ev = r26.idxmax()
|
||||
ex = r26.drop(ev)
|
||||
cum_ex = (1 + ex).prod() - 1
|
||||
ann_ex = (1 + cum_ex) ** (365.25 / (14 * len(ex))) - 1
|
||||
print(f" 2026: evento {ev.date()} {r26.max()*100:+.1f}% | ex-evento cum "
|
||||
f"{cum_ex*100:+.2f}% su {len(ex)} periodi ≈ {ann_ex*100:+.1f}%/anno "
|
||||
f"→ IL CARRY E' MORTO, resta solo il crash-alpha")
|
||||
# TVL al giorno JELLY (per scalare il -13.5M unrealized)
|
||||
if ll and ll.get("tvl"):
|
||||
for day in ("2025-03-12", "2025-03-26", "2025-10-10", "2026-02-08"):
|
||||
ts = pd.Timestamp(day, tz="UTC")
|
||||
i = tvl.index.get_indexer([ts], method="nearest")[0]
|
||||
print(f" TVL {day}: ${tvl.iloc[i]/1e6:.0f}M "
|
||||
f"(-13.5M JELLY = {-13.5e6/tvl.iloc[i]*100:.1f}% MtM)" if day == "2025-03-26"
|
||||
else f" TVL {day}: ${tvl.iloc[i]/1e6:.0f}M")
|
||||
# sconto wrapper: wHLP mercato vs NAV nativo su finestra comune (da max wHLP a oggi)
|
||||
if cg and cg.get("prices"):
|
||||
w_from = pr.idxmax()
|
||||
nav_win = ret14[ret14.index >= w_from].dropna()
|
||||
nav_chg = (1 + nav_win).prod() - 1
|
||||
whlp_chg = pr.iloc[-1] / pr.max() - 1
|
||||
print(f" Da {w_from.date()}: wHLP mercato {whlp_chg*100:+.2f}% vs NAV nativo "
|
||||
f"{nav_chg*100:+.2f}% → sconto wrapper ≈ {(whlp_chg-nav_chg)*100:+.1f}pt "
|
||||
f"(costo di un exit-in-stress via wrapper)")
|
||||
|
||||
# ---------------- E. allocation math (Kelly log-utility, outcome discreti) ----------------
|
||||
print("\n[E] Allocation math — Kelly log-utility su outcome annui discreti")
|
||||
# Outcome annui dell'ALLOCAZIONE (non del bankroll): base 2 scenari di coda:
|
||||
# - evento socializzazione stile-JELLY-senza-salvataggio: -20% dell'allocazione
|
||||
# - morte protocollo/bridge (hack, insolvenza, socializzazione totale): -100%
|
||||
# mu base = run-rate onesto 2026 (decay in corso: usare 6%, non il 19% del 2025)
|
||||
scenarios = [
|
||||
("base mu=6%", [(0.06, 0.83), (-0.20, 0.15), (-1.00, 0.02)]),
|
||||
("ottimista mu=12%", [(0.12, 0.87), (-0.20, 0.10), (-1.00, 0.03)]),
|
||||
("pessimista mu=3%", [(0.03, 0.80), (-0.25, 0.17), (-1.00, 0.03)]),
|
||||
]
|
||||
for name, outs in scenarios:
|
||||
mu_adj = sum(x * p for x, p in outs)
|
||||
fgrid = np.linspace(0.001, 0.999, 999)
|
||||
util = [sum(p * np.log(1 + f * x) for x, p in outs) for f in fgrid]
|
||||
f_star = float(fgrid[int(np.argmax(util))])
|
||||
print(f" {name:20s} mu_adj={mu_adj:+.1%} Kelly pieno f*={f_star:.2f} "
|
||||
f"0.25·Kelly={0.25*f_star:.1%} del bankroll")
|
||||
f_use = 0.10 # ~0.25·Kelly dello scenario base, cap operativo
|
||||
print(f" → allocazione difendibile ~{f_use:.0%} del bankroll (VRP01 fu ~0.27 Kelly):")
|
||||
for cap in (600, 2000, 5000, 20000):
|
||||
a = f_use * cap
|
||||
print(f" capitale ${cap:>6,}: ~${a:,.0f} → atteso ~${a*0.06:,.0f}/anno; "
|
||||
f"coda -${a*0.20:,.0f} (JELLY-like) / -${a:,.0f} (protocollo)")
|
||||
|
||||
print("\nFonti dati:")
|
||||
for k, v in SOURCES.items():
|
||||
print(f" {k:28s} {v}")
|
||||
print(" hlp_vault.json https://api.hyperliquid.xyz/info "
|
||||
'{"type":"vaultDetails","vaultAddress":"' + VAULT + '"}')
|
||||
print(" MORTI: ASXN /api/hlp_pnl (Turnstile), DefiLlama yields (HLP non-token), "
|
||||
"stats-data.hyperliquid.xyz (downsampled)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,473 @@
|
||||
"""r0724_pm_deribit_probe — Riconciliazione LIVE Polymarket <-> Deribit su digitali BTC/ETH.
|
||||
|
||||
Domanda: quanto del "gap ~11pp" (arXiv 2606.19517, PM vs prob implicite Deribit) sopravvive
|
||||
OGGI a una riconciliazione onesta che aggiusta i mismatch di SPEC, e il trade hedged
|
||||
(lato cheap su PM + digitale opposta via VERTICAL di opzioni Deribit) ha senso a ~$600?
|
||||
|
||||
Mismatch di spec quantificati (non assunti):
|
||||
1. TEMPO: i PM "above $K on <date>" risolvono sulla candela 1m Binance delle 12:00 ET
|
||||
(=16:00 UTC in estate); le daily Deribit scadono 08:00 UTC (settlement TWAP 30m
|
||||
dell'indice) -> 8h di varianza extra lato PM. dP_time = N(d2;T_pm) - N(d2;T_der).
|
||||
2. FONTE: PM risolve su Binance BTC/USDT; Deribit su indice BTC-USD. Basis misurato
|
||||
live (= sconto USDT/USD): pochi bps di spot MA a orizzonte daily con IV ~15%
|
||||
vale svariati pp di probabilita' ATM. dP_src = N(d2; S*(1+basis)) - N(d2; S).
|
||||
3. REPLICA: la digitale via vertical ha bound sub/super-replicanti:
|
||||
call-spread [K,K+w] <= 1{S>K} <= call-spread [K-w,K] (identico coi put OTM).
|
||||
Lato ITM i book Deribit sono MORTI -> si replica SEMPRE dal lato OTM
|
||||
(K>=S: call; K<S: put, P_above = 1 - put_spread/w). Stima centrale = spread
|
||||
centrato [K-w,K+w] sui mark; banda = [sub_mark, super_mark]; eseguibile =
|
||||
sub-replica incrociando bid/ask REALI.
|
||||
4. COSTI: PM fee 0 ma spread+depth CLOB; Deribit taker 0.0003 ccy/contratto cap
|
||||
12.5% del premio per gamba + delivery 0.00015 (cap 12.5%) sul leg ITM; margine
|
||||
del leg corto in standard margin (nessun netting del vertical in SM).
|
||||
5. RISCHIO NON HEDGIABILE: P(cross) = probabilita' che S stia da lati OPPOSTI di K
|
||||
alle 08:00 e alle 16:00 UTC (le due gambe risolvono in conflitto -> payoff 0 o 2):
|
||||
MC bivariato con sigma implicita. Vicino all'ATM e' grande -> il "lock" non esiste.
|
||||
|
||||
Tutto live, API pubbliche tokenless, zero storage; se un endpoint e' vuoto lo dichiara.
|
||||
Riproducibile: uv run python scripts/research/r0724_pm_deribit_probe.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
|
||||
UA = {"User-Agent": "Mozilla/5.0 (research probe)"}
|
||||
|
||||
|
||||
def get(url: str, timeout: int = 20, retries: int = 2):
|
||||
for k in range(retries + 1):
|
||||
try:
|
||||
req = urllib.request.Request(url, headers=UA)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.load(r)
|
||||
except Exception as e: # noqa: BLE001
|
||||
if k == retries:
|
||||
print(f" [FETCH FAIL] {url[:100]} -> {e}")
|
||||
return None
|
||||
time.sleep(1.0)
|
||||
|
||||
|
||||
def ncdf(x: float) -> float:
|
||||
return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0)))
|
||||
|
||||
|
||||
def bs_digital(S: float, K: float, sig: float, T_yr: float) -> float:
|
||||
if T_yr <= 0 or sig <= 0:
|
||||
return 1.0 if S > K else 0.0
|
||||
d2 = (math.log(S / K) - 0.5 * sig * sig * T_yr) / (sig * math.sqrt(T_yr))
|
||||
return ncdf(d2)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- Polymarket
|
||||
PM_EVENTS = [
|
||||
("BTC", "bitcoin-above-on-july-25-2026", "25JUL26"),
|
||||
("ETH", "ethereum-above-on-july-25-2026", "25JUL26"),
|
||||
("BTC", "bitcoin-above-on-july-26-2026", "26JUL26"),
|
||||
("ETH", "ethereum-above-on-july-26-2026", "26JUL26"),
|
||||
("BTC", "bitcoin-above-on-july-27-2026", "27JUL26"),
|
||||
("ETH", "ethereum-above-on-july-27-2026", "27JUL26"),
|
||||
]
|
||||
|
||||
|
||||
def parse_strike(question: str) -> float | None:
|
||||
import re
|
||||
|
||||
m = re.search(r"\$([\d,]+(?:\.\d+)?)", question)
|
||||
return float(m.group(1).replace(",", "")) if m else None
|
||||
|
||||
|
||||
def fetch_pm():
|
||||
out = []
|
||||
for asset, slug, dexp in PM_EVENTS:
|
||||
ev = get(f"https://gamma-api.polymarket.com/events?slug={slug}")
|
||||
if not ev:
|
||||
print(f"[PM] evento {slug}: VUOTO/bloccato")
|
||||
continue
|
||||
ev = ev[0]
|
||||
for m in ev["markets"]:
|
||||
K = parse_strike(m["question"])
|
||||
if K is None:
|
||||
continue
|
||||
desc = m.get("description", "")
|
||||
tok = json.loads(m.get("clobTokenIds", "[]"))
|
||||
out.append(
|
||||
dict(
|
||||
asset=asset,
|
||||
dexp=dexp,
|
||||
end=ev["endDate"],
|
||||
K=K,
|
||||
yes_bid=float(m["bestBid"]) if m.get("bestBid") else None,
|
||||
yes_ask=float(m["bestAsk"]) if m.get("bestAsk") else None,
|
||||
vol24=float(m.get("volume24hr") or 0),
|
||||
spec_ok=("Binance" in desc and "12:00 in the ET" in desc),
|
||||
yes_token=tok[0] if tok else None,
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def pm_book_depth(token_id: str):
|
||||
b = get(f"https://clob.polymarket.com/book?token_id={token_id}")
|
||||
if not b or not b.get("bids") or not b.get("asks"):
|
||||
return None
|
||||
bids = [(float(x["price"]), float(x["size"])) for x in b["bids"]]
|
||||
asks = [(float(x["price"]), float(x["size"])) for x in b["asks"]]
|
||||
bb, ba = max(p for p, _ in bids), min(p for p, _ in asks)
|
||||
mid = 0.5 * (bb + ba)
|
||||
d = {}
|
||||
for w in (0.01, 0.02):
|
||||
d[w] = (
|
||||
sum(s * p for p, s in bids if p >= mid - w),
|
||||
sum(s * p for p, s in asks if p <= mid + w),
|
||||
)
|
||||
return dict(bb=bb, ba=ba, depth=d)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------- Deribit
|
||||
def fetch_deribit(ccy: str):
|
||||
bs = get(
|
||||
"https://www.deribit.com/api/v2/public/get_book_summary_by_currency"
|
||||
f"?currency={ccy}&kind=option"
|
||||
)
|
||||
idx = get(
|
||||
f"https://www.deribit.com/api/v2/public/get_index_price?index_name={ccy.lower()}_usd"
|
||||
)
|
||||
ins = get(
|
||||
f"https://www.deribit.com/api/v2/public/get_instruments?currency={ccy}"
|
||||
"&kind=option&expired=false"
|
||||
)
|
||||
if not bs or not idx or not ins:
|
||||
return None
|
||||
books = {row["instrument_name"]: row for row in bs["result"]}
|
||||
expts = {}
|
||||
for i in ins["result"]:
|
||||
expts[i["instrument_name"].split("-")[1]] = i["expiration_timestamp"] / 1000.0
|
||||
return dict(books=books, index=idx["result"]["index_price"], expts=expts)
|
||||
|
||||
|
||||
def _q(books, ccy, dexp, strike, typ, index):
|
||||
"""(bid_usd, ask_usd, mark_usd) di uno strumento; None se assente."""
|
||||
n = f"{ccy}-{dexp}-{int(strike)}-{typ}"
|
||||
r = books.get(n)
|
||||
if not r:
|
||||
return None
|
||||
return dict(
|
||||
n=n,
|
||||
bid=(r["bid_price"] or 0.0) * index,
|
||||
ask=(r["ask_price"] * index) if r["ask_price"] else None,
|
||||
mark=(r.get("mark_price") or 0.0) * index,
|
||||
)
|
||||
|
||||
|
||||
def deribit_digital(books, ccy, dexp, K, index):
|
||||
"""Stima onesta di P(S_der > K) via vertical OTM-side.
|
||||
|
||||
Ritorna: mark centrato, banda [sub_mark, super_mark], eseguibili
|
||||
(exec_buy = costo per COMPRARE la sub-replica dell'above-digitale ai prezzi reali;
|
||||
exec_sell = incasso per VENDERLA), gambe usate.
|
||||
"""
|
||||
typ = "C" if K >= index else "P"
|
||||
strikes = sorted(
|
||||
float(n.split("-")[2])
|
||||
for n in books
|
||||
if n.split("-")[1] == dexp and n.endswith(f"-{typ}")
|
||||
)
|
||||
if K not in strikes:
|
||||
return None # tutti gli strike PM qui coincidono con strike listati
|
||||
i = strikes.index(K)
|
||||
if i == 0 or i == len(strikes) - 1:
|
||||
return None
|
||||
Km, Kp = strikes[i - 1], strikes[i + 1]
|
||||
qm, q0, qp = (
|
||||
_q(books, ccy, dexp, Km, typ, index),
|
||||
_q(books, ccy, dexp, K, typ, index),
|
||||
_q(books, ccy, dexp, Kp, typ, index),
|
||||
)
|
||||
if not (qm and q0 and qp):
|
||||
return None
|
||||
|
||||
if typ == "C":
|
||||
centered = (qm["mark"] - qp["mark"]) / (Kp - Km)
|
||||
sub = (q0["mark"] - qp["mark"]) / (Kp - K) # <= P
|
||||
sup = (qm["mark"] - q0["mark"]) / (K - Km) # >= P
|
||||
# comprare sub-replica: buy C(K), sell C(Kp)
|
||||
exec_buy = (
|
||||
(q0["ask"] - qp["bid"]) / (Kp - K) if q0["ask"] is not None else None
|
||||
)
|
||||
exec_sell = (
|
||||
(q0["bid"] - qp["ask"]) / (Kp - K) if qp["ask"] is not None else None
|
||||
)
|
||||
legs = (q0, qp)
|
||||
else:
|
||||
# P_above = 1 - put_spread/w ; [K,Kp] put spread super-replica il below
|
||||
centered = 1.0 - (qp["mark"] - qm["mark"]) / (Kp - Km)
|
||||
sub = 1.0 - (qp["mark"] - q0["mark"]) / (Kp - K) # <= P_above
|
||||
sup = 1.0 - (q0["mark"] - qm["mark"]) / (K - Km) # >= P_above
|
||||
# comprare (sinteticamente) l'above = VENDERE il put spread [K,Kp]:
|
||||
# incasso bid(Kp)-ask(K); prezzo implicito pagato = 1 - incasso/w
|
||||
exec_buy = (
|
||||
1.0 - (qp["bid"] - q0["ask"]) / (Kp - K) if q0["ask"] is not None else None
|
||||
)
|
||||
exec_sell = (
|
||||
1.0 - (qp["ask"] - q0["bid"]) / (Kp - K) if qp["ask"] is not None else None
|
||||
)
|
||||
legs = (q0, qp)
|
||||
return dict(
|
||||
typ=typ,
|
||||
Km=Km,
|
||||
Kp=Kp,
|
||||
w=Kp - K,
|
||||
centered=centered,
|
||||
sub=sub,
|
||||
sup=sup,
|
||||
exec_buy=exec_buy,
|
||||
exec_sell=exec_sell,
|
||||
legs=legs,
|
||||
)
|
||||
|
||||
|
||||
IV_CACHE: dict = {}
|
||||
|
||||
|
||||
def deribit_iv(ccy: str, dexp: str, name: str) -> float | None:
|
||||
if name in IV_CACHE:
|
||||
return IV_CACHE[name]
|
||||
t = get(f"https://www.deribit.com/api/v2/public/ticker?instrument_name={name}")
|
||||
iv = None
|
||||
if t:
|
||||
iv = t["result"].get("mark_iv")
|
||||
iv = iv / 100.0 if iv else None
|
||||
IV_CACHE[name] = iv
|
||||
return iv
|
||||
|
||||
|
||||
def p_cross(S, K, sig, T1_yr, T2_yr, n=200_000, seed=7):
|
||||
"""MC: P(lati opposti di K a T1 e T2) e P(sopra a T2 ma sotto a T1) ecc."""
|
||||
rng = random.Random(seed)
|
||||
lo_hi = hi_lo = 0
|
||||
lnK = math.log(K / S)
|
||||
s1 = sig * math.sqrt(T1_yr)
|
||||
s2x = sig * math.sqrt(max(T2_yr - T1_yr, 1e-12))
|
||||
for _ in range(n):
|
||||
x1 = -0.5 * s1 * s1 + s1 * rng.gauss(0, 1)
|
||||
x2 = x1 - 0.5 * s2x * s2x + s2x * rng.gauss(0, 1)
|
||||
a, b = x1 > lnK, x2 > lnK
|
||||
if not a and b:
|
||||
lo_hi += 1
|
||||
elif a and not b:
|
||||
hi_lo += 1
|
||||
return lo_hi / n, hi_lo / n
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------- run
|
||||
def main():
|
||||
now = datetime.now(timezone.utc)
|
||||
print(f"=== PROBE PM<->DERIBIT {now.isoformat(timespec='seconds')} ===\n")
|
||||
|
||||
pm = fetch_pm()
|
||||
nbad = sum(1 for m in pm if not m["spec_ok"])
|
||||
print(f"[PM] {len(pm)} mercati above/below; spec Binance/12:00ET non confermata su {nbad}")
|
||||
|
||||
der = {c: fetch_deribit(c) for c in ("BTC", "ETH")}
|
||||
for c, d in der.items():
|
||||
if d:
|
||||
print(f"[Deribit] {c} index={d['index']:.2f}")
|
||||
|
||||
def binance_spot(sym):
|
||||
for host in ("https://api.binance.com", "https://data-api.binance.vision"):
|
||||
r = get(f"{host}/api/v3/ticker/price?symbol={sym}", timeout=10, retries=0)
|
||||
if r and "price" in r:
|
||||
return float(r["price"])
|
||||
return None
|
||||
|
||||
binance = {"BTC": binance_spot("BTCUSDT"), "ETH": binance_spot("ETHUSDT")}
|
||||
kr = get("https://api.kraken.com/0/public/Ticker?pair=USDTZUSD", timeout=10, retries=1)
|
||||
usdtusd = None
|
||||
try:
|
||||
usdtusd = float(list(kr["result"].values())[0]["c"][0])
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
print(f"[Binance] BTCUSDT={binance['BTC']} ETHUSDT={binance['ETH']} | USDT/USD={usdtusd}")
|
||||
basis = {}
|
||||
for c in ("BTC", "ETH"):
|
||||
basis[c] = (binance[c] / der[c]["index"] - 1.0) if (der[c] and binance[c]) else 0.0
|
||||
print(f" basis {c} BinanceUSDT vs indice Deribit: {basis[c]*1e4:+.1f} bps")
|
||||
|
||||
rows = []
|
||||
for m in pm:
|
||||
c, d = m["asset"], der[m["asset"]]
|
||||
if not d or m["dexp"] not in d["expts"]:
|
||||
continue
|
||||
S = d["index"]
|
||||
dig = deribit_digital(d["books"], c, m["dexp"], m["K"], S)
|
||||
if dig is None:
|
||||
continue
|
||||
T_der = max(d["expts"][m["dexp"]] - now.timestamp(), 0) / (365.25 * 86400)
|
||||
t_pm = datetime.fromisoformat(m["end"].replace("Z", "+00:00")).timestamp()
|
||||
T_pm = max(t_pm - now.timestamp(), 0) / (365.25 * 86400)
|
||||
near = abs(math.log(m["K"] / S)) < 0.09
|
||||
sig = deribit_iv(c, m["dexp"], dig["legs"][0]["n"]) if near else None
|
||||
if sig is None:
|
||||
sig = 0.45
|
||||
dP_time = bs_digital(S, m["K"], sig, T_pm) - bs_digital(S, m["K"], sig, T_der)
|
||||
dP_src = bs_digital(S * (1 + basis[c]), m["K"], sig, T_pm) - bs_digital(
|
||||
S, m["K"], sig, T_pm
|
||||
)
|
||||
pm_mid = (
|
||||
0.5 * (m["yes_bid"] + m["yes_ask"])
|
||||
if (m["yes_bid"] is not None and m["yes_ask"] is not None)
|
||||
else (m["yes_ask"] or m["yes_bid"])
|
||||
)
|
||||
raw = pm_mid - dig["centered"] if pm_mid is not None else None
|
||||
resid = raw - dP_time - dP_src if raw is not None else None
|
||||
depth = pm_book_depth(m["yes_token"]) if (m["yes_token"] and near) else None
|
||||
rows.append(
|
||||
dict(
|
||||
m=m,
|
||||
dig=dig,
|
||||
sig=sig,
|
||||
T_der=T_der,
|
||||
T_pm=T_pm,
|
||||
dP_time=dP_time,
|
||||
dP_src=dP_src,
|
||||
pm_mid=pm_mid,
|
||||
raw=raw,
|
||||
resid=resid,
|
||||
depth=depth,
|
||||
S=S,
|
||||
near=near,
|
||||
)
|
||||
)
|
||||
|
||||
print("\n=== RICONCILIAZIONE (prob %, gap pp; digitale = vertical OTM-side) ===")
|
||||
hdr = (
|
||||
f"{'mkt':<22}{'PM b/a':>12}{'Der mark[sub,sup]':>20}{'exec b/s':>13}"
|
||||
f"{'raw':>7}{'dT':>6}{'dSrc':>6}{'resid':>7}{'iv%':>5}"
|
||||
)
|
||||
print(hdr)
|
||||
print("-" * len(hdr))
|
||||
for r in rows:
|
||||
m, g = r["m"], r["dig"]
|
||||
name = f"{m['asset']} >{int(m['K'])} {m['dexp'][:5]}"
|
||||
pmba = (
|
||||
f"{(m['yes_bid'] or 0)*100:.1f}/{(m['yes_ask'] or 0)*100:.1f}"
|
||||
if (m["yes_bid"] is not None or m["yes_ask"] is not None)
|
||||
else "n/a"
|
||||
)
|
||||
ex = (
|
||||
f"{g['exec_buy']*100:.0f}/{g['exec_sell']*100:.0f}"
|
||||
if (g["exec_buy"] is not None and g["exec_sell"] is not None)
|
||||
else "n/q"
|
||||
)
|
||||
print(
|
||||
f"{name:<22}{pmba:>12}"
|
||||
f"{g['centered']*100:>8.1f}[{g['sub']*100:.0f},{g['sup']*100:.0f}]".ljust(42)
|
||||
+ f"{ex:>13}"
|
||||
f"{(r['raw'] or 0)*100:>+7.1f}{r['dP_time']*100:>+6.1f}"
|
||||
f"{r['dP_src']*100:>+6.1f}{(r['resid'] or 0)*100:>+7.1f}"
|
||||
f"{r['sig']*100:>5.0f}"
|
||||
)
|
||||
print(
|
||||
"\n Der mark = spread centrato sui mark, [sub,sup] = bound di replica;"
|
||||
" exec b/s = comprare/vendere la sub-replica ai bid/ask REALI."
|
||||
"\n raw = PM_mid - Der_mark; resid = raw - dT - dSrc"
|
||||
" (>0: PM sovraprezza il lato above vs Deribit spec-adjusted)."
|
||||
)
|
||||
|
||||
# sistematicita' per bucket di moneyness
|
||||
print("\n=== RESIDUO PER BUCKET (solo quote PM a doppio lato) ===")
|
||||
buckets = {"ITM(K<S-1.5%)": [], "ATM(|1.5%|)": [], "OTM(K>S+1.5%)": []}
|
||||
for r in rows:
|
||||
if r["resid"] is None or r["m"]["yes_bid"] is None or r["m"]["yes_ask"] is None:
|
||||
continue
|
||||
lm = math.log(r["m"]["K"] / r["S"])
|
||||
b = "ITM(K<S-1.5%)" if lm < -0.015 else ("OTM(K>S+1.5%)" if lm > 0.015 else "ATM(|1.5%|)")
|
||||
buckets[b].append(r["resid"])
|
||||
for b, v in buckets.items():
|
||||
if v:
|
||||
pos = sum(1 for x in v if x > 0)
|
||||
print(
|
||||
f" {b:<15} n={len(v):>2} resid medio {sum(v)/len(v)*100:+.1f}pp"
|
||||
f" mediana {sorted(v)[len(v)//2]*100:+.1f}pp >0: {pos}/{len(v)}"
|
||||
)
|
||||
|
||||
print("\n=== DEPTH CLOB PM (vicino allo spot) ===")
|
||||
for r in rows:
|
||||
if r["depth"]:
|
||||
m, dp = r["m"], r["depth"]
|
||||
d1, d2 = dp["depth"][0.01], dp["depth"][0.02]
|
||||
print(
|
||||
f" {m['asset']} >{int(m['K'])} {m['dexp']}: book {dp['bb']:.3f}/{dp['ba']:.3f}"
|
||||
f" depth±1c ${d1[0]:,.0f}/${d1[1]:,.0f} ±2c ${d2[0]:,.0f}/${d2[1]:,.0f}"
|
||||
f" vol24h ${m['vol24']:,.0f}"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ trade hedged demo
|
||||
print("\n=== TRADE HEDGED a size minima (0.1 BTC / 1 ETH) — numeri VERI ===")
|
||||
min_amt = {"BTC": 0.1, "ETH": 1.0}
|
||||
fee_rate, dlv_rate = 0.0003, 0.00015
|
||||
for r in rows:
|
||||
m, g = r["m"], r["dig"]
|
||||
if r["resid"] is None or not r["near"] or abs(r["resid"]) < 0.01:
|
||||
continue
|
||||
if g["exec_buy"] is None or g["exec_sell"] is None:
|
||||
continue
|
||||
c, S = m["asset"], r["S"]
|
||||
amt = min_amt[c]
|
||||
W = g["w"] * amt # notional digitale $
|
||||
if r["resid"] > 0:
|
||||
# PM ricco sul lato above: buy PM NO al (1-yes_bid) + buy digitale-above Deribit
|
||||
pm_px = 1 - (m["yes_bid"] or 0)
|
||||
der_px = g["exec_buy"]
|
||||
side = "BUY PM NO + LONG vertical OTM (sub-replica above)"
|
||||
else:
|
||||
pm_px = m["yes_ask"] or 1
|
||||
der_px = 1 - g["exec_sell"]
|
||||
side = "BUY PM YES + SHORT vertical OTM (sub-replica below)"
|
||||
cost = (pm_px + der_px) * W
|
||||
lock = W - cost
|
||||
fees = 0.0
|
||||
for leg in g["legs"]:
|
||||
fees += min(fee_rate * amt * S, 0.125 * leg["mark"] * amt)
|
||||
dlv = min(dlv_rate * amt * S, 0.125 * max(le["mark"] for le in g["legs"]) * amt)
|
||||
otm_frac = abs(g["Kp"] - S) / S
|
||||
mark_short = g["legs"][1]["mark"] / S
|
||||
im = (max(0.15 - otm_frac, 0.10) + mark_short) * amt
|
||||
pc = p_cross(S, m["K"], r["sig"], r["T_der"], r["T_pm"])
|
||||
capital = cost + im * S
|
||||
net = lock - fees - dlv
|
||||
days = r["T_pm"] * 365.25
|
||||
print(f"\n {m['asset']} >{int(m['K'])} {m['dexp']} resid {r['resid']*100:+.1f}pp -> {side}")
|
||||
print(
|
||||
f" gambe Deribit {g['legs'][0]['n']}/{g['legs'][1]['n']} amount {amt} {c}"
|
||||
f" -> payout digitale ${W:,.0f}"
|
||||
)
|
||||
print(
|
||||
f" costo PM ${pm_px*W:,.2f} + Deribit ${der_px*W:,.2f} = ${cost:,.2f};"
|
||||
f" lock lordo ${lock:,.2f}; fee entry ${fees:.2f} + delivery ${dlv:.2f}"
|
||||
f" -> netto ${net:,.2f}"
|
||||
)
|
||||
print(
|
||||
f" margine leg corto (SM, no netting) ~${im*S:,.0f};"
|
||||
f" capitale impegnato ~${capital:,.0f};"
|
||||
f" ritorno se lock regge: {net/capital*100:.2f}% in {days:.1f}g"
|
||||
f" (~{net/capital*365.25/days*100:.0f}%/anno)"
|
||||
)
|
||||
print(
|
||||
f" RISCHIO CROSS 08->16 UTC (MC, iv {r['sig']*100:.0f}%):"
|
||||
f" P(sotto@08,sopra@16)={pc[0]*100:.1f}% P(sopra@08,sotto@16)={pc[1]*100:.1f}%"
|
||||
f" -> P(gambe in conflitto)={sum(pc)*100:.1f}%"
|
||||
)
|
||||
|
||||
print("\n[NOTE] Deribit settle = TWAP 30m pre-08:00 UTC vs PM candela 1m 16:00 UTC;")
|
||||
print("la sub-replica paga <1 nella rampa [K,K+w] -> il 'lock' e' un bound inferiore")
|
||||
print("solo FUORI dalla rampa; ATM con w=500 (BTC) la rampa e' ~1-1.5 sigma daily.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user