Files
PythagorasGoal/scripts/research/r0830_usde_quota.py
T
Adriano Dal Pastro 035bd086a2 usde: l'EV non puo' scegliere la quota — e aspettare 4 settimane costa $1,33
La decisione di quota e' dovuta lunedi' 31/08. Il risultato strutturale, dichiarato
prima di guardare i numeri: l'EV e' LINEARE in q, quindi il suo argmax e' sempre un
angolo (0% o 100%) e non puo' produrre una quota interna. Ogni quota intermedia nasce
da un criterio sulla CODA, che va dichiarato e non ottimizzato (M8). Lo script quindi
non sceglie: mette il prezzo accanto a ogni criterio candidato.

Misure:
- rendimento 3,26% annuo su n=4 finestre (3 pagate), banda bootstrap 95%
  [1,13% - 4,51%] su 200.000 ricampionamenti. Larga 3,4 punti su un livello di 3,2%:
  a n=4 il tasso non e' misurato, e' abbozzato. Il punto stimato si DERIVA da
  usde_watch.rendimento() invece di essere ridichiarato (P1), e la divergenza fra i
  due denominatori (0,06 punti) e' stampata, non appianata (P12).
- l'hazard annuo di pareggio E' l'APR: la quota e' EV-positiva se e solo se si crede
  che Ethena stia sotto il 3,3%/anno di evento catastrofico. Numero ASSUNTO, non
  stimato, come il p di Deribit del 26/07 — il cui framework P=1-(1-p)^20 lo script
  riproduce prima di usarlo (10/18/33/64% contro 10/18/34/64% registrati).
- il haircut 10% NON morde a nessuna quota testata, fino al 90%: il libro gira al
  2,00% di margine ($11,37 su $568,64), tre ordini di grandezza sotto il collaterale.
  Il vincolo non e' il margine (D5: buco quantificato e innocuo).
- il churn da depeg critico vale ~$52 di nozionale a quota 50%: secondo ordine.

Il numero che decide (N10 — una data si giustifica col COSTO della misura): la
conversione e' fee 0 e ~3 bps di spread, quindi la decisione e' reversibile a
~6 bps. Alzare a 50% fra quattro settimane invece che domani costa $1,33 di resa non
incassata e porta il campione da 4 a 32 finestre, stringendo la banda da ~3,4 punti
a ~1,2.

Due letture del rischio che non si annullano (M28): l'USDE fa perdere soldi solo nel
mondo in cui Ethena salta E Deribit sopravvive, quindi il rischio aggiunto e' di
secondo ordine — ma e' un terzo strato sullo stesso conto, e N4 dice che un rischio
di venue si compra con un secondo CONTO. La prima dice che costa poco, la seconda
che non e' li' che si compra sicurezza.

Verdetto a runtime: risoluzione insufficiente per alzare · nessun vincolo operativo ·
scala (la resa di oggi vale $16/anno, meno di UN mese di versamento: la leva binding
resta il bonifico) · asimmetria (gia' a 24,2% l'evento emittente vale 3,1x il maxDD
dell'intero libro) · reversibile e quasi gratis da rimandare.

La scelta resta dell'operatore: lo script mette i prezzi, non ne sceglie uno (N4).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E1gJmWPA4rEhmc6CQQuNZw
2026-08-30 17:38:35 +00:00

304 lines
17 KiB
Python

#!/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())