Files
PythagorasGoal/src/live/usde.py
T
Adriano Dal Pastro f82f685528 cinque punti della revisione 09/09: usde_watch a 1000 trade + finestra 24h, cuscino_watch (equity USDC, riconverte da solo), PREVDAY-01 kill/veto cablati, SCALA-01 al 2027-02-28, versamento 04/09 dichiarato
Decisi dall'operatore il 2026-09-10, verificati da revisione fable (15 segnalazioni, 12 applicate).

- usde_watch: TRADE_LIMIT 1000 (count max Deribit) e trade_copertura(): lista troncata o ultima
  lettura oltre la finestra di 24h del gateway => somma NON leggibile, reward non attribuito (P12).
  Riga del 07/09 corretta nel log con campo `correzione` (400 USDE erano acquisti, non reward).
- cuscino_watch.py (cron :53, monitor_health): equity USDC contro cuscino derivato da
  usde.cuscino_richiesto_usd (formula spostata in src/live/usde.py, usde_convert la importa);
  OK/PREAVVISO/SCOPERTO/BLIND; sotto zero lancia usde_convert --quota quota_ripristino(0.20)=0.64
  --esegui con guardie (execution_enabled, depeg_warn, 1 tentativo/6h). Primo giro: PREAVVISO, +$26.
- usde_convert: il tetto del venue vale solo in ACQUISTO (bloccava la vendita).
- paper_prevday: GATE PREVDAY-01 cablato (2027-06-21, kill Sharpe giornaliero < -0,50 su >=180 g
  attivi, veto >=80% barre ricostruibili + divergenze non crescenti con soglia materiale).
  Oggi: +0,95 su 81 g, 1942/1943 ricostruibili, kill NON MATURO.
- CLAUDE.md: arming 20/06 (TP01) / 23/06 (BOOK); piano EUR 5.000 chiuso col versamento 04/09
  ($2.414,68 dal balance, dichiarato); SCALA-01 non prima del 2027-02-28 (A2 dal 01/09);
  PREVDAY-01 con data, kill, veto; §5.17 riparato con i limiti dichiarati (ratchet, slack zero a 0,70).
- test: 1062 (+31): test_cuscino_watch (16), test_paper_prevday_gate (8), test_usde_watch (+5).

Fixes #2
Fixes #3
Fixes #4
Fixes #5
Fixes #6

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kqvff47UBGeYfj1QeN4zE
2026-09-10 13:24:28 +00:00

132 lines
5.8 KiB
Python

