"""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}") # la correlazione di rango si CALCOLA, non si racconta: sulle sole famiglie lineari, # cioe' quelle su cui la domanda del filone e' stata posta. a = np.array([rA[f] for f in LINEAR], float) c = np.array([rB[f] for f in LINEAR], float) rho = float(np.corrcoef(a, c)[0, 1]) print(f"\n 📌 LA CLASSIFICA SI ROVESCIA. Correlazione di rango fra i due criteri sulle sei") print(f" famiglie lineari: {rho:+.2f}. SOL_USDC e' 1ª per open interest fra le lineari e") print(" ULTIMA di tutte per quote presenti; BTC_USDC, dichiarato 'praticamente morto' (5") print(" strumenti con OI>=100), ha quasi tutti i put quotati a due lati. L'open interest") print(" 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'.") print("\n" + "-" * 104) print("2b. E ALLORA DOV'E' LA LIQUIDITA' CHE SI VEDE NEL VOLUME? Nel mercato a BLOCCHI,") print(" che ha un minimo d'ordine suo — e non e' raggiungibile con $635.") print("-" * 104) print(f"{'famiglia':<10} {'con volume 24h':>15} {'volume 24h $':>14} {'blocco min':>11} " f"{'blocco min $':>13} {'x il conto':>11}") for f in FAMS: sub = [r for r in rows if r["fam"] == f] if not sub: continue nv = sum(1 for r in sub if r["vol"] > 0) vusd = sum(r["vol_usd"] for r in sub) bm = sub[0]["blockmin"] bm_usd = bm * tab[f]["S"] * (1.0 if not is_inv(f) else 1.0) print(f"{f:<10} {nv:>15} {vusd:>14,.0f} {bm:>11g} {bm_usd:>13,.0f} " f"{bm_usd/CAPITAL:>10.0f}x") print("\n 📌 SOL_USDC ha molti piu' strumenti con volume nelle ultime 24h che quotati a due") print(" lati: quelle opzioni si negoziano, ma via blocchi/RFQ. Il minimo di blocco e' un") print(" fatto del venue, non un'opinione, e a $635 e' fuori portata di due ordini di") print(" grandezza. **Non si conclude che il mercato alt non esista: si conclude che la parte") print(" a cui questo conto ha accesso — il book continuo — e' quella sottile.**") # ---------------------------------------------------------------- §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.") print("\n --- CONTROLLO POSITIVO: questo snapshot e' di SABATO. E' atipico? ---") print(" Non posso aspettare una settimana, ma posso puntare lo stesso strumento su una") print(" grandezza che il progetto ha GIA' misurato su 19 settimane di giorni feriali:") print(" attraversare lo spread sul canonico BTC/ETH inverse costa ~10% del credito (30/07).") for f in INVERSE: if f in res: print(f" {f:<4} inverse: costo di attraversamento oggi = " f"{(1-res[f]['fv'])*100:5.1f}% del credito (atteso ~10%)") print(" Se coincide, il sabato non sta degradando i book che il progetto conosce, e la") print(" lettura sulle famiglie nuove non e' un artefatto del giorno. **E' un controllo") print(" debole** (una grandezza sola, un istante), non una prova di tipicita'.") robustezza_dte(rows) return res def robustezza_dte(rows: list[dict]) -> None: """L'UNICO grado di liberta' di questo file, esposto invece che nascosto. VRP01 e' settimanale (tenor 7g, finestra 4-10). Qui la finestra e' stata allargata a 4-45 per dare piu' scadenze alle famiglie alt, che ne hanno poche. Allargare una finestra e' una scelta, e una scelta va misurata: se la conclusione dipendesse da essa, sarebbe la conclusione a essere stata scelta. """ global DTE_HI print("\n --- ROBUSTEZZA: la finestra DTE e' il mio unico grado di liberta' ---") print(f" {'famiglia':<10} {'finestra':>9} {'scad':>5} {'f_venue':>8} {'spr% corta':>11} " f"{'cred eseg$':>11} {'maxloss$':>9}") orig = DTE_HI try: for hi in (10.0, 45.0): DTE_HI = hi for f in FAMS: gg = scegli(rows, f, eseguibile=True) fv, ss, ce, ml = [], [], [], [] for g in gg: s, lo = g["short"], g["long"] cm = usd(s["mark"], s) - usd(lo["mark"], lo) c = usd(s["bid"], s) - usd(lo["ask"], lo) if cm <= 0: continue fv.append(c / cm); ss.append(rel_spread(s)); ce.append(c) ml.append(loss_usd(s["K"], lo["K"], s) - c) w = f"{DTE_LO:.0f}-{hi:.0f}g" if not fv: print(f" {f:<10} {w:>9} {len(gg):>5} {'n/d':>8}") continue print(f" {f:<10} {w:>9} {len(fv):>5} {np.median(fv):>8.3f} " f"{np.nanmedian(ss)*100:>10.1f}% {np.median(ce):>11.2f} " f"{np.median(ml):>9.2f}") print() finally: DTE_HI = orig print(" 📌 Da leggere sull'ORDINE, non sulla terza cifra: le quote si muovono fra un giro e") print(" l'altro (su tre snapshot a pochi minuti ETH_USDC ha dato 0.947-0.983, le inverse") print(" 0.847-0.942, le alt 0.79-0.87, SOL sempre negativo). Cio' che NON si muove e' la") print(" graduatoria, e alla finestra VERA di VRP01 (4-10g) e' piu' netta, non meno.") 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())