#!/usr/bin/env python """fee_watch.py — sorveglia lo schema fee di Deribit e applica la regola DECISA IN ANTICIPO. PERCHE' ESISTE. Il 2026-07-26 Deribit ha annunciato un nuovo schema fee dal **2026-08-01** senza pubblicare i numeri (la tabella nell'articolo Insights e' un'immagine, fonte secondaria). La risposta del progetto e' stata misurare la CURVA invece di aspettare il numero (`scripts/research/r0726_fee_sensitivity.py`): bps/lato %RT TP01 Sh SKH01 Sh BOOK Sh BOOK CAGR 0 0.00% 1.322 1.567 1.849 21.69% 3 0.06% 1.303 1.495 1.799 20.99% 5 0.10% 1.290 1.446 1.766 20.53% <- assunzione di TUTTI i backtest 10 0.20% 1.258 1.324 1.682 19.39% 15 0.30% 1.226 1.200 1.597 18.26% sensibilita' marginale del BOOK: -0.017 Sharpe/bps, -0.23% CAGR/bps SKH01 e' ~4x piu' sensibile di TP01 (-0.69% vs -0.09% CAGR/bps): round-trip discreti contro posizione continua vol-targeted -> se il taker sale, il primo parametro da rivedere e' il PESO 75/25, non altro. REGOLA CONGELATA (decisa PRIMA di vedere il numero, per non deciderla col numero davanti): * taker <= 5 bps/lato -> non si tocca nulla (i backtest restano conservativi o esatti) * 5 < taker <= 10 bps -> si riporta il costo, nessuna azione dovuta * taker > 10 bps/lato -> si rivede il PESO di SKH01 (`r0724_skh_live_weight` / `r0726_reeval_live_weight`), passando da `weights_tilt_null` COSA LEGGE. `public/get_instrument` (endpoint PUBBLICO, nessuna chiave): `taker_commission` e `maker_commission` sono il tier BASE dell'istrumento, cioe' quello che paga questo conto — a $600 il volume 30g e' trascurabile e ogni soglia VIP e' fuori portata. Sorveglia anche `max_liquidation_commission`, che l'annuncio dice diventera' 1% su tutti i prodotti. Cross-check autorevole (best-effort): la fee REALMENTE pagata, dai trade del conto. ⚠️ Il tier VIP di un conto ad alto volume NON e' leggibile qui. La guardia `test_fee_watch_assume_tier_base` congela questa assunzione: se il conto smettesse di essere al tier base, questo script misurerebbe la cosa sbagliata. uv run python scripts/live/fee_watch.py # report uv run python scripts/live/fee_watch.py --quiet # stampa/allerta solo se qualcosa cambia """ from __future__ import annotations import json import sys from pathlib import Path import requests ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from src.live.notifier import notify # noqa: E402 STATE = ROOT / "data" / "fee_watch" / "state.json" INSTRUMENTS = ("BTC-PERPETUAL", "ETH-PERPETUAL") API = "https://www.deribit.com/api/v2/public/get_instrument" # --- riferimenti CONGELATI (cambiarli invalida la curva di r0726_fee_sensitivity.py) --- BASELINE_TAKER_BPS = 5.0 # 0.10% RT: l'assunzione di OGNI backtest del progetto BASELINE_MAKER_BPS = 0.0 SOGLIA_OK = 5.0 # <= -> nessuna azione SOGLIA_AZIONE = 10.0 # > -> rivedere il peso di SKH01 D_SHARPE_PER_BPS = -0.017 # sensibilita' marginale del BOOK, misurata D_CAGR_PER_BPS = -0.0023 def fetch_fees(instrument: str) -> dict: """Commissioni correnti dell'istrumento (frazione di nozionale -> bps).""" r = requests.get(API, params={"instrument_name": instrument}, timeout=15) r.raise_for_status() res = r.json()["result"] return dict( instrument=instrument, taker_bps=float(res["taker_commission"]) * 1e4, maker_bps=float(res["maker_commission"]) * 1e4, liq_bps=float(res.get("max_liquidation_commission", 0.0)) * 1e4, ) def verdict(taker_bps: float) -> tuple[str, str]: """Applica la regola congelata. Ritorna (livello, motivazione).""" if taker_bps <= SOGLIA_OK: return "OK", f"taker {taker_bps:.1f}bps <= {SOGLIA_OK:.0f}: non si tocca nulla" if taker_bps <= SOGLIA_AZIONE: d = (taker_bps - BASELINE_TAKER_BPS) return "NOTA", (f"taker {taker_bps:.1f}bps: costo ~{d * D_SHARPE_PER_BPS:+.3f} Sharpe / " f"{d * D_CAGR_PER_BPS:+.2%} CAGR di book, nessuna azione dovuta") return "AZIONE", (f"taker {taker_bps:.1f}bps > {SOGLIA_AZIONE:.0f}: rivedere il PESO di SKH01 " f"(4x piu' fee-sensibile di TP01) via weights_tilt_null") def realized_fee_bps(limit: int = 20) -> dict: """Fee REALMENTE pagata sui trade del conto, in bps di nozionale. Fonte autorevole, ma disponibile solo se il book ha tradato di recente: {} non e' 'zero', e' 'non misurata'.""" try: from src.live.deribit import DeribitRead d = DeribitRead() except Exception: return {} out: dict[str, float] = {} for ins in INSTRUMENTS: tot_fee_usd = tot_notional = 0.0 try: trades = d.trade_history(ins, limit=limit) except Exception: continue for t in trades: try: notional = abs(float(t.get("amount") or 0.0)) # perp: nozionale in USD price = float(t.get("price") or 0.0) fee = abs(float(t.get("fee") or 0.0)) # in valuta di settlement if notional <= 0 or price <= 0: continue tot_fee_usd += fee * price tot_notional += notional except (TypeError, ValueError): continue if tot_notional > 0: out[ins] = tot_fee_usd / tot_notional * 1e4 return out def load_state() -> dict: if STATE.exists(): try: return json.loads(STATE.read_text()) except Exception: return {} return {} def save_state(cur: dict) -> None: STATE.parent.mkdir(parents=True, exist_ok=True) STATE.write_text(json.dumps(cur, indent=2, sort_keys=True)) def diff_vs(prev: dict, cur: dict) -> list[str]: """Cambiamenti rispetto all'ultima lettura. Prima lettura = nessun cambiamento (non e' un evento: e' l'inizializzazione).""" if not prev: return [] ch = [] for ins, c in cur.items(): p = prev.get(ins) if not p: ch.append(f"{ins}: strumento nuovo nella sorveglianza") continue for k, lab in (("taker_bps", "taker"), ("maker_bps", "maker"), ("liq_bps", "liquidazione")): if abs(float(p.get(k, -1)) - float(c[k])) > 1e-9: ch.append(f"{ins} {lab}: {float(p.get(k, float('nan'))):.2f} -> {c[k]:.2f} bps") return ch def run() -> dict: cur = {ins: fetch_fees(ins) for ins in INSTRUMENTS} prev = load_state() changes = diff_vs(prev, cur) worst = max(c["taker_bps"] for c in cur.values()) lvl, why = verdict(worst) return dict(current=cur, changes=changes, worst_taker_bps=worst, level=lvl, reason=why, first_read=not prev) def main() -> int: r = run() quiet = "--quiet" in sys.argv interessante = bool(r["changes"]) or r["level"] != "OK" if not quiet or interessante: print("=" * 86) print(" FEE WATCH — schema fee Deribit vs la regola decisa in anticipo") print("=" * 86) print(f"\n {'strumento':<16}{'taker':>10}{'maker':>10}{'liquidaz.':>12}") for ins, c in r["current"].items(): print(f" {ins:<16}{c['taker_bps']:>9.2f}{c['maker_bps']:>10.2f}{c['liq_bps']:>11.2f}") print(f"\n baseline dei backtest: taker {BASELINE_TAKER_BPS:.1f} / " f"maker {BASELINE_MAKER_BPS:.1f} bps (0.10% RT)") print(f" verdetto: [{r['level']}] {r['reason']}") if r["first_read"]: print(" (prima lettura: stato inizializzato, nessun confronto possibile)") for c in r["changes"]: print(f" CAMBIO: {c}") real = realized_fee_bps() if real: print("\n cross-check sui trade REALI del conto (fonte autorevole):") for ins, bps in real.items(): print(f" {ins:<16}{bps:>9.2f} bps/lato effettivi") else: print("\n cross-check sui trade reali: NON MISURATO (nessun trade recente leggibile)") if r["changes"] or r["level"] == "AZIONE": notify("💸 FEE WATCH — schema fee Deribit", { "verdetto": f"[{r['level']}] {r['reason']}", **{f"cambio {i+1}": c for i, c in enumerate(r["changes"])}, }) save_state(r["current"]) return 2 if r["level"] == "AZIONE" else 0 if __name__ == "__main__": raise SystemExit(main())