"""USDE come collaterale a rendimento — autorita' unica su config, prezzo e valutazione.
PERCHE' UN MODULO. Il 2026-08-26 il test di eligibilita' ($500 USDC->USDE) ha scoperto che
`shadow._equity` leggeva solo il conto USDC: -24% fantasma, falso 'USCITA DI FONDI' e vendite
indesiderate al giro successivo. La riparazione e' nata dentro shadow.py; qui diventa
struttura: shadow (equity ORARIA del book) e scripts/live/usde_watch.py (sorveglianza
GIORNALIERA) derivano entrambi da questo modulo, che a sua volta legge la sezione `usde` di
`config/live.json` (P1: il bersaglio si deriva, non si ridichiara).
LE PROPRIETA' DELLA VALUTAZIONE (blindate in tests/test_shadow_usde.py e test_usde_watch.py):
* il prezzo viene dall'indice pubblico `usde_usdc` (mediana multi-exchange, clamp +-0.5%
per fonte) — MAI dal book interno: il 10/10/2025 Binance marco' USDe a $0.65 sull'oracle
del proprio book mentre l'indice mondo diceva ~$0.99;
* un depeg sotto la pari PASSA nel sizing (il rischio vero e' il mark-down del collaterale);
* sopra la pari si clampa a 1.0 (conservativo);
* prezzo non leggibile -> 1.0 DICHIARATO, mai 0 (contare 0 ricrea il falso -24%; P5: il
fallback si sceglie sul danno).
"""
from __future__ import annotations
import json
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
CFG_PATH = PROJECT_ROOT / "config" / "live.json"
SPOT = "USDE_USDC" # coppia spot Deribit (fee 0, spread osservato ~3 bps il 26/08)
# Default = i valori decisi il 2026-08-26 (r0826_usde_scenari + verifica sul venue). Usati SOLO
# se config/live.json non ha la sezione `usde`: la provenienza sta nel campo `fonte` di cfg(),
# cosi' un report dice quale delle due configurazioni sta guardando (P7).
_DEFAULTS = {
"index_name": "usde_usdc",
"haircut": 0.10,
"quota_max_frac": 0.50,
"venue_cap_frac": 0.312, # tetto MISURATO del venue sulla quota USDE (non documentato)
"venue_cap_misurato": "2026-08-31",
"depeg_warn": 0.99,
"depeg_crit": 0.95,
}
def cfg() -> dict:
"""Sezione `usde` di config/live.json; default dichiarati se assente. Mai solleva."""
try:
raw = json.loads(CFG_PATH.read_text()).get("usde")
except Exception:
raw = None
out = dict(_DEFAULTS)
if isinstance(raw, dict):
out.update(raw)
out["fonte"] = "config/live.json"
else:
out["fonte"] = "default (sezione `usde` assente in config)"
return out
def index_url(index_name: str | None = None) -> str:
return ("https://www.deribit.com/api/v2/public/get_index_price?index_name="
f"{index_name or cfg()['index_name']}")
def prezzo_indice(url: str | None = None, timeout: float = 5.0) -> float | None:
"""Indice pubblico Deribit (tokenless). None se non leggibile o fuori banda. Mai solleva."""
try:
import requests
r = requests.get(url or index_url(), timeout=timeout).json()
px = float(r["result"]["index_price"])
return px if 0.0 < px < 2.0 else None
except Exception:
return None
def prezzo(client=None, url: str | None = None) -> tuple[float | None, str]:
"""Catena di prezzo: indice pubblico -> ticker spot via gateway -> None. -> (px, fonte)."""
px = prezzo_indice(url=url)
if px is not None:
return px, "indice"
if client is not None:
try:
px = float(client.ticker(SPOT).get("last_price") or 0) or None
if px is not None and 0.0 < px < 2.0:
return px, "ticker spot"
except Exception:
pass
return None, "non leggibile"
def valuta(eq_usde: float, px: float | None) -> tuple[float, str]:
"""PURA. -> (valore USD del collaterale USDE, nota di provenienza)."""
if eq_usde <= 0:
return 0.0, ""
if px is None or not (0.0 < px < 2.0):
return eq_usde, f"USDE {eq_usde:,.0f} valutato 1.0000 (indice non leggibile)"
p = min(px, 1.0)
return eq_usde * p, f"USDE {eq_usde:,.0f} @ {p:.4f}"
# --- CUSCINO USDC DI REGOLAMENTO ------------------------------------------------------------
# P&L e funding dei perp USDC-lineari si regolano in USDC, non nel collaterale: a quota USDE alta
# il saldo USDC va negativo alla prima perdita del libro e Deribit lo finanzia allo 0,05%/GIORNO
# (KB "Cross collateral specifications": 18,25%/anno, 4,3x la resa USDE). Il cuscino richiesto e'
# il disaster-SL sulla MASSIMA esposizione lorda che il libro puo' prendere:
# n_asset x frac x disaster_sl_pct x equity_totale
# tutto letto da config/live.json e src/live/book.py, MAI ridichiarato (P1). Prima del 10/09 la
# formula viveva in scripts/live/usde_convert.py; il sorvegliante cuscino_watch la deriva da qui,
# e usde_convert pure — due lettori, una formula.
N_ASSET = 2
def frazione_cuscino() -> tuple[float, str]:
"""PURA sulla config. -> (frazione dell'equity totale che deve restare in USDC, come)."""
cfg = json.loads(CFG_PATH.read_text())
sl = float(cfg["disaster_sl_pct"])
from src.live import book as B # lazy: book importa shadow, che importa questo modulo
frac = float(B.WEIGHT)
return N_ASSET * frac * sl, f"{N_ASSET} asset x frac {frac} x SL {sl:.0%}"
def cuscino_richiesto_usd(equity_tot: float) -> tuple[float, str]:
"""Cuscino USDC di REGOLAMENTO in dollari, per un'equity totale data. -> (usd, come)."""
f, come = frazione_cuscino()
return equity_tot * f, f"{come} su ${equity_tot:,.2f}"
def quota_ripristino(margine_frac: float) -> float:
"""PURA sulla config. Quota USDE che lascia il cuscino PIU' un margine (frazione del cuscino):
e' il bersaglio della riconversione automatica quando il cuscino e' scoperto. Il margine serve
a non tornare sul bordo alla prima ora in perdita (P6: la persistenza e' latenza)."""
f, _ = frazione_cuscino()
return max(0.0, 1.0 - f * (1.0 + margine_frac))