#!/usr/bin/env python """r0830_usde_quota.py — la decisione di QUOTA USDE, dovuta lunedi' 31/08. SOLA LETTURA. COSA DECIDE E COSA NO. Il gate USDE-01 e' CHIUSO IDONEO (28/08): la pista e' aperta. Resta da scegliere *quanto* del conto tenere in USDE. Questo script NON sceglie: mette accanto ai criteri candidati la quota che ciascuno implica e il suo prezzo, perche' la scelta e' dell'operatore (N4: il rischio emittente non e' diversificabile dagli sleeve). IL RISULTATO STRUTTURALE, DICHIARATO PRIMA DI GUARDARE I NUMERI (N6/P6). Il valore atteso e' LINEARE in q: EV(q) = q*E*(apr - hazard*...). Un massimo lineare sta sempre in un ANGOLO (0% o 100%). ⇒ **l'EV non puo' scegliere una quota interna**: puo' solo dire se il segno e' positivo. La quota interna nasce SOLO da un criterio sulla CODA, e quel criterio va dichiarato, non ottimizzato (M8: un argmax dentro un plateau non e' una decisione). Quindi la struttura e': (A) il rendimento osservato, CON LA SUA BANDA e il suo n — un pagamento non e' un tasso; (B) l'hazard di PAREGGIO: sopra quello ogni quota e' EV-negativa. Criterio senza criterio; (C) il rischio MARGINALE, dato che il 100%-Deribit e' gia' stato accettato il 26/07; (D) i vincoli operativi: il haircut morde? il depeg costa churn? (E) il menu dei criteri di coda, ciascuno con la quota che implica e cosa costa in resa. LENTI (N8: quando le leve hanno ordini di grandezza diversi, dirlo). La resa si legge in DUE monete e NON vanno sommate: (1) CASSA, gli euro veri all'anno; (2) DRIFT-EQUIVALENTE, via l'equivalenza registrata in CLAUDE.md 100 EUR/mese == +4,07%/anno di drift, che e' una capitalizzazione a 10 anni, non un flusso. SEMPLIFICAZIONE DICHIARATA: EUR e USD trattati 1:1. A ~1,08 sposta i "mesi di versamento" di ~8% (su 2,0 mesi = 0,16 mesi): non cambia nessuna soglia di questa tabella. uv run python scripts/research/r0830_usde_quota.py """ from __future__ import annotations import importlib.util import json import sys from pathlib import Path import numpy as np ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from src.live import usde as U # noqa: E402 # P1: il tasso puntuale si DERIVA dal sorvegliante che gira, non si ridichiara qui. Il bootstrap # (banda) resta locale perche' usde_watch non lo fa — e i due denominatori NON coincidono: # rendimento() divide per il TEMPO VERO fra le letture, questo per il NUMERO di finestre. # La divergenza si stampa invece di essere appianata (P12). _spec = importlib.util.spec_from_file_location( "usde_watch", ROOT / "scripts" / "live" / "usde_watch.py") _uw = importlib.util.module_from_spec(_spec) sys.modules["usde_watch"] = _uw _spec.loader.exec_module(_uw) WATCH = ROOT / "data" / "live" / "usde_watch.jsonl" # --- stato del conto, letto dal venue il 2026-08-30T17:05Z e CONGELATO qui per riproducibilita' --- EQ_USDC = 1563.66151017 AVAIL_USDC = 1552.28862113 # -> margine in uso = equity - available EQ_USDE = 500.175691 GROSS_NOTIONAL = 355.327065 + 213.317387 # --- framework di rischio EREDITATO dalla decisione 26/07, non re-inventato --- # "100% Deribit fino a $20k: accettato P(perso tutto) 10/18/34/64% a p=0,5/1/2/5%" # riprodotto esattamente da P = 1-(1-p)^T con T=20. Il test lo verifica. ORIZZONTE_ANNI = 20 P_DERIBIT = [0.005, 0.01, 0.02, 0.05] # --- piano di versamento: la leva CERTA (N1). Baseline dichiarata in CLAUDE.md --- VERSAMENTO_BASE = 250.0 # EUR/mese, piano corrente VERSAMENTO_ALT = 500.0 # EUR/mese, l'alternativa gia' misurata EUR_PER_100_DRIFT = 4.07 / 100.0 # 100 EUR/mese == +4,07%/anno di drift # --- ancore del libro, dalla banda onesta di CLAUDE.md §2 --- MAXDD_BOOK_HI = 0.0794 # maxDD FULL book, estremo alto della banda [5,99-7,94] MARGINE_PERP = 0.02 # tasso di margine iniziale osservato (verificato sotto) SPREAD_BPS = 3.0 # spot USDE_USDC, osservato sul venue il 26/08 (fee 0) N_BOOT = 200_000 # M23: la risoluzione del MC si dichiara RNG = np.random.default_rng(20260830) def finestre(records: list[dict]) -> list[float]: """I reward per finestra, in USDE. Uno zero E' un'osservazione (P5), non un buco.""" return [float(r["reward_stimato"]) for r in records if r.get("reward_stimato") is not None and not r.get("con_trade")] def apr_banda(rew: list[float], base: float) -> dict: """APR puntuale e banda bootstrap sulle finestre. Con n piccolo la banda E' il risultato.""" n = len(rew) if n == 0: return dict(apr=None, lo=None, hi=None, n=0) a = np.array(rew, dtype=float) punt = a.mean() / base * 365.0 draws = RNG.choice(a, size=(N_BOOT, n), replace=True).mean(axis=1) / base * 365.0 lo, hi = np.percentile(draws, [2.5, 97.5]) return dict(apr=punt, lo=float(lo), hi=float(hi), n=n, per_finestra=[x / base * 365.0 for x in rew]) def p_rovina(p: float, anni: int = ORIZZONTE_ANNI) -> float: return 1.0 - (1.0 - p) ** anni def main() -> int: recs = [json.loads(l) for l in WATCH.read_text().splitlines() if l.strip()] c = U.cfg() px = float(recs[-1]["px"]) usde_usd, _ = U.valuta(EQ_USDE, px) E = EQ_USDC + usde_usd q0 = usde_usd / E rew = finestre(recs) R = apr_banda(rew, EQ_USDE) UFF = _uw.rendimento(recs) # il numero che gira, derivato R["apr_uff"] = UFF["apr"] R["giorni_uff"] = UFF["giorni"] print("=" * 79) print(" ANALISI QUOTA USDE — decisione dovuta 2026-08-31") print(f" config: {c['fonte']} · stato conto congelato al 2026-08-30T17:05Z") print("=" * 79) # ---------------------------------------------------------------- (A) il rendimento print("\n(A) IL RENDIMENTO, COL SUO n") print(f" finestre misurabili : {R['n']} (pagate {sum(1 for x in rew if x > 0)})") print(" per finestra : " + ", ".join(f"{x*100:.2f}%" for x in R["per_finestra"])) print(f" APR di usde_watch : {R['apr_uff']*100:.2f}% annuo" f" (denominatore = tempo vero, {R['giorni_uff']:.2f} giorni) <- IL NUMERO CHE GIRA") print(f" APR per-finestra : {R['apr']*100:.2f}% annuo" f" (denominatore = n finestre, {R['n']}.00 giorni)") print(f" divergenza : {abs(R['apr_uff']-R['apr'])*100:.2f} punti — i due" f" denominatori differiscono di {R['n']-R['giorni_uff']:.2f} giorni. Dichiarata (P12),") print(" non appianata: sotto si usa il puntuale di usde_watch.") print(f" banda bootstrap 95% : [{R['lo']*100:.2f}% - {R['hi']*100:.2f}%]" f" (n={R['n']}, {N_BOOT:,} ricampionamenti)") APR = R["apr_uff"] larghezza = R["hi"] - R["lo"] print(f" ⚠️ la banda e' larga {larghezza*100:.2f} punti su un livello di " f"{R['apr']*100:.2f}% (per-finestra): a n={R['n']} il tasso NON e' misurato, e' abbozzato.") # ---------------------------------------------------------------- (B) hazard di pareggio print("\n(B) L'HAZARD DI PAREGGIO — l'unico criterio che non richiede un criterio") print(f" Perdita totale non recuperabile, resa {APR*100:.2f}%/anno.") print(f" ⇒ hazard annuo di pareggio = APR = {APR*100:.2f}%/anno") print(f" (banda: {R['lo']*100:.2f}% - {R['hi']*100:.2f}%)") print(f" ⇒ recupero di una perdita totale con la sola resa: {1/APR:.1f} anni" f" — INDIPENDENTE dalla quota.") print(f" Letto sul verso della decisione: la quota e' EV-positiva SE E SOLO SE si crede") print(f" che Ethena abbia meno del {APR*100:.1f}% annuo di probabilita' di evento") print(f" catastrofico. Quel numero e' ASSUNTO, non stimato — come il p di Deribit (26/07).") # ---------------------------------------------------------------- (C) rischio marginale print("\n(C) IL RISCHIO MARGINALE, dato il 100%-Deribit gia' accettato (26/07)") print(f" Framework ereditato: P = 1-(1-p)^{ORIZZONTE_ANNI}. Riproduzione della riga 26/07:") riga = " p annuo ".ljust(14) + "".join(f"{p*100:>7.1f}%" for p in P_DERIBIT) print(riga) print(" P(perso tutto)".ljust(14) + "".join(f"{p_rovina(p)*100:>7.0f}%" for p in P_DERIBIT)) print(" atteso 26/07 " + "".join(f"{v:>7}" for v in ("10%", "18%", "34%", "64%"))) ok_repro = all(abs(p_rovina(p) * 100 - v) < 1.5 for p, v in zip(P_DERIBIT, (10, 18, 34, 64))) print(f" riproduzione : {'OK' if ok_repro else 'DIVERGE'} (tolleranza 1,5 punti)") print(" ⇒ L'USDE fa perdere soldi SOLO nel mondo in cui Ethena salta E Deribit sopravvive.") print(" Nel mondo in cui salta Deribit, USDC e USDE si perdono insieme: la quota e'") print(" irrilevante. Il rischio AGGIUNTO dalla quota e' quindi di SECONDO ordine") print(" rispetto a quello gia' accettato — ma e' un TERZO strato sullo STESSO conto,") print(" e N4 dice che un rischio di venue si compra con un secondo CONTO, non") print(" aumentando l'esposizione al primo. Le due letture non si annullano: la prima") print(" dice che la quota costa poco, la seconda che non e' li' che si compra sicurezza.") # ---------------------------------------------------------------- (D) vincoli operativi print("\n(D) I VINCOLI OPERATIVI — il haircut morde? il depeg costa?") margine_uso = EQ_USDC - AVAIL_USDC tasso_margine = margine_uso / GROSS_NOTIONAL print(f" nozionale lordo : ${GROSS_NOTIONAL:,.2f}") print(f" margine in uso : ${margine_uso:,.2f} ⇒ tasso {tasso_margine*100:.2f}%") print(f" tetto del libro : leva lorda 1,00x ⇒ nozionale max ${E:,.0f}" f" ⇒ margine max ~${E*tasso_margine:,.0f}") print(f" haircut USDE {c['haircut']:.0%} : il {1-c['haircut']:.0%} dell'USDE fa margine") print("\n quota USDC USDE margine utilizzabile serve al tetto morde?") binding = [] for q in (q0, 0.25, 0.50, 0.75, 0.90): usde_q, usdc_q = E * q, E * (1 - q) util = usdc_q + usde_q * (1 - c["haircut"]) serve = E * tasso_margine morde = util < serve binding.append(morde) print(f" {q:5.1%} ${usdc_q:9,.0f} ${usde_q:9,.0f} ${util:14,.0f}" f" ${serve:12,.0f} {'SI' if morde else 'no'}") print(f" ⇒ il haircut NON morde a nessuna quota testata: il libro gira al" f" {tasso_margine*100:.1f}% di margine,") print(" tre ordini di grandezza sotto il collaterale. D5: buco quantificato e innocuo.") depeg_eq = (1.0 - c["depeg_crit"]) print(f"\n churn da depeg: a soglia critica {c['depeg_crit']} l'equity cala di" f" {depeg_eq:.0%}*q,") for q in (q0, 0.50): print(f" a quota {q:.1%} ⇒ equity {depeg_eq*q*100:.2f}% ⇒ target di posizione" f" -{depeg_eq*q*100:.2f}% (~${E*depeg_eq*q*1.0:,.0f} di nozionale da girare)") print(" ⇒ il libro de-risca mentre l'USDE e' a sconto e ri-carica al ritorno del peg:") print(" compra alto e vende basso, ma per importi di secondo ordine. Non e' il vincolo.") # ---------------------------------------------------------------- (E) il menu dei criteri print("\n(E) IL MENU DEI CRITERI DI CODA — ciascuno con la quota che implica e il suo prezzo") print(" (l'EV e' lineare in q: NON produce un ottimo interno. Questi si', ma sono") print(" scelte di tolleranza, non misure.)") criteri = [ ("C0 status quo, nessuna decisione", q0, "la quota di oggi, arrivata li' per la taglia del test di eligibilita'"), ("C1 <= maxDD del libro (7,94%)", MAXDD_BOOK_HI, "un evento emittente non deve superare cio' che la strategia gia' fa"), ("C2 <= 6 mesi di versamento", 6 * VERSAMENTO_BASE / E, f"a {VERSAMENTO_BASE:.0f} EUR/mese: il piano arretra <= mezzo anno"), ("C3 <= 12 mesi di versamento", 12 * VERSAMENTO_BASE / E, f"a {VERSAMENTO_BASE:.0f} EUR/mese: il piano arretra <= un anno"), ("C4 tetto di ALLERTA di config", c["quota_max_frac"], "non e' un criterio: e' la soglia oltre cui il watch grida"), ] print(f"\n {'criterio':38s} {'quota':>7s} {'USDE $':>9s} {'resa $/a':>9s}" f" {'drift bk':>9s} {'== EUR/m':>9s} {'perdita':>9s} {'mesi vers':>9s}") for nome, q, _ in criteri: q = min(max(q, 0.0), 1.0) usde_q = E * q resa = usde_q * APR drift = q * APR eur_mese = drift / EUR_PER_100_DRIFT * 100.0 mesi = usde_q / VERSAMENTO_BASE print(f" {nome:38s} {q:6.1%} {usde_q:9,.0f} {resa:9,.2f}" f" {drift*100:8.2f}% {eur_mese:9,.0f} {usde_q:9,.0f} {mesi:9.1f}") print("\n note sulle colonne (N8: due monete, non si sommano):") print(" · 'resa $/a' = CASSA vera all'anno, alla taglia di oggi") print(" · '== EUR/m' = quanto varrebbe in VERSAMENTO l'equivalente di drift, lente 10 anni") print(" · 'mesi vers' = quanti mesi di bonifico brucia una perdita TOTALE") # ---------------------------------------------------------------- (F) attesa e reversibilita' print("\n(F) COSA COSTA ASPETTARE — N10: una data si giustifica col COSTO della misura,") print(" non con la sua difficolta' percepita.") print(f" La conversione e' su spot {U.SPOT}: fee 0, spread osservato ~{SPREAD_BPS:.0f} bps") print(f" (26/08) ⇒ andata+ritorno ~{2*SPREAD_BPS:.0f} bps. La decisione e' REVERSIBILE" " a costo quasi nullo:") print(" non e' una porta a senso unico, e va decisa come tale.") print(f"\n {'attendere':>10s} {'finestre':>9s} {'costo andata/ritorno':>21s}" f" {'resa persa':>11s} {'costo totale':>13s}") for sett, q_target in ((0, 0.50), (2, 0.50), (4, 0.50), (8, 0.50)): incremento = E * q_target - usde_usd giorni = sett * 7 persa = incremento * APR * giorni / 365.0 rt = incremento * 2 * SPREAD_BPS / 10_000.0 n_fin = R["n"] + giorni print(f" {sett:>7d} sett {n_fin:>9d} {rt:>20,.2f}$ {persa:>10,.2f}$" f" {rt+persa:>12,.2f}$") print(f"\n ⇒ portare la quota da {q0:.1%} a 50% fra QUATTRO settimane invece che domani") inc = E * 0.50 - usde_usd costo_4 = inc * APR * 28 / 365.0 print(f" costa ${costo_4:,.2f} di resa non incassata, e compra" f" {R['n']+28}/{R['n']} = {(R['n']+28)/R['n']:.0f}x le finestre.") print(" A n=32 la banda si stringe di ~sqrt(8) ≈ 2,8x: da ~3,4 punti a ~1,2.") print(f" ${costo_4:,.2f} per sapere se il tasso e' 1,1% o 4,5% e' il miglior rapporto") print(" misura/costo che il progetto abbia incontrato (cfr. i due gate d'attesa da mesi") print(" eliminati in una sera perche' nessuno aveva contato i secondi, N10).") # ---------------------------------------------------------------- verdetto a runtime (N11) print("\n" + "=" * 79) print(" VERDETTO calcolato a runtime") print("=" * 79) eur_mese_q0 = q0 * APR / EUR_PER_100_DRIFT * 100.0 cassa_q0 = usde_usd * APR mesi_q0 = usde_usd / VERSAMENTO_BASE inc50 = E * 0.50 - usde_usd costo_attesa_4s = inc50 * APR * 28 / 365.0 verdetti = [] if R["n"] < 8: verdetti.append(f"RISOLUZIONE INSUFFICIENTE PER ALZARE: n={R['n']} finestre, banda" f" [{R['lo']*100:.2f}%-{R['hi']*100:.2f}%] larga {larghezza*100:.1f} punti." f" Alzare la quota ora e' comprare un tasso che non e' ancora misurato.") if not any(binding): verdetti.append("NESSUN VINCOLO OPERATIVO: il haircut non morde a nessuna quota" " testata; il vincolo non e' il margine.") if cassa_q0 < VERSAMENTO_BASE: verdetti.append(f"SCALA: la resa alla quota di oggi vale ${cassa_q0:,.0f}/anno," f" cioe' meno di UN mese di versamento ({VERSAMENTO_BASE:.0f})." f" La leva binding resta il bonifico (N1/N3), non la quota.") if MAXDD_BOOK_HI < q0: verdetti.append(f"ASIMMETRIA: gia' alla quota di oggi ({q0:.1%}) l'evento emittente" f" vale {q0/MAXDD_BOOK_HI:.1f}x il maxDD dell'intero libro" f" ({MAXDD_BOOK_HI:.2%}), che e' il rischio che il progetto" f" ha passato due anni a limare.") verdetti.append( f"REVERSIBILE E QUASI GRATIS DA RIMANDARE: alzare a 50% fra 4 settimane invece che" f" domani costa ${costo_attesa_4s:,.2f} di resa e {2*SPREAD_BPS:.0f} bps di andata/ritorno," f" e porta il campione da {R['n']} a {R['n']+28} finestre.") for v in verdetti: print(f" · {v}") print(f"\n Quota di oggi {q0:.1%} = ${usde_usd:,.0f} · resa ${cassa_q0:,.0f}/anno" f" · == {eur_mese_q0:,.0f} EUR/mese di drift · {mesi_q0:.1f} mesi di bonifico a rischio") print(" La SCELTA resta dell'operatore: questo script mette il prezzo accanto a ogni") print(" criterio, non ne sceglie uno (N4, e la colonna 'cosa la riapre' della §3).") return 0 if __name__ == "__main__": sys.exit(main())