"""USDC-SURFACE — la superficie opzioni USDC-LINEARE, misurata contro la sua gemella INVERSE appaiata a STESSA SCADENZA E STESSO STRIKE (2026-08-23, filone §64). IL BUCO. Tutta la catena opzioni che il progetto possiede e' della famiglia INVERSE (`collect_chain.py` interroga `{"currency":"BTC"|"ETH"}`). Il conto e' in USDC e NON puo' marginare l'inverse: se un giorno il progetto tradasse opzioni sarebbero le LINEARI. Ogni conclusione del progetto sulla famiglia lineare e' oggi un controfattuale costruito sui mid inverse — §28 ha gia' pagato quell'errore in un'altra veste. COSA C'E' GIA', e NON viene rifatto: §14 ALT-OPTIONS (r0822_alt_options.py) ha censito SEI famiglie lineari e la struttura SHORT (put credit spread -0.28/-0.10) su strike AUTO-SELEZIONATI per famiglia. §46 TAIL-HEDGE ha misurato f sulle quote vere USDC per il lato LONG e ha refutato la copertura statica (maxDD sale in 162 celle su 162). §51 NEWDATA-SCOUT raccomanda di RACCOGLIERE la catena USDC (+117 chiamate/giro = +18%). QUI si fa l'unica cosa che nessuno dei tre ha fatto: **il confronto APPAIATO** fra le due famiglie sullo STESSO strumento economico (stessa scadenza, stesso strike, stesso tipo) — che e' l'unico confronto informativo, perche' un confronto fra strike auto-selezionati misura la GRIGLIA degli strike quotati e non la famiglia. ATTESE DICHIARATE PRIMA DI MISURARE (derivate dai numeri gia' pubblicati e dall'aritmetica dei tick, NON dai dati di oggi; ognuna e' stampata a video con l'esito): A1 REPLICA §51 — coi filtri del collettore vivo (<=95 giorni, OI>=100) la catena USDC BTC+ETH e' 113 strumenti contro 649 inverse (+17,4%). Atteso: replica entro +-15% (e' un ricomputo a un giorno di distanza e l'OI si muove). A2 REPLICA §14 — la correlazione di rango fra "OI>=100" e "% put quotati a due lati" sulle sei famiglie lineari e' -0,77. Atteso: stesso segno e stesso ordine di grandezza. A3 FATTO (b), TICK — `tick_size = 5.0` su tutte le put BTC_USDC: atteso CONFERMATO. "5,00 USDC su tutte le USDC": atteso REFUTATO (§14 pubblica ETH_USDC 0,2 / SOL 0,1). E soprattutto l'INFERENZA implicita — che il pavimento del tick sia un muro della famiglia LINEARE — atteso REFUTATO: 1 tick inverse vale 0,0001 BTC = ~$7,7 contro i $5,00 del lineare, quindi il pavimento e' PEGGIORE sulla famiglia che il progetto gia' usa. (Aritmetica sui tick pubblicati, non ancora una misura.) A4 SPREAD APPAIATO — mi aspetto la lineare PIU' LARGA dell'inverse a pari strike (il flusso sta sull'inverse). ⚠️ Contro-indizio gia' pubblicato: §14 da' f_venue ETH_USDC 0,980 contro ETH inverse 0,930 su strike auto-selezionati. Attesa dichiarata INCERTA, 50/50. A5 PREZZO APPAIATO — |ΔIV| appaiata piccola rispetto al mezzo-spread: stesso sottostante, stessi market maker, arbitraggio diretto fra le due famiglie. Atteso CONFERMATO. E' l'attesa che DECIDE la raccomandazione §51: se le due superfici prezzano uguale, la catena inverse e' gia' un proxy sufficiente e raccogliere la lineare non ripara nessun numero. A6 LATO LONG — nessun muro di CAPITALE in nessuna delle due famiglie (comprare costa il premio, §46). Il muro e' il tick, ed e' peggio sull'inverse (A3). Atteso CONFERMATO. A7 COSA ABILITA — l'unica cosa che la lineare abilita e l'inverse no e' la struttura SHORT a rischio definito con max-loss piccolo (§14: ETH_USDC $16,62 contro ETH inverse $145,88), cioe' esattamente cio' che la regola "niente short-vol da modello in deploy" vieta. Atteso CONFERMATO. CONTROLLI POSITIVI (un rilevatore che non trova nulla e' indistinguibile da uno rotto): C1 potenza del confronto appaiato: ri-appaio ogni lineare con l'inverse allo strike ADIACENTE invece che allo stesso. Se |ΔIV| non cresce, il confronto non ha risoluzione e ogni "le due superfici coincidono" e' vuoto. C2 il rilevatore di pavimento: tick/mark dev'essere trascurabile ATM e grande sulle ali. Se desse lo stesso numero ovunque sarebbe rotto. C3 il rilevatore di larghezza: SOL_USDC (9% di put quotati, §14) dev'essere molto peggio di BTC_USDC (93%). Se non li separa, non separa niente. C4 tipicita' del giorno: e' DOMENICA. Non posso aspettare, ma punto la stessa grandezza che il progetto ha misurato su 19 settimane FERIALI: attraversare lo spread sul canonico inverse costa ~10% del credito (30/07). §14 ha letto 7,0-7,3% di SABATO. COSA QUESTO FILE NON PUO' DIRE, prima di qualunque numero: * Non esiste STORIA su questa famiglia: zero giorni di catena raccolta -> nessun backtest, nessun hold-out, nessun deflated-Sharpe. Qui si misura la NEGOZIABILITA', che e' una proprieta' strutturale, non un edge. **Nessuna conclusione sul VALORE di una strategia.** * Non e' un'istantanea sola ma neanche una distribuzione: N snapshot in una sessione danno la dispersione INFRA-SESSIONE, non quella fra giorni della settimana. * Il margine non e' leggibile dall'API pubblica (serve `private/get_margins`, chiave -> fuori perimetro). Dove serve, si usa la formula PUBBLICATA (fonte secondaria, dichiarata). RETE: sole GET pubbliche, nessun ordine, mai. Pacing <=2 req/s. Il file SI RIFIUTA di partire nelle finestre :05-:10 (`cron_book`) e :24-:30 (`cron_chain`): 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/r0823b_usdc_surface.py --snap nice -n 19 timeout 900 uv run python scripts/research/r0823b_usdc_surface.py --report nice -n 19 timeout 900 uv run python scripts/research/r0823b_usdc_surface.py # snap+report """ 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 SNAPDIR = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/usdcsurf") # --- parametri, tutti dichiarati qui e nessuno scelto guardando l'esito ------------------------ CAPITAL = 635.0 # il conto vero RISK_BUDGET = 0.20 # quota di conto a rischio, come §14 OI_MIN = 100.0 # la soglia di collect_chain.py DTE_MAX_COLL = 95.0 # la finestra di collect_chain.py DTE_LO, DTE_HI = 4.0, 45.0 # la finestra di §14 (dal settimanale VRP01 a ~1.5 mesi) TAKER = 0.0003 # frazione del sottostante TAKER_CAP = 0.125 # cap: 12.5% del premio SHORT_D, LONG_D = -0.28, -0.10 # la geometria di VRP01, non ri-scelta qui TAIL_D = -0.05 # l'ala di §46 (lato long) PAIRS = (("BTC_USDC", "BTC"), ("ETH_USDC", "ETH")) LINEAR6 = ("ETH_USDC", "BTC_USDC", "SOL_USDC", "XRP_USDC", "HYPE_USDC", "AVAX_USDC") DEPTH_DELTAS = (-0.05, -0.10, -0.28, -0.50) # ------------------------------------------------------------------ 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 5 <= m <= 10: raise SystemExit(f"minuto {m}: finestra di cron_book (:07). Rilancia dopo il :10.") if 24 <= m <= 30: raise SystemExit(f"minuto {m}: finestra di cron_chain (:25). 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 # ------------------------------------------------------------------ Black-76 (r=0, come il sleeve) def _ncdf(x: float) -> float: return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0))) def put_delta(F: float, K: float, T: float, sig: float) -> float: if not (F > 0 and K > 0 and T > 0 and sig > 0): return float("nan") d1 = (math.log(F / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T)) return _ncdf(d1) - 1.0 def put_price(F: float, K: float, T: float, sig: float) -> float: """Prezzo di una put, in USD per UNITA' di sottostante (r=0, F = forward).""" if not (F > 0 and K > 0 and T > 0 and sig > 0): return float("nan") d1 = (math.log(F / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T)) d2 = d1 - sig * math.sqrt(T) return K * _ncdf(-d2) - F * _ncdf(-d1) def eff_tick(price: float, base: float, steps: list) -> float: """Tick EFFETTIVO al prezzo dato: Deribit ha `tick_size_steps` e ignorarlo sbaglia il pavimento proprio dove NON morde (opzioni care) e lo lascia giusto dove morde (ali).""" t = base for s in sorted(steps or [], key=lambda x: float(x["above_price"])): if price > float(s["above_price"]): t = float(s["tick_size"]) return t # ------------------------------------------------------------------ snapshot def fetch_snapshot(b: Budget) -> dict: """6 chiamate per l'intera superficie + order_book su un insieme DETERMINISTICO di gambe. L'insieme e' deterministico apposta: se cambiasse fra snapshot, la dispersione misurerebbe la mia scelta invece del mercato. """ raw = {"ts": datetime.now(UTC).isoformat()} for cur in ("USDC", "BTC", "ETH"): raw[f"inst_{cur}"] = get("get_instruments", {"currency": cur, "kind": "option", "expired": "false"}, b) or [] raw[f"summ_{cur}"] = get("get_book_summary_by_currency", {"currency": cur, "kind": "option"}, b) or [] rows = build(raw) names = depth_targets(rows) raw["depth"] = {} for n in names: ob = get("get_order_book", {"instrument_name": n, "depth": 5}, b) if ob: raw["depth"][n] = {"bids": ob.get("bids") or [], "asks": ob.get("asks") or [], "greeks": ob.get("greeks") or {}, "mark": ob.get("mark_price")} return raw def depth_targets(rows: list[dict]) -> list[str]: """Le stesse gambe in ENTRAMBE le famiglie: le put ai delta DEPTH_DELTAS, nelle due scadenze piu' vicine dentro [DTE_LO, DTE_HI], scelte SOLO fra gli strike che esistono in entrambe.""" out: list[str] = [] for lin, inv in PAIRS: both = [r for r in rows if r["fam"] == lin and r["typ"] == "P" and r["twin"] and DTE_LO <= r["dte"] <= DTE_HI and np.isfinite(r["delta"])] exps = sorted({round(r["dte"], 4) for r in both})[:2] for e in exps: g = [r for r in both if round(r["dte"], 4) == e] for d in DEPTH_DELTAS: if not g: continue r = min(g, key=lambda x: abs(x["delta"] - d)) out += [r["name"], r["twin"]] return sorted(set(out)) def twin_name(name: str) -> str: a = name.split("-") a[0] = a[0].replace("_USDC", "") return "-".join(a) 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 now_ms = datetime.fromisoformat(raw["ts"]).timestamp() * 1000.0 if raw.get("ts") \ else datetime.now(UTC).timestamp() * 1000.0 rows = [] for name, i in inst.items(): s = summ.get(name) if s is None: continue fam = name.split("-")[0] inv = "_" not in fam # BTC / ETH = inverse; *_USDC = lineare 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 F = float(s.get("underlying_price") or 0.0) # e' il FORWARD della scadenza iv = float(s.get("mark_iv") or 0.0) K = float(i.get("strike") or 0.0) T = max(dte, 0.0) / 365.25 typ = "P" if i.get("option_type") == "put" else "C" mark = float(s.get("mark_price") or 0.0) tw = twin_name(name) if not inv else None rows.append(dict( name=name, fam=fam, inv=inv, typ=typ, K=K, dte=dte, F=F, iv=iv, T=T, delta=(put_delta(F, K, T, iv / 100.0) if (F and iv and K and T > 0 and typ == "P") else float("nan")), bid=bid, ask=ask, mark=mark, 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), tick0=float(i.get("tick_size") or 0.0), steps=i.get("tick_size_steps") or [], minamt=float(i.get("min_trade_amount") or 0.0), blockmin=float(i.get("block_trade_min_trade_amount") or 0.0), twin=tw, )) return rows # ------------------------------------------------------------------ unita' (USD per unita' di sott.) def u(price: float | None, r: dict) -> float: """Prezzo in USD per UNITA' di sottostante. Lineare: gia' in USDC/unita'. Inverse: in cripto per contratto (contract_size 1 = 1 unita') -> x forward.""" if price is None: return float("nan") return price * (r["F"] if r["inv"] else 1.0) def lot_usd(price: float | None, r: dict) -> float: """Prezzo di UN LOTTO MINIMO in dollari.""" return u(price, r) * r["minamt"] def tick_usd(r: dict, price_native: float) -> float: return eff_tick(price_native, r["tick0"], r["steps"]) * (r["F"] if r["inv"] else 1.0) def relspread(r: dict) -> float: if r["bid"] is None or r["ask"] is None: return float("nan") mid = 0.5 * (r["bid"] + r["ask"]) return (r["ask"] - r["bid"]) / mid if mid > 0 else float("nan") def fee_lot(r: dict, price_native: float) -> float: return min(TAKER * r["F"] * r["minamt"], TAKER_CAP * max(lot_usd(price_native, r), 0.0)) def med(v) -> float: v = [x for x in v if x is not None and np.isfinite(x)] return float(np.median(v)) if v else float("nan") # ================================================================== §0 repliche def sez0_repliche(rows: list[dict], att: dict) -> None: print("\n" + "=" * 104) print("0. REPLICA DEI TRE FATTI GIA' PUBBLICATI — prima di produrre un numero nuovo") print("=" * 104) # --- A1: il conteggio con i filtri del collettore vivo def conta(fams, oi_min): return sum(1 for r in rows if r["fam"] in fams and r["dte"] <= DTE_MAX_COLL and r["dte"] > 0 and r["oi"] >= oi_min) usdc_n = conta(("BTC_USDC", "ETH_USDC"), OI_MIN) inv_n = conta(("BTC", "ETH"), OI_MIN) usdc_raw = conta(("BTC_USDC", "ETH_USDC"), 0.0) inv_raw = conta(("BTC", "ETH"), 0.0) inc = usdc_n / inv_n * 100 if inv_n else float("nan") print(f" A1 §51 filtri del collettore (<=95g, OI>={OI_MIN:.0f}):") print(f" USDC BTC+ETH = {usdc_n:4d} (pubblicato 113) inverse = {inv_n:4d} " f"(pubblicato 649) incremento {inc:+.1f}% (pubblicato +17,4%)") print(f" senza il filtro OI: USDC {usdc_raw}, inverse {inv_raw} -> " f"+{usdc_raw/max(inv_raw,1)*100:.0f}% " f"(e' l'origine del '+90%' corretto in §51: il filtro taglia il " f"{100*(1-usdc_n/max(usdc_raw,1)):.0f}% della famiglia USDC)") ok1 = abs(usdc_n - 113) <= 0.15 * 113 and abs(inv_n - 649) <= 0.15 * 649 att["A1"] = ("CONFERMATA" if ok1 else "SCOSTAMENTO >15%") # --- A2: la correlazione di rango OI vs quotati (le sei famiglie lineari) stat = {} for f in LINEAR6: 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] oi = sum(1 for r in rows if r["fam"] == f and r["dte"] <= DTE_MAX_COLL and r["dte"] > 0 and r["oi"] >= OI_MIN) stat[f] = dict(oi=oi, 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"]))} rho = float(np.corrcoef([rA[f] for f in LINEAR6], [rB[f] for f in LINEAR6])[0, 1]) print(f"\n A2 §14 rho di rango (OI>=100) vs (% put quotati a due lati), 6 famiglie lineari:") print(f" {rho:+.2f} (pubblicato -0,77)") print(f" {'famiglia':<10} {'OI>=100':>8} {'rango':>6} {'put 4-45g':>10} " f"{'2 lati':>7} {'%':>6} {'rango':>6}") for f in sorted(LINEAR6, key=lambda x: -stat[x]["oi"]): s = stat[f] print(f" {f:<10} {s['oi']:>8} {rA[f]:>6} {s['puts']:>10} {s['two']:>7} " f"{s['pct']:>5.0%} {rB[f]:>6}") att["A2"] = "CONFERMATA" if (rho < -0.4) else "REFUTATA" # --- A3: il tick print(f"\n A3 §46 il tick, e cosa vale davvero:") print(f" {'famiglia':<10} {'n put':>6} {'tick base':>10} {'tick_size_steps':>34} " f"{'tick base in $/unita':>21}") tickfacts = {} for f in ("BTC_USDC", "BTC", "ETH_USDC", "ETH"): sub = [r for r in rows if r["fam"] == f and r["typ"] == "P"] if not sub: continue ticks = {r["tick0"] for r in sub} F = med([r["F"] for r in sub]) t0 = sorted(ticks)[0] steps = sub[0]["steps"] tusd = t0 * (F if sub[0]["inv"] else 1.0) tickfacts[f] = tusd st = "; ".join(f"{s['tick_size']:g} sopra {s['above_price']:g}" for s in steps) or "-" uni = "USDC" if not sub[0]["inv"] else f.split("_")[0] print(f" {f:<10} {len(sub):>6} {t0:>10g} {st:>34} {tusd:>18.3f} $" f" [{uni}/unita']") if f == "BTC_USDC": print(f" {'':<10} tick unico su TUTTE le put BTC_USDC? " f"{'SI (5.0)' if ticks == {5.0} else f'NO: {sorted(ticks)}'}") print(f"\n 📌 il pavimento in DOLLARI, che e' il confronto che nessuno aveva fatto:") for lin, inv in PAIRS: if lin in tickfacts and inv in tickfacts: r = tickfacts[inv] / tickfacts[lin] print(f" {inv:<4} 1 tick = ${tickfacts[inv]:.3f}/unita' contro " f"{lin:<9} ${tickfacts[lin]:.3f}/unita' -> l'INVERSE e' {r:.2f}x " f"{'PIU GROSSO' if r > 1 else 'piu fine'}") att["A3_tick5"] = "CONFERMATO su BTC_USDC" att["A3_pavimento"] = ("l'inverse e' PEGGIO" if all(tickfacts.get(i, 0) > tickfacts.get(l, 9e9) for l, i in PAIRS) else "il lineare e' peggio") # ================================================================== §1 censimento def sez1_censimento(rows: list[dict]) -> None: print("\n" + "=" * 104) print("1. CENSIMENTO VIVO — le due famiglie di BTC/ETH, i fatti del venue") print("=" * 104) print(f"{'famiglia':<10} {'opz':>5} {'put':>5} {'put 4-45g':>10} {'2 lati':>7} {'%':>5} " f"{'spr% med':>9} {'OI>=100':>8} {'min_amt':>8} {'$/lotto noz':>12} {'blocco $':>11}") for lin, inv in PAIRS: for f in (lin, inv): sub = [r for r in rows if r["fam"] == f] puts = [r for r in sub if r["typ"] == "P"] win = [r for r in puts if DTE_LO <= r["dte"] <= DTE_HI] two = [r for r in win if r["bid"] is not None and r["ask"] is not None] F = med([r["F"] for r in sub]) a = sub[0] oi = sum(1 for r in sub if r["dte"] <= DTE_MAX_COLL and r["dte"] > 0 and r["oi"] >= OI_MIN) print(f"{f:<10} {len(sub):>5} {len(puts):>5} {len(win):>10} {len(two):>7} " f"{(len(two)/len(win) if win else float('nan')):>4.0%} " f"{med([relspread(r) for r in two])*100:>8.1f}% {oi:>8} {a['minamt']:>8g} " f"{a['minamt']*F:>12,.0f} {a['blockmin']*F:>11,.0f}") lin_all = [r for r in rows if r["fam"] in ("BTC_USDC", "ETH_USDC")] inv_all = [r for r in rows if r["fam"] in ("BTC", "ETH")] names_inv = {r["name"] for r in inv_all} gemelli = sum(1 for r in lin_all if r["twin"] in names_inv) print(f"\n 📌 SOVRAPPOSIZIONE DELLE GRIGLIE: {gemelli}/{len(lin_all)} strumenti lineari " f"({gemelli/max(len(lin_all),1):.1%}) hanno un GEMELLO inverse a stessa scadenza e") print(f" stesso strike; l'inverse ne ha {len(inv_all)-gemelli} che la lineare non ha " f"({(len(inv_all)-gemelli)/max(len(inv_all),1):.0%} della sua griglia).") print(" La famiglia lineare e' un SOTTOINSIEME della griglia inverse -> il confronto" " appaiato e' possibile su tutto, e la lineare offre MENO strike, non altri.") # ================================================================== §2 il confronto appaiato def coppie(rows: list[dict]) -> list[tuple[dict, dict]]: idx = {r["name"]: r for r in rows} out = [] for r in rows: if r["inv"] or r["fam"] not in ("BTC_USDC", "ETH_USDC"): continue t = idx.get(r["twin"] or "") if t is not None: out.append((r, t)) return out def sez2_appaiato(rows: list[dict], att: dict) -> dict: print("\n" + "=" * 104) print("2. IL CONFRONTO APPAIATO — stessa scadenza, stesso strike, stesso tipo") print("=" * 104) print(" La statistica e' la MEDIANA DELLE DIFFERENZE APPAIATE (convenzione del progetto),") print(" mai la differenza delle mediane: confrontare i mediani di due insiemi di strike") print(" diversi misura la griglia, non la famiglia.") cp = coppie(rows) res = {} print(f"\n{'coppia':<12} {'n coppie':>8} {'entrambe 2 lati':>16} {'ΔIV appaiata (pt vol)':>22} " f"{'mezzo-spread IV*':>17} {'Δ spr% appaiata':>16}") for lin, inv in PAIRS: sub = [(a, b) for a, b in cp if a["fam"] == lin and DTE_LO <= a["dte"] <= DTE_HI and a["typ"] == "P"] both = [(a, b) for a, b in sub if a["bid"] and a["ask"] and b["bid"] and b["ask"] and a["iv"] > 0 and b["iv"] > 0] div = [a["iv"] - b["iv"] for a, b in both] dsp = [relspread(a) - relspread(b) for a, b in both] # mezzo-spread espresso in punti di vol: (mezzo spread in $) / vega, approssimato # con la derivata numerica del prezzo BS rispetto a sigma su 1 punto di vol. halfiv = [] for a, _ in both: v = (put_price(a["F"], a["K"], a["T"], (a["iv"] + 1.0) / 100.0) - put_price(a["F"], a["K"], a["T"], (a["iv"] - 1.0) / 100.0)) / 2.0 hs = 0.5 * (u(a["ask"], a) - u(a["bid"], a)) if v > 1e-9: halfiv.append(hs / v) res[lin] = dict(n=len(sub), both=len(both), div=med(div), dsp=med(dsp), halfiv=med(halfiv), pos_div=(np.mean([x > 0 for x in div]) if div else float("nan")), pos_dsp=(np.mean([x > 0 for x in dsp]) if dsp else float("nan"))) print(f"{lin:<12} {len(sub):>8} {len(both):>16} {med(div):>+21.3f} " f"{med(halfiv):>16.3f} {med(dsp)*100:>+15.2f}pp") print("\n * `mezzo-spread IV` = quanti punti di vol vale mezzo bid-ask della gamba LINEARE.") print(" E' il metro giusto per ΔIV: una differenza di prezzo piu' piccola del mezzo") print(" spread non e' una differenza di prezzo, e' rumore di quotazione.") for lin, _ in PAIRS: r = res[lin] ratio = abs(r["div"]) / r["halfiv"] if r["halfiv"] and np.isfinite(r["halfiv"]) else np.nan print(f" {lin:<10} |ΔIV| / mezzo-spread = {ratio:.2f} " f"(>1 = le due superfici prezzano DAVVERO diverso; <1 = dentro il rumore)") print(f" {'':<10} ΔIV > 0 nel {r['pos_div']:.0%} delle coppie, " f"Δspread > 0 nel {r['pos_dsp']:.0%}") att["A4"] = ("CONFERMATA (lineare piu' larga)" if all(res[l]["dsp"] > 0 for l, _ in PAIRS) else "REFUTATA (lineare NON piu' larga)" if all(res[l]["dsp"] < 0 for l, _ in PAIRS) else "MISTA fra i due sottostanti") att["A5"] = ("CONFERMATA ma PER COSTRUZIONE: il test che avevo dichiarato (ΔIV al mark) " "non ha potenza (§2b, una sola superficie di mark al venue); sostituito dalla " "misura sulle QUOTE, che lo conferma") # --- 2b: la IV al MARK e' la stessa PER CONVENZIONE DEL VENUE -> test (i) senza potenza print("\n --- 2b LA IV AL MARK NON E' UNA MISURA: quanto spesso e' LO STESSO NUMERO?") print(f" {'coppia':<12} {'coppie con mark_iv':>19} {'bit-identiche':>14} {'%':>6} " f"{'max |ΔIV| non nulla':>21}") tot_id, tot_n = 0, 0 for lin, _ in PAIRS: sub = [(a, b) for a, b in cp if a["fam"] == lin and a["iv"] > 0 and b["iv"] > 0] eq = [1 for a, b in sub if abs(a["iv"] - b["iv"]) < 1e-12] nz = [abs(a["iv"] - b["iv"]) for a, b in sub if abs(a["iv"] - b["iv"]) >= 1e-12] tot_id += len(eq); tot_n += len(sub) print(f" {lin:<12} {len(sub):>19} {len(eq):>14} {len(eq)/max(len(sub),1):>5.1%} " f"{(max(nz) if nz else 0.0):>21.3f}") frac = tot_id / max(tot_n, 1) print(f"\n 🚨 {frac:.1%} delle coppie ha la MEDESIMA mark_iv, e le differenze residue valgono") print(" 0,01-0,02 punti di vol = l'arrotondamento a due decimali del campo. **Deribit marca") print(" le due famiglie sulla STESSA superficie.** Conseguenza metodologica, e non e' un") print(" dettaglio: il test (i) sul MARK, cosi' come l'avevo scritto in A5, **non ha") print(" potenza** — non misura il mercato, misura la convenzione di marcatura del venue.") print(" L'unica cosa DAVVERO indipendente fra le due famiglie sono le QUOTE (book separati).") # --- 2c: le quote, che sono l'unica cosa indipendente print("\n --- 2c LE QUOTE ESEGUIBILI, appaiate (in % del mark: unita' comune fra famiglie)") print(f" {'coppia':<12} {'n coppie 2-lati':>16} {'Δbid':>9} {'Δask':>9} " f"{'mezzo-spread lin':>17} {'mezzo-spread inv':>17} {'Δ appaiata':>11}") for lin, _ in PAIRS: both = [(a, b) for a, b in cp if a["fam"] == lin and a["typ"] == "P" and DTE_LO <= a["dte"] <= DTE_HI and a["bid"] and a["ask"] and b["bid"] and b["ask"] and a["mark"] > 0 and b["mark"] > 0] db = [(u(a["bid"], a) - u(b["bid"], b)) / u(a["mark"], a) for a, b in both] da = [(u(a["ask"], a) - u(b["ask"], b)) / u(a["mark"], a) for a, b in both] hl = [(u(a["ask"], a) - u(a["bid"], a)) / (2 * u(a["mark"], a)) for a, b in both] hi_ = [(u(b["ask"], b) - u(b["bid"], b)) / (2 * u(b["mark"], b)) for a, b in both] dh = [x - y for x, y in zip(hl, hi_)] res[lin]["dhalf"] = med(dh) res[lin]["nboth"] = len(both) res[lin]["hl"] = med(hl) res[lin]["hi"] = med(hi_) print(f" {lin:<12} {len(both):>16} {med(db):>+8.2%} {med(da):>+8.2%} " f"{med(hl):>16.2%} {med(hi_):>16.2%} {med(dh):>+10.2%}") print(" `Δbid`/`Δask` = quanto la quota lineare sta sopra/sotto la gemella inverse, in") print(" percentuale del premio equo. `mezzo-spread` = quanto costa attraversare, per gamba.") print(" La colonna che conta per il f di VRP01 e' l'ULTIMA: e' su ~130 gambe appaiate, non") print(" sulle 3 strutture di §7(ii).") # --- 2d: dove UNA sola delle due famiglie e' quotata. Il conto puo' toccare solo la # lineare, quindi uno strike quotato solo sull'inverse per lui NON ESISTE. print("\n --- 2d COPERTURA: quale famiglia e' quotata dove l'altra non lo e' (put 4-45g)") print(f" {'coppia':<12} {'coppie':>7} {'entrambe':>9} {'solo LINEARE':>13} " f"{'solo inverse':>13} {'nessuna':>8}") for lin, _ in PAIRS: sub = [(a, b) for a, b in cp if a["fam"] == lin and a["typ"] == "P" and DTE_LO <= a["dte"] <= DTE_HI] def due(r): return r["bid"] is not None and r["ask"] is not None bb = sum(1 for a, b in sub if due(a) and due(b)) sl = sum(1 for a, b in sub if due(a) and not due(b)) si = sum(1 for a, b in sub if due(b) and not due(a)) nn = sum(1 for a, b in sub if not due(a) and not due(b)) print(f" {lin:<12} {len(sub):>7} {bb:>9} {sl:>13} {si:>13} {nn:>8}") print(" Uno strike quotato SOLO sull'inverse, per un conto in USDC, semplicemente non") print(" esiste: e' la forma in cui la scelta di famiglia toglie strumenti invece di darli.") # --- C1 controllo positivo: strike adiacente print("\n --- C1 CONTROLLO POSITIVO: lo stesso confronto con l'inverse allo strike ADIACENTE") idx = {r["name"]: r for r in rows} print(f" {'coppia':<12} {'|ΔIV| stesso strike':>20} {'|ΔIV| strike adiacente':>23} " f"{'rapporto':>9} {'esito':>10}") for lin, inv in PAIRS: base, shift = [], [] byexp: dict = {} for r in rows: if r["fam"] == inv and r["typ"] == "P": byexp.setdefault(round(r["dte"], 4), []).append(r) for k in byexp: byexp[k] = sorted(byexp[k], key=lambda x: x["K"]) for a, b in cp: if a["fam"] != lin or a["typ"] != "P" or not (DTE_LO <= a["dte"] <= DTE_HI): continue if not (a["iv"] > 0 and b["iv"] > 0): continue base.append(abs(a["iv"] - b["iv"])) g = byexp.get(round(b["dte"], 4), []) j = next((i for i, x in enumerate(g) if x["name"] == b["name"]), None) if j is not None and j + 1 < len(g) and g[j + 1]["iv"] > 0: shift.append(abs(a["iv"] - g[j + 1]["iv"])) mb, ms = med(base), med(shift) ok = ms > 2.0 * mb print(f" {lin:<12} {mb:>20.3f} {ms:>23.3f} {ms/max(mb,1e-9):>9.1f}x " f"{'POTENZA OK' if ok else '⚠️ SENZA POTENZA':>10}") print(" ⚠️ Il rapporto e' enorme perche' il denominatore e' ESATTAMENTE zero (vedi 2b):") print(" il controllo passa, ma su una grandezza degenere. Percio' lo stesso controllo va") print(" rifatto sulla grandezza NON degenere, il mid eseguibile:") print(f" {'coppia':<12} {'|Δmid| stesso strike':>21} {'|Δmid| adiacente':>18} " f"{'rapporto':>9} {'esito':>16}") for lin, inv in PAIRS: byexp: dict = {} for r in rows: if r["fam"] == inv and r["typ"] == "P" and r["bid"] and r["ask"]: byexp.setdefault(round(r["dte"], 4), []).append(r) for k in byexp: byexp[k] = sorted(byexp[k], key=lambda x: x["K"]) base, shift = [], [] for a, b in cp: if a["fam"] != lin or a["typ"] != "P" or not (DTE_LO <= a["dte"] <= DTE_HI): continue if not (a["bid"] and a["ask"] and b["bid"] and b["ask"] and a["mark"] > 0): continue ma = 0.5 * (u(a["bid"], a) + u(a["ask"], a)) mb = 0.5 * (u(b["bid"], b) + u(b["ask"], b)) base.append(abs(ma - mb) / u(a["mark"], a)) g = byexp.get(round(b["dte"], 4), []) j = next((i for i, x in enumerate(g) if x["name"] == b["name"]), None) if j is not None and j + 1 < len(g): c = g[j + 1] shift.append(abs(ma - 0.5 * (u(c["bid"], c) + u(c["ask"], c))) / u(a["mark"], a)) r0, r1 = med(base), med(shift) print(f" {lin:<12} {r0:>20.2%} {r1:>17.2%} {r1/max(r0,1e-12):>9.1f}x " f"{('POTENZA OK' if r1 > 2*r0 else '⚠️ SENZA POTENZA'):>16}") print(" Se lo strike sbagliato NON alzasse la distanza, il confronto non distinguerebbe") print(" due superfici diverse e ogni 'coincidono' sarebbe vuoto.") return res # ================================================================== §3 il pavimento del tick DBUCK = ((0.00, 0.02), (0.02, 0.05), (0.05, 0.10), (0.10, 0.20), (0.20, 0.35), (0.35, 0.65)) TBUCK = ((0.0, 7.0), (7.0, 30.0), (30.0, 95.0)) def sez3_tick(rows: list[dict], att: dict) -> dict: print("\n" + "=" * 104) print("3. IL PAVIMENTO DEL TICK — `tick effettivo / premio equo`, per delta e per scadenza") print("=" * 104) print(" E' il numero TRASFERIBILE del filone: non dipende dal capitale, non dipende dal") print(" lotto, non dipende dal giorno. `>1` significa che il premio equo sta SOTTO un tick,") print(" cioe' che quell'opzione non e' quotabile al suo valore: il prezzo minimo possibile") print(" e' gia' un multiplo del giusto. Il rapporto E' quel multiplo.") out: dict = {} for lin, inv in PAIRS: print(f"\n --- {lin} contro {inv} (mediana del rapporto; 'n' = strumenti nel riquadro)") print(f" {'|delta|':<12} " + " ".join(f"{f'{a:.0f}-{b:.0f}g':>18}" for a, b in TBUCK)) for dlo, dhi in DBUCK: cells = [] for tlo, thi in TBUCK: vals = {} for f in (lin, inv): sub = [r for r in rows if r["fam"] == f and r["typ"] == "P" and np.isfinite(r["delta"]) and dlo <= abs(r["delta"]) < dhi and tlo <= r["dte"] < thi and r["mark"] > 0] vals[f] = med([eff_tick(r["mark"], r["tick0"], r["steps"]) / r["mark"] for r in sub]) out[(f, dlo, tlo)] = (vals[f], len(sub)) cells.append(f"{vals[lin]:>7.2f} /{vals[inv]:>7.2f}") print(f" {f'{dlo:.2f}-{dhi:.2f}':<12} " + " ".join(f"{c:>18}" for c in cells)) print(f" {'':<12} " + " ".join(f"{'lineare / inverse':>18}" for _ in TBUCK)) # C2: controllo positivo del rilevatore atm = [v for (f, d, t), (v, n) in out.items() if d == 0.35 and np.isfinite(v)] w05 = [v for (f, d, t), (v, n) in out.items() if d == 0.02 and np.isfinite(v)] w02 = [v for (f, d, t), (v, n) in out.items() if d == 0.00 and np.isfinite(v)] print(f"\n --- C2 CONTROLLO POSITIVO del rilevatore") print(f" soglia DICHIARATA A PRIORI: ala > 20x ATM.") print(f" ATM (|d| 0.35-0.65) mediana {med(atm):.4f} · |d| 0.02-0.05 {med(w05):.3f} " f"= {med(w05)/max(med(atm),1e-12):.1f}x -> " f"{'PASSA' if med(w05) > 20*med(atm) else 'NON PASSA la soglia a priori'}") print(f" ⚠️ ERRORE MIO, non del rilevatore: la griglia di delta che avevo dichiarato") print(f" NON CONTENEVA l'ala di cui parla §46. Una put marcata 0,82 USDC su un") print(f" sottostante a 77k non sta a |delta| 0.02-0.05, sta SOTTO 0.02. Bucket") print(f" aggiunto: |d| 0.00-0.02 mediana {med(w02):.3f} = " f"{med(w02)/max(med(atm),1e-12):.0f}x ATM -> " f"{'SEPARA' if med(w02) > 20*med(atm) else 'NON SEPARA'}") worst = med([v for (f, d, t), (v, n) in out.items() if d == 0.00 and t == 0.0 and np.isfinite(v)]) print(f" ⚠️ E la soglia che avevo dichiarato era mal posta anche come STATISTICA: una") print(f" mediana di bucket mescola 0-7g e 30-95g. Sulla cella dove il fenomeno vive") print(f" (|d|<0.02 E 0-7g) il rapporto e' {worst:.2f} = {worst/max(med(atm),1e-12):.0f}x ATM.") print(f" Riporto la soglia come dichiarata (NON superata) e la correzione accanto.") mono = all(med([v for (f, d, t), (v, n) in out.items() if d == a and np.isfinite(v)]) >= med([v for (f, d, t), (v, n) in out.items() if d == b and np.isfinite(v)]) for a, b in zip([x[0] for x in DBUCK], [x[0] for x in DBUCK][1:])) print(f" monotonia sui {len(DBUCK)} bucket di delta (il rapporto deve solo CRESCERE") print(f" andando verso l'ala): {'SI' if mono else 'NO'}") print(" Un rilevatore che desse lo stesso numero ATM e sull'ala sarebbe rotto, non muto.") # il numero di §46, ricalcolato in modo appaiato print("\n --- il '6x' di §46, rifatto sulle DUE famiglie invece che su una sola") for lin, inv in PAIRS: for f in (lin, inv): sub = [r for r in rows if r["fam"] == f and r["typ"] == "P" and r["mark"] > 0 and np.isfinite(r["delta"]) and abs(r["delta"]) < 0.05 and r["dte"] <= 30] if not sub: continue rat = [eff_tick(r["mark"], r["tick0"], r["steps"]) / r["mark"] for r in sub] nq = sum(1 for x in rat if x > 1.0) print(f" {f:<10} ali |d|<0.05, <=30g: n={len(sub):>4} " f"tick/premio mediano {med(rat):>6.2f}x " f"non quotabili al giusto: {nq}/{len(sub)} ({nq/len(sub):.0%})") att["A3_6x"] = "ala |d|<0.05: mediana misurata (vedi tabella)" # --- la versione APPAIATA del pavimento: la tabella qui sopra confronta POPOLAZIONI # diverse (l'inverse ha 624 strike che la lineare non ha) e in una cella si ribalta. print("\n --- LO STESSO PAVIMENTO, APPAIATO (stesso strumento nelle due famiglie)") print(" La tabella qui sopra confronta POPOLAZIONI diverse: l'inverse ha 624 strike che") print(" la lineare non ha, e in una cella (BTC |d|<0.02, 0-7g) il segno si ROVESCIA.") print(" Appaiato non puo' succedere: il numeratore e' un fatto del venue e il") print(" denominatore e' lo stesso premio (una sola superficie di mark, §2b).") idx = {r["name"]: r for r in rows} print(f" {'coppia':<12} {'n appaiate':>11} {'rapporto lin/inv':>17} " f"{'atteso da tick $':>17} {'lin quotabile & inv NO':>24} {'il contrario':>13}") for lin, inv in PAIRS: rr, only_lin, only_inv = [], 0, 0 for r in rows: if r["fam"] != lin or r["typ"] != "P" or r["mark"] <= 0: continue t = idx.get(r["twin"] or "") if t is None or t["mark"] <= 0: continue a = eff_tick(r["mark"], r["tick0"], r["steps"]) / r["mark"] b = eff_tick(t["mark"], t["tick0"], t["steps"]) / t["mark"] rr.append(a / b) only_lin += int(a < 1.0 <= b) only_inv += int(b < 1.0 <= a) sub = [r for r in rows if r["fam"] == lin and r["typ"] == "P"] sui = [r for r in rows if r["fam"] == inv and r["typ"] == "P"] F = med([r["F"] for r in sui]) atteso = (sub[0]["tick0"]) / (sui[0]["tick0"] * F) print(f" {lin:<12} {len(rr):>11} {med(rr):>17.3f} {atteso:>17.3f} " f"{only_lin:>24} {only_inv:>13}") print(" `atteso da tick $` = (tick lineare in $) / (tick inverse in $): se il rapporto") print(" misurato coincide, il pavimento e' esattamente il tick e nient'altro.") # --- cosa costa DAVVERO comprare l'ala: il pavimento si paga sull'ASK, non sul mark print("\n --- IL PAVIMENTO SI PAGA SULL'ASK: quanto si paga sopra il premio equo, per") print(" COMPRARE (lato long, l'unico che il progetto puo' guardare)") print(f" {'|delta|':<12} " + " ".join(f"{f:>18}" for f in ("BTC_USDC", "BTC", "ETH_USDC", "ETH"))) for dlo, dhi in DBUCK: cells = [] for f in ("BTC_USDC", "BTC", "ETH_USDC", "ETH"): sub = [r for r in rows if r["fam"] == f and r["typ"] == "P" and r["ask"] and r["mark"] > 0 and np.isfinite(r["delta"]) and dlo <= abs(r["delta"]) < dhi and r["dte"] <= 45.0] v = med([r["ask"] / r["mark"] for r in sub]) cells.append(f"{v:>10.2f}x n={len(sub):<4}") print(f" {f'{dlo:.2f}-{dhi:.2f}':<12} " + " ".join(f"{c:>18}" for c in cells)) print(" `x` = ask / premio equo del venue. E' il sovrapprezzo che si paga per") print(" comprare quell'ala, e sull'ala profonda NON e' negoziabile: e' il tick.") print("\n ⚠️ anche questa tabella confronta POPOLAZIONI diverse. La versione appaiata") print(" (stesso strumento, solo dove ENTRAMBE hanno un ask):") print(f" {'|delta|':<12} " + " ".join(f"{f'{l} / {i}':>26}" for l, i in PAIRS)) for dlo, dhi in DBUCK: cells = [] for lin, inv in PAIRS: rr = [] for r in rows: if (r["fam"] != lin or r["typ"] != "P" or not r["ask"] or r["mark"] <= 0 or not np.isfinite(r["delta"]) or not (dlo <= abs(r["delta"]) < dhi) or r["dte"] > 45.0): continue t = idx.get(r["twin"] or "") if t is None or not t["ask"] or t["mark"] <= 0: continue rr.append((r["ask"] / r["mark"]) / (t["ask"] / t["mark"])) cells.append(f"{med(rr):>8.3f} n={len(rr):<4}") print(f" {f'{dlo:.2f}-{dhi:.2f}':<12} " + " ".join(f"{c:>26}" for c in cells)) print(" <1 = la LINEARE si compra piu' vicino al giusto; >1 = la inverse.") # --- il premio minimo quotabile, per lotto: il numero che nessun capitale aggira print("\n --- IL PREMIO MINIMO QUOTABILE PER UN LOTTO (1 tick x lotto minimo)") print(f" {'famiglia':<10} {'tick base':>11} {'lotto min':>10} {'premio min $/lotto':>19} " f"{'noz. lotto $':>13} {'premio min / nozionale':>23}") for lin, inv in PAIRS: for f in (lin, inv): sub = [r for r in rows if r["fam"] == f and r["typ"] == "P"] if not sub: continue a = sub[0] F = med([r["F"] for r in sub]) pmin = a["tick0"] * a["minamt"] * (F if a["inv"] else 1.0) noz = a["minamt"] * F print(f" {f:<10} {a['tick0']:>11g} {a['minamt']:>10g} {pmin:>19.3f} " f"{noz:>13,.0f} {pmin/noz:>22.5%}") print(" A sinistra c'e' la granularita' (quanto piccolo puo' essere un biglietto), a") print(" destra il pavimento in proporzione: e' il secondo che decide se un'ala e'") print(" comprabile al suo valore, ed e' INDIPENDENTE dal capitale.") return out # ================================================================== §4 le strutture def scegli_put(rows: list[dict], fam: str, target: float, lato: str, dte_lo: float, dte_hi: float) -> list[dict]: """Per ogni scadenza, la put col delta piu' vicino al target FRA QUELLE CHE HANNO IL LATO CHE SERVE (bid per vendere, ask per comprare). Un book a un lato solo non e' un book vuoto: e' un book che serve una direzione sola, e la restrizione giusta e' asimmetrica.""" g = [r for r in rows if r["fam"] == fam and r["typ"] == "P" and np.isfinite(r["delta"]) and dte_lo <= r["dte"] <= dte_hi and ((r["bid"] is not None) if lato == "vendi" else (r["ask"] is not None))] out = [] for e in sorted({round(r["dte"], 4) for r in g}): gg = [r for r in g if round(r["dte"], 4) == e] out.append(min(gg, key=lambda r: abs(r["delta"] - target))) return out def sez4_strutture(rows: list[dict], att: dict) -> None: print("\n" + "=" * 104) print(f"4. LA DOMANDA CHE DECIDE — cosa entra in un conto da ${CAPITAL:.0f}, e in quale famiglia") print("=" * 104) print(" Tre strutture a rischio definito, per UN LOTTO MINIMO, ai prezzi ESEGUIBILI") print(" (si compra all'ask e si vende al bid, mai al mid: §28).") print(" L1 LONG put d-0,05 ... l'assicurazione di coda di §46") print(" L2 LONG debit spread compra d-0,28 / vende d-0,10 ... rischio definito, lato long") print(" S1 SHORT credit spread vende d-0,28 / compra d-0,10 ... la geometria di VRP01") print(f"\n{'struttura':<4} {'famiglia':<10} {'scad':>4} {'ticket $':>9} {'max perdita $':>13} " f"{'eseg/equo':>10} {'fee/ticket':>10} {'n a 20% conto':>13} {'entra a $635?':>13}") tab: dict = {} for lin, inv in PAIRS: for f in (lin, inv): # L1 -------------------------------------------------------------- long put di coda gs = scegli_put(rows, f, TAIL_D, "compra", 5.0, 35.0) if gs: tick_ = [lot_usd(r["ask"], r) for r in gs] equo = [lot_usd(r["mark"], r) for r in gs] fee = [fee_lot(r, r["ask"]) for r in gs] t, e_ = med(tick_), med(equo) n = math.floor(RISK_BUDGET * CAPITAL / t) if t > 0 else 0 tab[("L1", f)] = t print(f"{'L1':<4} {f:<10} {len(gs):>4} {t:>9.2f} {t:>13.2f} " f"{(t/e_ if e_ > 0 else float('nan')):>10.2f} " f"{med(fee)/max(t,1e-9):>9.0%} {n:>13} " f"{'SI' if t <= CAPITAL else 'NO':>13}") # L2 -------------------------------------------------------- long put debit spread hi = scegli_put(rows, f, SHORT_D, "compra", DTE_LO, DTE_HI) lo = scegli_put(rows, f, LONG_D, "vendi", DTE_LO, DTE_HI) lom = {round(r["dte"], 4): r for r in lo} cost, costm, fees = [], [], [] for a in hi: b = lom.get(round(a["dte"], 4)) if b is None or b["K"] >= a["K"]: continue c = lot_usd(a["ask"], a) - lot_usd(b["bid"], b) cm = lot_usd(a["mark"], a) - lot_usd(b["mark"], b) if cm <= 0: continue cost.append(c); costm.append(cm) fees.append(fee_lot(a, a["ask"]) + fee_lot(b, b["bid"])) if cost: t, e_ = med(cost), med(costm) n = math.floor(RISK_BUDGET * CAPITAL / t) if t > 0 else 0 tab[("L2", f)] = t print(f"{'L2':<4} {f:<10} {len(cost):>4} {t:>9.2f} {t:>13.2f} " f"{t/e_:>10.2f} {med(fees)/max(t,1e-9):>9.0%} {n:>13} " f"{'SI' if t <= CAPITAL else 'NO':>13}") # S1 ------------------------------------------------------- short put credit spread sh = scegli_put(rows, f, SHORT_D, "vendi", DTE_LO, DTE_HI) wg = scegli_put(rows, f, LONG_D, "compra", DTE_LO, DTE_HI) wgm = {round(r["dte"], 4): r for r in wg} ml, cred, credm, fees = [], [], [], [] for a in sh: b = wgm.get(round(a["dte"], 4)) if b is None or b["K"] >= a["K"]: continue c = lot_usd(a["bid"], a) - lot_usd(b["ask"], b) cm = lot_usd(a["mark"], a) - lot_usd(b["mark"], b) if cm <= 0: continue cred.append(c); credm.append(cm) ml.append((a["K"] - b["K"]) * a["minamt"] - c) fees.append(fee_lot(a, a["bid"]) + fee_lot(b, b["ask"])) if ml: m, e_ = med(ml), med(credm) n = math.floor(RISK_BUDGET * CAPITAL / m) if m > 0 else 0 tab[("S1", f)] = m print(f"{'S1':<4} {f:<10} {len(ml):>4} {med(cred):>9.2f} {m:>13.2f} " f"{med(cred)/e_:>10.2f} {med(fees)/max(abs(med(cred)),1e-9):>9.0%} {n:>13} " f"{'SI' if m <= CAPITAL else 'NO':>13}") print("\n `eseg/equo` = ticket eseguibile / ticket al mark del venue. Sul LONG e' >1 (paghi") print(" di piu' del giusto), sul credit e' <1 (incassi di meno): e' il f_venue di §14.") print(" `max perdita` per L1/L2 e' il ticket stesso (rischio definito per costruzione).") print(" ⚠️ S1 e' misurata per COMPLETEZZA: la regola del 19/06 (`niente short-vol da modello") print(" in deploy`) non e' una regola di eseguibilita' e non decade col lotto.") att["A6"] = ("CONFERMATA" if all(tab.get(("L1", f), 9e9) < CAPITAL for _, f in PAIRS) else "REFUTATA") print("\n --- LA DOMANDA DI §64: esiste sulla LINEARE una struttura che l'INVERSE non fa?") for lin, inv in PAIRS: for k, nome in (("L1", "long put di coda"), ("L2", "long debit spread"), ("S1", "short credit spread")): a, b = tab.get((k, lin)), tab.get((k, inv)) if a is None or b is None: print(f" {k} {nome:<22} {lin:<10} {'n/d' if a is None else f'{a:8.2f}'} " f" {inv:<4} {'n/d' if b is None else f'{b:8.2f}'}") continue v_lin = a <= RISK_BUDGET * CAPITAL v_inv = b <= RISK_BUDGET * CAPITAL marca = ("SOLO LINEARE" if (v_lin and not v_inv) else "entrambe" if (v_lin and v_inv) else "SOLO INVERSE" if v_inv else "nessuna delle due") print(f" {k} {nome:<22} {lin:<10} ${a:>8.2f} {inv:<4} ${b:>8.2f} " f"a budget {RISK_BUDGET:.0%}: {marca}") # ================================================================== §5 profondita' e controlli def sez5_profondita(rows: list[dict], raw: dict) -> None: print("\n" + "=" * 104) print("5. PROFONDITA' IN CIMA AL BOOK — sulle STESSE gambe nelle due famiglie") print("=" * 104) d = raw.get("depth") or {} if not d: print(" nessun order_book in questo snapshot (raccolto con --no-depth?).") return idx = {r["name"]: r for r in rows} print(f"{'strumento':<26} {'delta':>7} {'lato':<5} {'lotti al best':>13} " f"{'$ al best':>11} {'prezzo/tick':>11} {'|d venue-BS|':>12}") for n in sorted(d): r = idx.get(n) if r is None: continue ob = d[n] for lato, side in (("bid", "bids"), ("ask", "asks")): lv = ob.get(side) or [] if not lv: print(f"{n:<26} {r['delta']:>7.3f} {lato:<5} {'book vuoto su questo lato':>13}") continue px, sz = float(lv[0][0]), float(lv[0][1]) gv = (ob.get("greeks") or {}).get("delta") dd = abs(float(gv) - r["delta"]) if (gv is not None and np.isfinite(r["delta"])) else float("nan") print(f"{n:<26} {r['delta']:>7.3f} {lato:<5} " f"{sz/max(r['minamt'],1e-12):>13,.0f} {u(px, r)*sz:>11,.0f} " f"{px/max(eff_tick(px, r['tick0'], r['steps']),1e-12):>11.1f} {dd:>12.4f}") print("\n `lotti al best` = size in cima / lotto minimo: quante strutture minime passano") print(" senza muovere il prezzo. `prezzo/tick` e' il controllo d'artefatto del 30/07 (un") print(" prezzo a 1-2 tick e' una griglia, non un prezzo). `|d venue-BS|` valida il MIO") print(" delta contro le greche del venue: se non e' piccolo, tutta la selezione e' sbagliata.") def sez5b_controlli(rows: list[dict], att: dict) -> None: print("\n" + "-" * 104) print("5b. CONTROLLI C3 e C4") print("-" * 104) # C3 — il rilevatore di larghezza separa una famiglia nota-larga da una nota-stretta? print(f" {'famiglia':<10} {'put 4-45g':>10} {'2 lati':>7} {'%':>6} {'spr% mediano':>13} " f"{'best bid a 1 tick':>18}") for f in ("BTC_USDC", "SOL_USDC"): 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] onetick = [r for r in puts if r["bid"] is not None and r["bid"] <= 1.01 * eff_tick(r["bid"], r["tick0"], r["steps"])] print(f" {f:<10} {len(puts):>10} {len(two):>7} " f"{(len(two)/len(puts) if puts else float('nan')):>5.0%} " f"{med([relspread(r) for r in two])*100:>12.1f}% " f"{len(onetick)/max(len([r for r in puts if r['bid'] is not None]),1):>17.0%}") print(" C3: se BTC_USDC e SOL_USDC uscissero uguali, il rilevatore non separa niente.") # C4 — tipicita' del giorno, sulla grandezza gia' misurata su 19 settimane feriali print("\n C4 tipicita': attraversare lo spread sul credit spread canonico INVERSE costa") print(" ~10% del credito (misura del 30/07 su 19 settimane FERIALI); §14 ha letto") print(" 7,0-7,3% di sabato. Oggi e' domenica:") for _, inv in PAIRS: sh = scegli_put(rows, inv, SHORT_D, "vendi", DTE_LO, DTE_HI) wg = {round(r["dte"], 4): r for r in scegli_put(rows, inv, LONG_D, "compra", DTE_LO, DTE_HI)} fv = [] for a in sh: b = wg.get(round(a["dte"], 4)) if b is None or b["K"] >= a["K"]: continue cm = lot_usd(a["mark"], a) - lot_usd(b["mark"], b) ce = lot_usd(a["bid"], a) - lot_usd(b["ask"], b) if cm > 0: fv.append(1.0 - ce / cm) print(f" {inv:<4} inverse: costo di attraversamento = {med(fv)*100:5.1f}% del credito" f" (atteso ~7-10%)") ok = all(0.0 <= med([1.0 - (lot_usd(a["bid"], a) - lot_usd(b["ask"], b)) / (lot_usd(a["mark"], a) - lot_usd(b["mark"], b)) for a in scegli_put(rows, inv, SHORT_D, "vendi", DTE_LO, DTE_HI) for b in [{round(r["dte"], 4): r for r in scegli_put(rows, inv, LONG_D, "compra", DTE_LO, DTE_HI)} .get(round(a["dte"], 4))] if b is not None and b["K"] < a["K"] and (lot_usd(a["mark"], a) - lot_usd(b["mark"], b)) > 0]) <= 0.25 for _, inv in PAIRS) att["C4"] = "il giorno non e' anomalo sui book noti" if ok else "⚠️ giorno anomalo" print(" E' un controllo DEBOLE (una grandezza, un istante), non una prova di tipicita'.") def sez6_dispersione(snaps: list[dict]) -> None: print("\n" + "=" * 104) print(f"6. DISPERSIONE FRA SNAPSHOT — {len(snaps)} istantanee della stessa sessione") print("=" * 104) if len(snaps) < 2: print(" UNA SOLA ISTANTANEA: qui non c'e' dispersione da riportare, e va detto cosi'.") print(" Rilancia con --snap a distanza di minuti per avere la banda infra-sessione.") return print(f"{'ora UTC':<18} {'USDC OI>=100':>12} {'inv OI>=100':>12} " f"{'BTC_USDC 2lati%':>16} {'ETH_USDC 2lati%':>16} {'SOL_USDC 2lati%':>16} " f"{'ΔIV BTC':>9} {'ΔIV ETH':>9} {'Δspr BTC pp':>12} {'Δspr ETH pp':>12}") acc: dict = {k: [] for k in ("uo", "io", "bq", "eq", "sq", "biv", "eiv", "bsp", "esp")} for raw in snaps: rows = build(raw) cp = coppie(rows) def q(f): 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] return len(two) / len(puts) if puts else float("nan") def dd(lin): both = [(a, b) for a, b in cp if a["fam"] == lin and a["typ"] == "P" and DTE_LO <= a["dte"] <= DTE_HI and a["bid"] and a["ask"] and b["bid"] and b["ask"] and a["iv"] > 0 and b["iv"] > 0] return (med([a["iv"] - b["iv"] for a, b in both]), med([relspread(a) - relspread(b) for a, b in both])) uo = sum(1 for r in rows if r["fam"] in ("BTC_USDC", "ETH_USDC") and 0 < r["dte"] <= DTE_MAX_COLL and r["oi"] >= OI_MIN) io = sum(1 for r in rows if r["fam"] in ("BTC", "ETH") and 0 < r["dte"] <= DTE_MAX_COLL and r["oi"] >= OI_MIN) biv, bsp = dd("BTC_USDC") eiv, esp = dd("ETH_USDC") bq, eq, sq = q("BTC_USDC"), q("ETH_USDC"), q("SOL_USDC") for k, v in zip(acc, (uo, io, bq, eq, sq, biv, eiv, bsp, esp)): acc[k].append(v) print(f"{raw['ts'][11:19]:<18} {uo:>12} {io:>12} {bq:>15.0%} {eq:>15.0%} {sq:>15.0%} " f"{biv:>+9.3f} {eiv:>+9.3f} {bsp*100:>+11.2f} {esp*100:>+11.2f}") print("\n banda (min-max) sulle grandezze che portano il verdetto:") for k, nome in (("uo", "USDC OI>=100"), ("io", "inverse OI>=100"), ("bq", "BTC_USDC quotati 2 lati"), ("eq", "ETH_USDC quotati 2 lati"), ("sq", "SOL_USDC quotati 2 lati"), ("biv", "ΔIV appaiata BTC (pt vol)"), ("eiv", "ΔIV appaiata ETH (pt vol)"), ("bsp", "Δspread appaiato BTC"), ("esp", "Δspread appaiato ETH")): v = [x for x in acc[k] if np.isfinite(x)] if v: print(f" {nome:<28} min {min(v):+10.4f} mediana {np.median(v):+10.4f} " f"max {max(v):+10.4f}") print("\n ⚠️ E' dispersione INFRA-SESSIONE: dice se il numero balla fra un giro e l'altro,") print(" NON se un altro giorno della settimana darebbe un'altra risposta.") # ================================================================== §7 la decisione su §51 def sez7_decisione(rows: list[dict], res2: dict, att: dict) -> dict: print("\n" + "=" * 104) print("7. LA DECISIONE CHE DIPENDE DA QUESTO FILONE — raccogliere o no la catena USDC (§51)") print("=" * 104) print(" §51 raccomanda di RACCOGLIERLA (+117 chiamate/giro, +18%) con UNA giustificazione:") print(" *rende onesti i numeri di uno sleeve vivo (VRP01), perche' il f=0,73 e' misurato") print(" sulla famiglia inverse che un conto USDC non puo' marginare*. Quella") print(" giustificazione e' una PREVISIONE: che la superficie lineare prezzi diversamente.") print(" Qui la previsione si misura, appaiata.") print("\n Il f di VRP01 ha DUE pezzi, e vanno separati perche' si trasferiscono in modo") print(" diverso:") print(" (i) errore di MODELLO (il sleeve prezza a DVOL ATM: struttura a termine 42% +") print(" skew 25%, §5/§35). Vive nella IV -> si trasferisce se ΔIV appaiata ~ 0.") print(" (ii) costo di ATTRAVERSAMENTO (bid/ask). Vive nello spread -> si trasferisce solo") print(" se lo spread appaiato e' simile.") out = {} print("\n (i) SI TRASFERISCE PER COSTRUZIONE, e non perche' l'abbia misurato io: §2b mostra") print(" che il venue marca le due famiglie sulla STESSA superficie (mark_iv identica") print(" nel 95% delle coppie, il resto e' l'arrotondamento a 2 decimali). L'errore di") print(" modello del sleeve e' calcolato CONTRO quella superficie: e' lo stesso numero") print(" nelle due famiglie, e nessuna raccolta di dati puo' cambiarlo.") print(f" {'coppia':<10} {'ΔIV appaiata':>13} {'mezzo-spread IV':>16} {'rapporto':>9}" f" (test SENZA POTENZA, riportato per trasparenza)") for lin, _ in PAIRS: r = res2[lin] rr = abs(r["div"]) / r["halfiv"] if r["halfiv"] and np.isfinite(r["halfiv"]) else np.nan out[f"iv_{lin}"] = rr print(f" {lin:<10} {r['div']:>+13.3f} {r['halfiv']:>16.3f} {rr:>9.2f}") print("\n (ii-a) IL COSTO DI ATTRAVERSAMENTO, per GAMBA, su tutte le coppie a due lati") print(" (e' la misura robusta: ~130 osservazioni appaiate invece di 3 strutture)") print(f" {'coppia':<10} {'n gambe':>8} {'mezzo-spread lineare':>21} " f"{'mezzo-spread inverse':>21} {'Δ appaiata':>11}") for lin, _ in PAIRS: r = res2[lin] out[f"dh_{lin}"] = r.get("dhalf", float("nan")) print(f" {lin:<10} {r.get('nboth', 0):>8} {r.get('hl', float('nan')):>20.2%} " f"{r.get('hi', float('nan')):>20.2%} {r.get('dhalf', float('nan')):>+10.2%}") # (ii) f_venue APPAIATO: la stessa struttura, gli stessi strike, nelle due famiglie print(f"\n (ii-b) f_venue sulla STESSA struttura e sugli STESSI strike nelle due famiglie") print(f" {'coppia':<10} {'n strutture':>12} {'f_venue lineare':>16} " f"{'f_venue inverse':>16} {'Δ appaiata':>11}") idx = {r["name"]: r for r in rows} for lin, inv in PAIRS: sh = scegli_put(rows, lin, SHORT_D, "vendi", DTE_LO, DTE_HI) wg = {round(r["dte"], 4): r for r in scegli_put(rows, lin, LONG_D, "compra", DTE_LO, DTE_HI)} fl, fi, dl = [], [], [] for a in sh: b = wg.get(round(a["dte"], 4)) if b is None or b["K"] >= a["K"]: continue A, B = idx.get(a["twin"] or ""), idx.get(b["twin"] or "") if A is None or B is None: continue if not (a["bid"] and b["ask"] and A["bid"] and B["ask"]): continue cmL = lot_usd(a["mark"], a) - lot_usd(b["mark"], b) ceL = lot_usd(a["bid"], a) - lot_usd(b["ask"], b) cmI = lot_usd(A["mark"], A) - lot_usd(B["mark"], B) ceI = lot_usd(A["bid"], A) - lot_usd(B["ask"], B) if cmL <= 0 or cmI <= 0: continue fl.append(ceL / cmL); fi.append(ceI / cmI); dl.append(ceL / cmL - ceI / cmI) out[f"fv_{lin}"] = med(dl) out[f"fvn_{lin}"] = len(dl) print(f" {lin:<10} {len(dl):>12} {med(fl):>16.3f} {med(fi):>16.3f} " f"{med(dl):>+11.3f}") print("\n ⚠️ e' la MEDIANA DELLE DIFFERENZE APPAIATE su strutture con gli STESSI") print(" strike: §14 confrontava strike auto-selezionati e misurava anche la griglia.") print("\n (iii) L'UNICA COSA DAVVERO DIVERSA FRA LE DUE FAMIGLIE E' LA GRIGLIA DEGLI") print(" STRIKE: la lineare e' un sottoinsieme (§1). Quanto deforma la struttura?") print(" Si sceglie il delta bersaglio due volte: fra TUTTI gli strike inverse, e") print(" fra i soli strike che esistono ANCHE sulla lineare.") print(f" {'coppia':<10} {'bersaglio':>10} {'scad':>5} {'|d| griglia piena':>18} " f"{'|d| griglia lineare':>20} {'Δ strike medio':>15}") for lin, inv in PAIRS: twins = {r["twin"] for r in rows if r["fam"] == lin and r["twin"]} for tgt in (SHORT_D, LONG_D, TAIL_D): full, restr, dk = [], [], [] for e in sorted({round(r["dte"], 4) for r in rows if r["fam"] == inv and r["typ"] == "P" and DTE_LO <= r["dte"] <= DTE_HI}): g = [r for r in rows if r["fam"] == inv and r["typ"] == "P" and round(r["dte"], 4) == e and np.isfinite(r["delta"])] gr = [r for r in g if r["name"] in twins] if not g or not gr: continue a = min(g, key=lambda r: abs(r["delta"] - tgt)) b = min(gr, key=lambda r: abs(r["delta"] - tgt)) full.append(abs(a["delta"] - tgt)); restr.append(abs(b["delta"] - tgt)) dk.append(abs(a["K"] - b["K"])) if full: print(f" {lin:<10} {tgt:>10.2f} {len(full):>5} {med(full):>18.4f} " f"{med(restr):>20.4f} {med(dk):>15,.0f}") print(" Se le due colonne coincidono, la griglia piu' povera NON deforma la") print(" struttura e un backtest fatto sull'inverse resta eseguibile sulla lineare.") return out def verdetto(att: dict, dec: dict, rows: list[dict]) -> None: print("\n" + "=" * 104) print("8. ATTESE DICHIARATE PRIMA DI MISURARE — esito") print("=" * 104) for k in sorted(att): print(f" {k:<14} {att[k]}") print("\n" + "=" * 104) print("9. VERDETTO") print("=" * 104) iv_ok = True # per COSTRUZIONE: una sola superficie di mark al venue (§2b) dh = [dec.get(f"dh_{l}") for l, _ in PAIRS] fv = [dec.get(f"fv_{l}") for l, _ in PAIRS] dh_ok = all(x is not None and np.isfinite(x) and abs(x) < 0.01 for x in dh) fv_ok = all(x is not None and np.isfinite(x) and abs(x) < 0.05 for x in fv) print(" (i) il PREZZO EQUO e' lo stesso numero per costruzione (una sola superficie di") print(" mark al venue, §2b) -> nessun dato da raccogliere puo' cambiarlo") print(f" (ii-a) mezzo-spread per gamba, Δ appaiata entro 1pp: {'SI' if dh_ok else 'NO'}" + " (Δ misurate: " + ", ".join(f"{x:+.2%}" for x in dh if x is not None) + ")") print(f" (ii-b) f_venue di struttura, Δ appaiata entro 0,05: {'SI' if fv_ok else 'NO'}" + " (Δ misurate: " + ", ".join(f"{x:+.3f}" for x in fv if x is not None) + ")") fv_ok = fv_ok and dh_ok if iv_ok and fv_ok: print("\n 🚨 LE DUE SUPERFICI SONO LA STESSA SUPERFICIE, a strike appaiati: la catena") print(" INVERSE che il progetto gia' raccoglie e' un proxy sufficiente della lineare") print(" sia sul PREZZO sia sul COSTO DI ATTRAVERSAMENTO.") print(" => LA RACCOMANDAZIONE DI §51 (raccogliere la catena USDC, +18% di chiamate/giro)") print(" NON E' GIUSTIFICATA DALLA RAGIONE CHE PORTA: il f di VRP01 misurato") print(" sull'inverse non e' 'il numero della famiglia sbagliata', e' lo stesso numero.") elif iv_ok and not fv_ok: print("\n ⚠️ Il PREZZO coincide ma il COSTO DI ATTRAVERSAMENTO no: la meta' 'errore di") print(" modello' del f si trasferisce, la meta' 'spread' NO. §51 va RIDOTTA alla sola") print(" domanda sullo spread, che si risponde con una CAMPAGNA A TERMINE, non con un") print(" collettore permanente (stessa forma della raccomandazione depth di §51).") else: print("\n Le due superfici prezzano DIVERSO a strike appaiati: la raccomandazione §51") print(" REGGE ed e' anzi piu' forte di come e' scritta.") print("\n Sulla domanda di §64 — *esiste sulla lineare una struttura che l'inverse non fa?*") print(" la risposta sta nella tabella §4, e va letta insieme a due regole gia' scritte:") print(" (a) 'niente short-vol da modello in deploy' (19/06) non e' una regola di") print(" eseguibilita' e non decade col lotto;") print(" (b) §46 ha gia' refutato la copertura statica di coda (maxDD sale in 162/162).") print("\n IN EURO/GIORNO: questo filone non produce reddito. Non propone nessuna struttura,") print(" non cambia nessun peso, non tocca il libro. Il suo intero valore e' una DECISIONE DI") print(" INFRASTRUTTURA da +18% di chiamate/giro sul collettore, e un numero (il pavimento") print(" del tick) che le prossime ondate non dovranno ri-misurare. **+0,00 EUR/giorno.**") # ================================================================== main def salva(raw: dict) -> Path: SNAPDIR.mkdir(parents=True, exist_ok=True) p = SNAPDIR / f"snap_{raw['ts'].replace(':', '').replace('-', '')[:15]}.json" p.write_text(json.dumps(raw)) return p def carica() -> list[dict]: if not SNAPDIR.exists(): return [] out = [] for p in sorted(SNAPDIR.glob("snap_*.json")): try: out.append(json.loads(p.read_text())) except Exception: print(f" ⚠️ snapshot illeggibile, saltato: {p.name}") return out def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--snap", action="store_true", help="solo un'istantanea nuova") ap.add_argument("--report", action="store_true", help="solo il report sugli snapshot su disco") a = ap.parse_args() fa_snap = a.snap or not a.report fa_rep = a.report or not a.snap print("=" * 104) print("USDC-SURFACE (§64) — la superficie lineare contro la sua gemella inverse, appaiata") print("=" * 104) print("Sole GET pubbliche, nessun ordine. Qui si misura la NEGOZIABILITA', non un edge.") b = Budget() if fa_snap: guardia_finestra() t0 = time.time() raw = fetch_snapshot(b) p = salva(raw) print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429, " f"{time.time()-t0:.0f}s -> {p.name}") if not fa_rep: return 0 snaps = carica() if not snaps: print("\nNESSUNO SNAPSHOT: rilancia con --snap. Verdetto: non misurabile.") return 1 raw = snaps[-1] rows = build(raw) print(f"\n[dato] {len(rows)} strumenti con specifiche E quote nello snapshot piu' recente " f"({raw['ts'][:19]} UTC); {len(snaps)} snapshot in sessione") if not rows: print("Il venue non ha risposto. Verdetto: non misurabile.") return 1 att: dict = {} sez0_repliche(rows, att) sez1_censimento(rows) res2 = sez2_appaiato(rows, att) sez3_tick(rows, att) sez4_strutture(rows, att) sez5_profondita(rows, raw) sez5b_controlli(rows, att) sez6_dispersione(snaps) dec = sez7_decisione(rows, res2, att) verdetto(att, dec, rows) print(f"\n[rete, totale] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429") return 0 if __name__ == "__main__": raise SystemExit(main())