Files
PythagorasGoal/scripts/research/r0822_alt_options.py
T

625 lines
31 KiB
Python

"""ALT-OPT — la famiglia opzioni USDC-LINEARE di Deribit, mai guardata dal progetto (2026-08-22).
CONTESTO. Il filone VRP-QUOTE-VERE di questa stessa ondata ha trovato che `collect_chain.py`
interroga `{"currency": "BTC"|"ETH"}`, che su Deribit restituisce le sole opzioni INVERSE
(regolate in BTC/ETH). Il conto e' in USDC. Esiste accanto una famiglia USDC-LINEARE con lotti
molto piu' piccoli, e contando l'open interest sembra che su SOL/XRP/HYPE/AVAX sia molto piu'
popolata di BTC_USDC. Il progetto non l'ha mai misurata.
DOMANDA. Esiste su questa famiglia una struttura a RISCHIO DEFINITO (put credit spread, la stessa
geometria di VRP01: vendi ~-0.28, compri ~-0.10) eseguibile con qualche centinaio di dollari, con
spread e liquidita' che non se la mangino?
IPOTESI DICHIARATA PRIMA DI MISURARE: che il vincolo binding fosse la TAGLIA DEL LOTTO (e' cosi'
che il progetto ha sempre bocciato le opzioni: "BTC min 0.1 = $6.210/lotto -> FUORI a $3.000") e
che le famiglie alt, con lotti da poche decine di dollari, la superassero. Sbagliata due volte:
il lotto non e' il vincolo, e la classifica di liquidita' su cui poggia la domanda si ROVESCIA.
COSA MISURA, in ordine, fermandosi appena un vincolo la uccide:
0. l'UNITA' dei contratti — due test empirici, perche' leggerla male sposta ogni cifra in
dollari di un fattore 10-1000 (errore commesso nella prima stesura di questo file);
1. i fatti del venue (min_trade_amount, tick_size, contract_size, $ di UN lotto);
2. la liquidita' misurata in DUE modi che danno risposte opposte: open interest vs quote
effettivamente presenti. E' il risultato principale del file;
3. la struttura: delta REALIZZATI, credito eseguibile vs credito al mark, spread, fee, max-loss;
4. il collaterale e quante strutture entrano in $635;
5. quanto costerebbe raccogliere il dato (perche' il verdetto massimo qui e' LEAD).
COSA NON PUO' FARE, detto prima dei numeri: **non esiste un backtest possibile**. Il nostro
collettore raccoglie solo le inverse BTC/ETH e l'archivio ereditato pure: zero giorni di storia su
queste famiglie. Questo file misura UN ISTANTE.
VINCOLI DI PROGETTO CITATI, non aggirati:
(a) "niente short-vol da modello in deploy" (19/06). Qui i prezzi sono VERI, non modellati —
che e' un'altra cosa — ma la regola resta finche' non esiste un campione che contenga uno
STRESS. Un istante di calma non e' un campione.
(b) SOL e' escluso dall'universo DIREZIONALE per decisione dell'operatore del 22/08. Quella
decisione riguarda TP01/SKH01 congelati come terza gamba di TREND, e il suo stesso testo
dichiara che un meccanismo diverso la riapre; un put credit spread non e' direzionale in
quel senso. Ma il guardrail sul DATO vale identico: nessun file nuovo in `data/raw/`, e i
prezzi del sottostante qui vengono da `underlying_price` della catena, mai da `load_data`.
(Nota: qui SOL non viene proposto — viene BOCCIATO, e sulla misura, non sulla decisione.)
(c) il gate IV-rank>0.30 e' l'unico alpha misurato di VRP01 e su questi sottostanti NON e'
calcolabile: non abbiamo un giorno di storia di vol implicita. Senza quel gate la struttura
non e' VRP01, e' vendere vol a caso.
CONVENZIONE DI UNITA' (la trappola di questo file, verificata al §0): le opzioni INVERSE si
quotano in CRIPTO per contratto, le USDC-LINEARI in USDC per UNITA' DI SOTTOSTANTE; e
`min_trade_amount` e' in unita' di sottostante, NON in contratti — quindi non va moltiplicato per
`contract_size` (farlo gonfia XRP_USDC da $1.469 a $1.469.000 per lotto).
RETE: sole letture pubbliche, nessun ordine, pacing ~2 richieste/s, e il file SI RIFIUTA di
partire nella finestra :24-:29 (e' quando `cron_chain` usa lo stesso IP: il rate limit Deribit e'
per-IP e il progetto ha gia' avuto un guasto per questo, 29/07).
nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py
nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py --cache
"""
from __future__ import annotations
import argparse
import json
import math
import time
from datetime import UTC, datetime
from pathlib import Path
import numpy as np
import requests
API = "https://www.deribit.com/api/v2/public"
TIMEOUT = 20
RPS = 2.0
CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/altopt_cache.json")
SHORT_DELTA, LONG_DELTA = -0.28, -0.10 # la geometria di VRP01, non ri-scelta qui
DTE_LO, DTE_HI = 4.0, 45.0 # dal settimanale di VRP01 (4-10) a ~1.5 mesi
OI_MIN = 100.0 # la soglia che usa collect_chain.py, qui sotto esame
CAPITAL = 635.0 # il conto vero
RISK_BUDGET = 0.20 # quota di conto a rischio, per la capacita'
TAKER_CAP = 0.125 # cap fee opzioni Deribit: 12.5% del premio
LINEAR = ("ETH_USDC", "BTC_USDC", "SOL_USDC", "XRP_USDC", "HYPE_USDC", "AVAX_USDC")
INVERSE = ("BTC", "ETH") # termine di paragone: il progetto le conosce
FAMS = LINEAR + INVERSE
# ---------------------------------------------------------------- rete (parca, sola lettura)
class Budget:
def __init__(self, rps: float = RPS) -> None:
self.rps, self._next, self.calls, self.err, self.r429 = rps, 0.0, 0, 0, 0
def wait(self) -> None:
now = time.monotonic()
if now < self._next:
time.sleep(self._next - now)
self._next = max(now, self._next) + 1.0 / self.rps
def guardia_finestra() -> None:
m = datetime.now(UTC).minute
if 24 <= m <= 29:
raise SystemExit(f"minuto {m}: finestra di cron_chain (:25-:29). Rilancia dopo il :30.")
def get(path: str, params: dict, b: Budget, tries: int = 3):
for k in range(tries):
b.wait()
b.calls += 1
try:
r = requests.get(f"{API}/{path}", params=params, timeout=TIMEOUT)
except Exception:
b.err += 1
time.sleep(1.5 * (k + 1))
continue
if r.status_code == 429:
b.r429 += 1
time.sleep(2.0 * (k + 1))
continue
if r.status_code != 200:
b.err += 1
time.sleep(1.0 * (k + 1))
continue
try:
return r.json()["result"]
except Exception:
b.err += 1
return None
return None
def fetch_all(b: Budget) -> dict:
"""6 chiamate: instruments + book_summary per USDC, BTC, ETH. Niente per-strumento."""
out = {}
for cur in ("USDC", "BTC", "ETH"):
out[f"inst_{cur}"] = get("get_instruments",
{"currency": cur, "kind": "option", "expired": "false"}, b) or []
out[f"summ_{cur}"] = get("get_book_summary_by_currency",
{"currency": cur, "kind": "option"}, b) or []
return out
# ---------------------------------------------------------------- Black-Scholes (r=0, come il sleeve)
def put_delta(S: float, K: float, T: float, sig: float) -> float:
if not (S > 0 and K > 0 and T > 0 and sig > 0):
return float("nan")
d1 = (math.log(S / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T))
return 0.5 * (1.0 + math.erf(d1 / math.sqrt(2.0))) - 1.0
# ---------------------------------------------------------------- unita'
def is_inv(fam: str) -> bool:
return fam in INVERSE
def usd(price: float | None, r: dict) -> float:
"""Prezzo di UN LOTTO MINIMO, in dollari.
Lineare: il prezzo e' gia' in USDC per unita' di sottostante -> x min_trade_amount.
Inverse: il prezzo e' in CRIPTO per contratto -> x min_trade_amount x sottostante.
`contract_size` NON entra: `min_trade_amount` e' gia' in unita' di sottostante (§0).
"""
if price is None:
return float("nan")
return price * r["minamt"] * (r["S"] if is_inv(r["fam"]) else 1.0)
def loss_usd(Ks: float, Kl: float, r: dict) -> float:
"""Perdita massima strutturale, per un lotto minimo.
Lineare: esatta, (Ks-Kl) x quantita'. Inverse: il payoff e' in cripto (max(0,K-S)/S), il caso
peggiore e' a S=Kl e vale (Ks-Kl)/Kl cripto = (Ks-Kl) dollari A QUEL PREZZO -> stessa formula,
ma APPROSSIMATA (il valore in dollari dipende da dove finisce S). Dichiarata, non nascosta.
"""
return (Ks - Kl) * r["minamt"]
# ---------------------------------------------------------------- margine (FONTE SECONDARIA)
def im_short_put(S: float, K: float, mark_lot: float, r: dict) -> float:
"""Initial margin di una short put, in USDC, per UN LOTTO, formula PUBBLICATA da Deribit:
IM = max(max(0.15 - OTM/S, 0.10), 0.075) x S + mark [per unita' di sottostante]
⚠️ FONTE SECONDARIA, dichiarata: l'API PUBBLICA non espone il margine. E il numero che decide
davvero — se Deribit NETTA le due gambe di uno spread in margine standard — non e' leggibile
da qui: si conferma con `private/get_margins`, che richiede una chiave (lettura, ma non
pubblica) e quindi NON e' stato usato.
"""
if not (S > 0 and K > 0):
return float("nan")
otm = max(0.0, S - K) / S # una put e' OTM quando K < S
return max(max(0.15 - otm, 0.10), 0.075) * S * r["minamt"] + max(0.0, mark_lot)
# ---------------------------------------------------------------- vista
def build(raw: dict) -> list[dict]:
inst, summ = {}, {}
for cur in ("USDC", "BTC", "ETH"):
for i in raw.get(f"inst_{cur}") or []:
inst[i["instrument_name"]] = i
for s in raw.get(f"summ_{cur}") or []:
summ[s["instrument_name"]] = s
rows, now_ms = [], datetime.now(UTC).timestamp() * 1000.0
for name, i in inst.items():
s = summ.get(name)
if s is None:
continue
fam = name.split("-")[0]
dte = (float(i.get("expiration_timestamp") or 0) - now_ms) / 86400_000.0
bid, ask = s.get("bid_price"), s.get("ask_price")
bid = float(bid) if bid else None # Deribit manda 0/None per "nessun lato"
ask = float(ask) if ask else None
iv, S = s.get("mark_iv"), s.get("underlying_price")
K, T = float(i.get("strike") or 0), max(dte, 0.0) / 365.25
typ = "P" if i.get("option_type") == "put" else "C"
rows.append(dict(
name=name, fam=fam, typ=typ, K=K, dte=dte, S=float(S) if S else float("nan"),
iv=float(iv) if iv else float("nan"),
delta=(put_delta(float(S), K, T, float(iv) / 100.0)
if (S and iv and K and T > 0 and typ == "P") else float("nan")),
bid=bid, ask=ask, mid=((bid + ask) / 2 if (bid and ask) else None),
mark=float(s.get("mark_price") or 0.0),
oi=float(s.get("open_interest") or 0.0), vol=float(s.get("volume") or 0.0),
vol_usd=float(s.get("volume_usd") or 0.0),
tick=float(i.get("tick_size") or 0.0), minamt=float(i.get("min_trade_amount") or 0.0),
csize=float(i.get("contract_size") or 0.0),
blockmin=float(i.get("block_trade_min_trade_amount") or 0.0),
taker=float(i.get("taker_commission") or 0.0003), settle=i.get("settlement_currency"),
))
return rows
def rel_spread(r: dict) -> float:
if r["bid"] is None or r["ask"] is None or not r["mid"]:
return float("nan")
return (r["ask"] - r["bid"]) / r["mid"]
# ---------------------------------------------------------------- §0 le unita'
def unita(rows: list[dict], b: Budget, rete: bool) -> None:
print("\n" + "=" * 104)
print("0. L'UNITA' DEI CONTRATTI — due test empirici, prima di qualunque cifra in dollari")
print("=" * 104)
print(" Test 1 (dai dati gia' scaricati): `volume_usd / (volume x mark_price)`.")
print(" = 1 se `volume` e' in UNITA' DI SOTTOSTANTE; = contract_size se e' in CONTRATTI;")
print(" = prezzo del sottostante se il mark e' in cripto (inverse).")
print(f"\n {'famiglia':<10} {'n oss':>6} {'vol_usd/(vol*mark)':>20} {'c_size':>8} "
f"{'sottost.':>10} {'lettura':>26}")
for f in FAMS:
rr = [r for r in rows if r["fam"] == f and r["vol"] > 0 and r["mark"] > 0]
if not rr:
continue
q = float(np.median([r["vol_usd"] / (r["vol"] * r["mark"]) for r in rr]))
cs, S = rr[0]["csize"], float(np.nanmedian([r["S"] for r in rr]))
if abs(q - 1.0) < 0.35:
lettura = "unita' di sottostante"
elif abs(q - cs) / max(cs, 1) < 0.35:
lettura = "CONTRATTI"
elif abs(q - S) / max(S, 1) < 0.35:
lettura = "cripto/contratto (inverse)"
else:
lettura = "?? non concludente"
print(f" {f:<10} {len(rr):>6} {q:>20.4f} {cs:>8g} {S:>10.2f} {lettura:>26}")
if not rete:
print("\n Test 2 (size minima nel book): non girato (--cache non usa la rete).")
return
print("\n Test 2 (rete): la size MINIMA presente nel book. Se compare esattamente")
print(" `min_trade_amount`, allora e' quello il lotto minimo IN UNITA' DI SOTTOSTANTE.")
print(f"\n {'famiglia':<10} {'min_amt':>9} {'c_size':>8} {'min size vista':>15} "
f"{'libri':>6} {'verdetto':>12}")
for f in LINEAR:
cand = sorted([r for r in rows if r["fam"] == f and (r["bid"] or r["ask"])],
key=lambda r: -r["oi"])[:4]
sizes, nb = [], 0
for r in cand:
ob = get("get_order_book", {"instrument_name": r["name"], "depth": 10}, b)
if not ob:
continue
nb += 1
for side in ("bids", "asks"):
sizes += [float(l[1]) for l in (ob.get(side) or [])]
if not sizes:
continue
mn, ma, cs = min(sizes), cand[0]["minamt"], cand[0]["csize"]
v = "confermato" if abs(mn - ma) < 1e-9 else ("CONTRATTI" if abs(mn - ma * cs) < 1e-9
else "non concl.")
print(f" {f:<10} {ma:>9g} {cs:>8g} {mn:>15g} {nb:>6} {v:>12}")
print("\n ⚠️ Errore commesso e corretto in sessione: la prima stesura moltiplicava")
print(" min_trade_amount x contract_size, e dava XRP_USDC a $1.468.900 di nozionale per")
print(" lotto. Un numero assurdo e' un test: nessun venue retail quota un minimo da $1,5M.")
# ---------------------------------------------------------------- §1 fatti del venue
def famiglie(rows: list[dict]) -> dict:
print("\n" + "=" * 104)
print("1. I FATTI DEL VENUE — public/get_instruments + get_book_summary_by_currency")
print("=" * 104)
print(f"{'famiglia':<10} {'strum':>6} {'min_amt':>9} {'c_size':>7} {'tick':>9} "
f"{'sottost.$':>11} {'$/lotto':>9} {'taker':>7} {'settle':>7}")
tab = {}
for f in FAMS:
sub = [r for r in rows if r["fam"] == f]
if not sub:
continue
S = float(np.nanmedian([r["S"] for r in sub]))
a = sub[0]
tab[f] = dict(n=len(sub), minamt=a["minamt"], csize=a["csize"], tick=a["tick"], S=S,
lot=a["minamt"] * S, taker=a["taker"],
nliq=sum(1 for r in sub if r["oi"] >= OI_MIN))
print(f"{f:<10} {len(sub):>6} {a['minamt']:>9.4g} {a['csize']:>7.4g} {a['tick']:>9.5g} "
f"{S:>11.2f} {tab[f]['lot']:>9.2f} {a['taker']:>7.4g} {str(a['settle']):>7}")
print("\n `$/lotto` = min_trade_amount x sottostante = il NOZIONALE di un lotto (NON il premio,")
print(" che e' molto minore). E' la granularita' minima imposta dal venue.")
print(" `taker` per le opzioni Deribit e' una frazione del SOTTOSTANTE con cap al 12.5% del")
print(" premio: sull'ala molto OTM morde il CAP, non la percentuale.")
print(" 📌 Il lotto minimo NON e' il vincolo: da $242 (ETH_USDC) a $1.469 (XRP_USDC), contro")
print(" i $7.738 di BTC inverse su cui il progetto aveva costruito il muro delle opzioni.")
return tab
# ---------------------------------------------------------------- §2 liquidita', due modi
def liquidita(rows: list[dict], tab: dict) -> None:
print("\n" + "=" * 104)
print("2. LA LIQUIDITA', MISURATA IN DUE MODI CHE DANNO RISPOSTE OPPOSTE")
print("=" * 104)
print(" A) open interest >= 100 = quante POSIZIONI sono aperte (il filtro che usa")
print(" `collect_chain.py`, ereditato da bite, e la misura su cui poggia la domanda).")
print(" B) quote a DUE LATI presenti adesso = quanti strumenti sono NEGOZIABILI a un prezzo")
print(" noto. Sui soli put nella finestra 4-45 giorni, cioe' dove vive la struttura.")
print(f"\n{'famiglia':<10} {'A: OI>=100':>11} {'rango A':>8} {'put 4-45g':>10} "
f"{'B: 2 lati':>10} {'% quotati':>10} {'rango B':>8}")
stat = {}
for f in FAMS:
puts = [r for r in rows if r["fam"] == f and r["typ"] == "P" and DTE_LO <= r["dte"] <= DTE_HI]
two = [r for r in puts if r["bid"] is not None and r["ask"] is not None]
stat[f] = dict(oi=tab[f]["nliq"], puts=len(puts), two=len(two),
pct=(len(two) / len(puts) if puts else float("nan")))
rA = {f: i + 1 for i, f in enumerate(sorted(stat, key=lambda x: -stat[x]["oi"]))}
rB = {f: i + 1 for i, f in enumerate(sorted(stat, key=lambda x: -stat[x]["pct"]))}
for f in sorted(FAMS, key=lambda x: -stat[x]["oi"]):
s = stat[f]
print(f"{f:<10} {s['oi']:>11} {rA[f]:>8} {s['puts']:>10} {s['two']:>10} "
f"{s['pct']:>9.0%} {rB[f]:>8}")
print("\n 📌 LA CLASSIFICA SI ROVESCIA. SOL_USDC e' 1° per open interest e ULTIMO per quote")
print(" presenti; BTC_USDC, dichiarato 'praticamente morto' (5 strumenti con OI>=100), ha")
print(" quasi tutti i put quotati a due lati. L'open interest misura posizioni ACCUMULATE,")
print(" non la possibilita' di negoziare ADESSO — e sull'universo lineare le due cose non")
print(" hanno quasi nulla a che vedere.")
print(" ⚠️ Ricaduta sul codice di produzione: `collect_chain.py` prefiltra a OI>=100. Sulle")
print(" inverse non fa danno (sono quotate comunque), ma NON e' un filtro di liquidita' e il")
print(" suo nome lo lascia credere. 4ª occorrenza di 'una riga presente non e' un dato")
print(" presente' — qui in versione: 'una posizione aperta non e' un prezzo negoziabile'.")
# ---------------------------------------------------------------- §3 la struttura
def scegli(rows: list[dict], fam: str, eseguibile: bool) -> list[dict]:
"""Per ogni scadenza: la put piu' vicina a -0.28 (corta) e a -0.10 (lunga).
`eseguibile=False` -> fra TUTTI gli strike (la struttura che si vorrebbe).
`eseguibile=True` -> solo fra gli strike che hanno il LATO che serve: per APRIRE bisogna
vendere la corta al BID e comprare l'ala all'ASK. E' una restrizione
asimmetrica, ed e' quella giusta: un book a un lato solo non e' un
book vuoto, e' un book che serve solo una direzione.
"""
puts = [r for r in rows if r["fam"] == fam and r["typ"] == "P"
and DTE_LO <= r["dte"] <= DTE_HI and np.isfinite(r["delta"])]
out = []
for e in sorted({round(r["dte"], 4) for r in puts}):
g = [r for r in puts if round(r["dte"], 4) == e]
gs = [r for r in g if r["bid"] is not None] if eseguibile else g
gl = [r for r in g if r["ask"] is not None] if eseguibile else g
if not gs or not gl:
continue
s = min(gs, key=lambda r: abs(r["delta"] - SHORT_DELTA))
lo = min(gl, key=lambda r: abs(r["delta"] - LONG_DELTA))
if s["K"] > lo["K"]:
out.append({"dte": e, "short": s, "long": lo, "n": len(g)})
return out
def fee_lotto(r: dict) -> float:
"""Fee taker di UNA gamba, un lotto: min(0.03% del nozionale, 12.5% del premio)."""
return min(r["taker"] * r["S"] * r["minamt"], TAKER_CAP * usd(r["mark"], r))
def struttura(rows: list[dict]) -> dict:
print("\n" + "=" * 104)
print(f"3. LA STRUTTURA — put credit spread {SHORT_DELTA}/{LONG_DELTA}, finestra "
f"{DTE_LO:.0f}-{DTE_HI:.0f} giorni, per UN LOTTO MINIMO")
print("=" * 104)
print(" `f_venue` = (bid_corta - ask_ala) / (mark_corta - mark_ala) = quanta parte del")
print(" credito che il venue stesso considera equo resta dopo aver attraversato lo spread.")
print(" E' il gemello del `f` misurato dal progetto il 30/07 (0,73 = errore del MODELLO):")
print(" qui il modello non c'entra, il riferimento e' il mark del venue.")
print(f"\n{'famiglia':<10} {'scad':>4} {'d corta':>8} {'d ala':>7} {'spr% corta':>11} "
f"{'spr% ala':>9} {'cred mark$':>11} {'cred eseg$':>11} {'f_venue':>8} "
f"{'fee/cred':>9} {'maxloss$':>9}")
res = {}
for f in FAMS:
gg = scegli(rows, f, eseguibile=True)
if not gg:
print(f"{f:<10} {'0':>4} nessuna scadenza con corta-bid E ala-ask presenti")
continue
acc = {k: [] for k in ("ds", "dl", "ss", "sl", "cm", "ce", "fee", "ml")}
for g in gg:
s, lo = g["short"], g["long"]
cm = usd(s["mark"], s) - usd(lo["mark"], lo)
ce = usd(s["bid"], s) - usd(lo["ask"], lo)
if cm <= 0:
continue
acc["ds"].append(s["delta"]); acc["dl"].append(lo["delta"])
acc["ss"].append(rel_spread(s)); acc["sl"].append(rel_spread(lo))
acc["cm"].append(cm); acc["ce"].append(ce)
acc["fee"].append(2.0 * (fee_lotto(s) + fee_lotto(lo)) / cm)
acc["ml"].append(loss_usd(s["K"], lo["K"], s) - ce)
if not acc["cm"]:
print(f"{f:<10} {len(gg):>4} nessuna struttura con credito positivo al mark")
continue
m = {k: float(np.nanmedian(v)) if v else float("nan") for k, v in acc.items()}
m["fv"] = float(np.median([e / c for e, c in zip(acc["ce"], acc["cm"])]))
m["n"] = len(acc["cm"])
res[f] = m
print(f"{f:<10} {m['n']:>4} {m['ds']:>8.3f} {m['dl']:>7.3f} {m['ss']*100:>10.1f}% "
f"{m['sl']*100:>8.1f}% {m['cm']:>11.2f} {m['ce']:>11.2f} {m['fv']:>8.3f} "
f"{m['fee']*100:>8.1f}% {m['ml']:>9.2f}")
print("\n ⚠️ `spr%` puo' essere NaN quando la gamba ha un lato solo: e' negoziabile per")
print(" aprire (il lato che serve c'e') ma non ha un mid, quindi non ha uno spread definito.")
print(" `fee/cred` = 2 gambe x (apertura + chiusura) col cap 12.5%. VRP01 tiene FINO A")
print(" SCADENZA (nessuna gestione infra-settimana, il profit-take e' REFUTED il 30/07),")
print(" quindi il costo realistico e' ~la META' di questa colonna + la delivery fee.")
print(f"\n --- controllo: gli STESSI strike scelti fra TUTTI gli strike (non solo quotati) ---")
print(f" {'famiglia':<10} {'ideale d corta':>15} {'eseg. d corta':>14} {'ideale d ala':>13} "
f"{'eseg. d ala':>12} {'scad ideali':>12} {'scad eseg.':>11}")
for f in FAMS:
gi, ge = scegli(rows, f, False), scegli(rows, f, True)
if not gi:
continue
di = float(np.median([g["short"]["delta"] for g in gi]))
li = float(np.median([g["long"]["delta"] for g in gi]))
de = float(np.median([g["short"]["delta"] for g in ge])) if ge else float("nan")
le = float(np.median([g["long"]["delta"] for g in ge])) if ge else float("nan")
print(f" {f:<10} {di:>15.3f} {de:>14.3f} {li:>13.3f} {le:>12.3f} "
f"{len(gi):>12} {len(ge):>11}")
print(" Se la colonna `eseg.` si allontana dal target -0.28/-0.10, la struttura che si")
print(" comprerebbe NON e' quella richiesta: la griglia degli strike quotati la deforma.")
return res
def profondita(rows: list[dict], fams: list[str], b: Budget) -> None:
print("\n" + "=" * 104)
print("3b. PROFONDITA' IN CIMA AL BOOK (public/get_order_book depth=5) sulle gambe scelte")
print("=" * 104)
print(f"{'famiglia':<10} {'gamba':<6} {'n':>3} {'lotti al best':>14} {'$ premio al best':>17} "
f"{'prezzo/tick':>12} {'max|d venue-BS|':>17}")
for f in fams:
gg = scegli(rows, f, eseguibile=True)[:4] # max 4 scadenze: budget di rete
for lato, key, side in (("corta", "short", "bids"), ("ala", "long", "asks")):
lots, prem, tk, dd = [], [], [], []
for g in gg:
r = g[key]
ob = get("get_order_book", {"instrument_name": r["name"], "depth": 5}, b)
if not ob or not (ob.get(side) or []):
continue
px, sz = float(ob[side][0][0]), float(ob[side][0][1])
lots.append(sz / r["minamt"] if r["minamt"] else float("nan"))
prem.append(usd(px, r) * (sz / max(r["minamt"], 1e-12)))
if r["tick"]:
tk.append(px / r["tick"])
gv = (ob.get("greeks") or {}).get("delta")
if gv is not None and np.isfinite(r["delta"]):
dd.append(abs(float(gv) - r["delta"]))
if not lots:
print(f"{f:<10} {lato:<6} {'0':>3} book vuoto sul lato che serve")
continue
print(f"{f:<10} {lato:<6} {len(lots):>3} {np.median(lots):>14.1f} "
f"{np.median(prem):>17.2f} {(np.median(tk) if tk else float('nan')):>12.1f} "
f"{(max(dd) if dd else float('nan')):>17.4f}")
print("\n `lotti al best` = size in cima al book / lotto minimo: quante strutture minime")
print(" entrano senza muovere il prezzo. `prezzo/tick` e' il controllo d'artefatto del")
print(" 30/07: un prezzo a 1-2 tick e' una griglia, non un prezzo.")
print(" `max|d venue-BS|` valida il MIO delta contro le greche del venue: se non e' piccolo,")
print(" tutta la selezione degli strike qui sopra e' sbagliata.")
# ---------------------------------------------------------------- §4 collaterale e capacita'
def collaterale(rows: list[dict], res: dict) -> None:
print("\n" + "=" * 104)
print(f"4. COLLATERALE E CAPACITA' — conto reale ${CAPITAL:.0f}")
print("=" * 104)
print(f"{'famiglia':<10} {'IM corta+ala$':>14} {'max-loss$':>10} {'n @IM':>7} "
f"{'n @maxloss':>11} {'n a budget 20%':>15} {'credito lordo$':>15}")
for f, m in res.items():
gg = scegli(rows, f, eseguibile=True)
ims = []
for g in gg:
s, lo = g["short"], g["long"]
if lo["ask"] is None:
continue
ims.append(im_short_put(s["S"], s["K"], usd(s["mark"], s), s) + usd(lo["ask"], lo))
if not ims:
continue
im, ml = float(np.median(ims)), m["ml"]
n_budget = math.floor(RISK_BUDGET * CAPITAL / ml) if ml > 0 else 0
print(f"{f:<10} {im:>14.2f} {ml:>10.2f} {CAPITAL/im:>7.1f} {CAPITAL/ml:>11.1f} "
f"{n_budget:>15} {n_budget*m['ce']:>15.2f}")
print("\n `IM corta+ala` = margine iniziale della sola put venduta (formula PUBBLICATA dal")
print(" venue, FONTE SECONDARIA) + il premio dell'ala, che si paga in contanti = il")
print(" fabbisogno se il venue NON netta le gambe. `max-loss` = il fabbisogno se LE NETTA.")
print(" ⚠️ Quale dei due valga NON e' leggibile dall'API pubblica: si chiude con")
print(" `private/get_margins` (lettura autenticata, fuori dal perimetro di questo file).")
print(" Ma la conclusione NON dipende da quale sia: il collaterale non e' il vincolo in")
print(" nessuna delle due letture.")
print(f" `n a budget 20%` = strutture che stanno in {RISK_BUDGET:.0%} del conto a max-loss;")
print(" `credito lordo` = il loro credito ESEGUIBILE sommato, per un ciclo. E' un incasso")
print(" LORDO PRIMA DI QUALUNQUE PERDITA, non un rendimento atteso: la struttura perde fino")
print(" a ~7x il credito quando va male (misura del progetto sul canonico BTC/ETH).")
def costo_raccolta(rows: list[dict], tab: dict) -> None:
print("\n" + "=" * 104)
print("5. COSTO DI RACCOLTA — cosa costerebbe aggiungere una famiglia al giro esistente")
print("=" * 104)
print(" `collect_chain.py` oggi: ~650 chiamate in ~160s a 4 rps (BTC+ETH inverse), al :25.")
print(f"{'famiglia':<10} {'a OI>=100':>10} {'a 2-lati':>9} {'+s a 4rps (2-lati)':>19} "
f"{'+% sul giro':>12}")
for f in LINEAR:
sub = [r for r in rows if r["fam"] == f]
two = sum(1 for r in sub if r["bid"] is not None and r["ask"] is not None)
print(f"{f:<10} {tab[f]['nliq']:>10} {two:>9} {(two+2)/4.0:>19.1f} "
f"{(two+2)/650*100:>11.0f}%")
print("\n ⚠️ Il vincolo non e' la CPU ma il rate limit Deribit PER-IP, gia' costato un guasto")
print(" il 29/07 (fallback silenzioso del feed 5m di SKH01). Ogni famiglia aggiunta allunga")
print(" la finestra in cui il giro occupa l'IP.")
# ---------------------------------------------------------------- main
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--cache", action="store_true", help="riusa lo snapshot su disco (niente rete)")
a = ap.parse_args()
print("=" * 104)
print("ALT-OPT — opzioni USDC-LINEARI su Deribit: c'e' una struttura eseguibile a $635?")
print("=" * 104)
print("Sola lettura pubblica, nessun ordine. UNO SNAPSHOT: qui non esiste un backtest.")
b, rete = Budget(), not a.cache
if a.cache and CACHE.exists():
raw = json.loads(CACHE.read_text())
print(f"\n[cache] {CACHE}")
else:
guardia_finestra()
t0 = time.time()
raw = fetch_all(b)
CACHE.parent.mkdir(parents=True, exist_ok=True)
CACHE.write_text(json.dumps(raw))
print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} 429, {time.time()-t0:.0f}s")
rows = build(raw)
print(f"[dato] {len(rows)} strumenti con specifiche E quote — snapshot "
f"{datetime.now(UTC):%Y-%m-%d %H:%M} UTC")
if not rows:
print("NESSUN DATO: il venue non ha risposto. Verdetto: non misurabile.")
return 1
unita(rows, b, rete)
tab = famiglie(rows)
liquidita(rows, tab)
res = struttura(rows)
vive = [f for f in res if res[f]["n"] >= 2]
if rete and vive:
guardia_finestra()
profondita(rows, vive, b)
else:
print("\n3b. PROFONDITA': non girata (--cache, oppure nessuna famiglia con >=2 strutture).")
collaterale(rows, res)
costo_raccolta(rows, tab)
print("\n" + "=" * 104)
print("6. COSA QUESTO FILE NON PUO' DIRE (dichiarato prima di qualunque verdetto)")
print("=" * 104)
print(" * Non c'e' STORIA: il collettore raccoglie BTC/ETH inverse, l'archivio ereditato")
print(" pure. Zero giorni di catena su queste famiglie -> nessun backtest, nessun hold-out,")
print(" nessun deflated-Sharpe. Il verdetto massimo ottenibile e' LEAD.")
print(" * Non c'e' IV-RANK: il gate>0.30 e' l'unico alpha misurato di VRP01 e richiede la")
print(" distribuzione storica della vol implicita del sottostante. Senza, la struttura non")
print(" e' VRP01: e' vendere vol a caso. (Su BTC/ETH quel gate e' passato 0/19 settimane.)")
print(" * Non c'e' STRESS nel campione: la regola 'niente short-vol da modello in deploy' e'")
print(" del 19/06 e la condizione dichiarata per rivalutarla e' un crash CATTURATO.")
print(" * Il margine e' da FORMULA PUBBLICATA, non dall'API, e il netting delle gambe non e'")
print(" leggibile pubblicamente.")
print(" * E' UN ISTANTE, per giunta di SABATO pomeriggio: uno spread relativo mediano a")
print(" un'ora del fine settimana non e' la sua distribuzione. Un book alt puo' essere")
print(" molto piu' quotato in settimana. Servirebbe almeno un ciclo settimanale.")
print(f"\n[rete, totale] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429")
return 0
if __name__ == "__main__":
raise SystemExit(main())