#!/usr/bin/env python """usde_watch.py โ€” sorveglianza giornaliera del collaterale USDE: reward, depeg, quota. SOLA LETTURA. PERCHE' ESISTE. Dal 2026-08-26 il conto tiene USDE come collaterale a rendimento (test di eligibilita': 500 USDE @ 1.0003, regola pre-registrata nel diario 2026-08-26-usde-analisi). Tre cose vanno sorvegliate e nessuna aveva un posto: 1. i REWARD giornalieri (~12:00 UTC) โ€” sono la ragione della posizione, e il gateway non espone il Transaction Log: si rilevano per DELTA di equity USDE al netto dei trade spot. Metodo DICHIARATO, coi suoi limiti: un delta con trade nel mezzo si riporta con `con_trade=true` e se i trade non sono leggibili il delta resta NON ATTRIBUITO invece di essere inventato (P12: una riparazione silenziosa e' un'invenzione). 2. il DEPEG โ€” soglie e criteri in config/live.json `_nota_usde` (P6: dichiarati prima): warn = fuori dalla banda operativa dello spot e oltre il clamp per-fonte dell'indice; crit = meta' del buffer di haircut consumata. 3. la QUOTA sul totale โ€” N4: la quota e' l'unica leva contro il rischio emittente (-100% = 25 anni di resa, non recuperabile). Sopra `quota_max_frac` allerta, e vale anche per deriva PASSIVA: il libro perde -> la quota sale da sola. VERDETTO DI ELIGIBILITA' (regola pre-registrata il 26/08, PRIMA dell'esito): >=1 reward entro il 29/08 -> IDONEO (si apre la decisione di quota, che e' dell'OPERATORE); zero reward alla lettura del 29/08 dopo la finestra delle 12:00 UTC -> NON IDONEO, si riconverte e la pista si chiude. Questo script APPLICA la regola, non la decide. LIMITE DICHIARATO (P6: la latenza si confronta con la durata del fenomeno). Cadenza giornaliera (cron 12:35 UTC, dopo la finestra reward): il depeg Binance del 10/10/2025 duro' ~8h, questa sorveglianza puo' MANCARLO. Accettato perche' la protezione di SIZING e' oraria per costruzione (shadow._collaterale_usde legge l'indice a ogni giro del book) e il danno e' limitato dalla quota. Qui si ALLERTA, non si protegge. ALLARMI (P9: il massimo si spende per l'evento vero): ๐Ÿšจ solo depeg datetime: return datetime.fromisoformat(ts.replace("Z", "+00:00")) def leggi(path: Path = STATE) -> list[dict]: if not path.exists(): return [] out = [] for ln in path.read_text().splitlines(): ln = ln.strip() if ln: out.append(json.loads(ln)) return out def analizza(prev: dict | None, eq_now: float, trades_usde: float | None) -> dict: """PURA. Reward per delta di equity USDE al netto dei trade spot fra le due letture. prev senza equity (primo giro, o giro BLIND) -> baseline: nessun delta da attribuire. trades_usde None = storia trade non leggibile -> il delta si riporta ma NON si attribuisce a reward (P12): meglio un giorno di latenza che un reward inventato.""" if prev is None or prev.get("eq_usde") is None: return dict(delta=None, reward_stimato=None, reward_rilevato=False, con_trade=None) delta = round(eq_now - float(prev["eq_usde"]), 8) if trades_usde is None: return dict(delta=delta, reward_stimato=None, reward_rilevato=False, con_trade=None) stima = round(delta - trades_usde, 8) return dict(delta=delta, reward_stimato=stima, reward_rilevato=bool(stima > EPS_REWARD), con_trade=bool(abs(trades_usde) > 0)) def verdetto(records: list[dict], now: datetime) -> tuple[str, str]: """PURA. Applica la regola pre-registrata del 26/08. -> (stato, motivo).""" for r in records: if r.get("reward_rilevato"): return "IDONEO", (f"reward rilevato il {r.get('data')} " f"(+{r.get('reward_stimato')} USDE)") if now >= _parse(VERDETTO_DOPO): return "NON_IDONEO", "zero reward entro la finestra del 29/08 -> riconvertire, pista chiusa" return "IN_ATTESA", f"finestre reward ~12:00 UTC, verdetto dopo {VERDETTO_DOPO}" def condizioni(rec: dict | None, c: dict) -> set[str]: """PURA. Condizioni di allerta attive su un record.""" if not rec: return set() if rec.get("stato") == "BLIND": return {"BLIND"} att = set() px = rec.get("px") if px is not None: if px < c["depeg_crit"]: att.add("DEPEG_CRIT") elif px < c["depeg_warn"]: att.add("DEPEG_WARN") q = rec.get("quota") if q is not None and q > c["quota_max_frac"]: att.add("QUOTA_OVER") return att def _safe_client(): try: from src.live.deribit import DeribitRead return DeribitRead() except Exception: return None def _trades_spot_da(client, ts_ms: int) -> tuple[float | None, int | None]: """Somma con segno (buy +, sell -) dell'USDE scambiato spot dopo ts_ms. None = non leggibile.""" if client is None: return None, None try: rows = client.trade_history(U.SPOT, limit=50) except Exception: return None, None tot, n = 0.0, 0 for t in rows: if int(t.get("timestamp") or 0) <= ts_ms: continue amt = float(t.get("amount") or 0) tot += amt if (t.get("direction") or "").lower() == "buy" else -amt n += 1 return round(tot, 8), n def main() -> int: quiet = "--quiet" in sys.argv c = U.cfg() records = leggi() prev = records[-1] if records else None now = datetime.now(timezone.utc) client = _safe_client() stato, motivo_blind, eq_usde, eq_usdc = "OK", None, None, None if client is None: stato, motivo_blind = "BLIND", "gateway non raggiungibile" else: try: eq_usde = float(client.account_summary("USDE").get("equity") or 0) except Exception as e: stato, motivo_blind = "BLIND", f"conto USDE non leggibile ({type(e).__name__})" try: eq_usdc = float(client.account_summary("USDC").get("equity") or 0) except Exception: eq_usdc = None # quota non computabile: dichiarato, non 0 px, px_fonte = U.prezzo(client) usd = None if eq_usde is not None: usd, _nota = U.valuta(eq_usde, px) trades_usde, n_trades = (None, None) an = dict(delta=None, reward_stimato=None, reward_rilevato=False, con_trade=None) if stato == "OK" and prev is not None and prev.get("eq_usde") is not None: trades_usde, n_trades = _trades_spot_da(client, int(prev["ts"])) an = analizza(prev, eq_usde, trades_usde) quota = None if usd is not None and eq_usdc is not None and (usd + eq_usdc) > 0: quota = round(usd / (usd + eq_usdc), 4) rec = dict( ts=int(now.timestamp() * 1000), data=now.strftime("%Y-%m-%dT%H:%M:%SZ"), stato=stato, motivo_blind=motivo_blind, eq_usde=eq_usde, px=px, px_fonte=px_fonte, usd=usd, eq_usdc=eq_usdc, quota=quota, trades_usde=trades_usde, n_trades=n_trades, **an, ) v, v_motivo = verdetto(records + [rec], now) rec["verdetto"], rec["verdetto_motivo"] = v, v_motivo STATE.parent.mkdir(parents=True, exist_ok=True) with STATE.open("a") as fh: fh.write(json.dumps(rec) + "\n") # --- allarmi: transizioni (warn/quota/blind), ripetizione solo per il crit --- att_now, att_prev = condizioni(rec, c), condizioni(prev, c) nuove = att_now - att_prev inviati = [] if "DEPEG_CRIT" in att_now: notify("๐Ÿšจ USDE DEPEG", {"indice": px, "soglia": c["depeg_crit"], "collaterale": f"${usd:,.0f}" if usd else "?", "azione": "valutare riconversione โ€” la quota e' il controllo (N4)"}) inviati.append("DEPEG_CRIT") if "DEPEG_WARN" in nuove: notify("โš ๏ธ USDE sotto la pari", {"indice": px, "soglia": c["depeg_warn"], "nota": "entra gia' nel sizing orario del book"}) inviati.append("DEPEG_WARN") if "QUOTA_OVER" in nuove: notify("โš ๏ธ quota USDE sopra il tetto", {"quota": f"{quota:.1%}" if quota else "?", "tetto": f"{c['quota_max_frac']:.0%}", "nota": "anche una deriva passiva conta (il libro perde -> quota sale)"}) inviati.append("QUOTA_OVER") if "BLIND" in nuove: notify("โš ๏ธ usde_watch BLIND", {"motivo": motivo_blind or "?", "nota": "'non vedo' non e' 'va tutto bene' (P5)"}) inviati.append("BLIND") v_prev = prev.get("verdetto") if prev else "IN_ATTESA" if v != v_prev: if v == "IDONEO": notify("๐Ÿ“Œ USDE: conto IDONEO ai reward", {"dettaglio": v_motivo, "prossimo passo": "decisione di quota dell'operatore (tetto allerta 50%)"}) elif v == "NON_IDONEO": notify("โš ๏ธ USDE: conto NON idoneo", {"dettaglio": v_motivo}) inviati.append(f"VERDETTO->{v}") cambiato = bool(inviati) or v != v_prev if not quiet or cambiato: print("=" * 78) print(f" USDE WATCH โ€” {rec['data']} (config: {c['fonte']})") print("=" * 78) if stato == "BLIND": print(f" stato : BLIND โ€” {motivo_blind}") else: print(f" equity USDE : {eq_usde:,.4f} @ {px if px is not None else 'n/d'}" f" ({px_fonte}) -> ${usd:,.2f}" if usd is not None else " equity USDE : n/d") if quota is not None: margine = eq_usdc + usd * (1.0 - c["haircut"]) print(f" quota : {quota:.1%} del totale (tetto allerta {c['quota_max_frac']:.0%})" f" ยท margine utilizzabile ~${margine:,.0f} (haircut {c['haircut']:.0%})") if an["delta"] is not None: attr = ("non attribuibile (trade non leggibili)" if an["reward_stimato"] is None else f"reward stimato {an['reward_stimato']:+.6f} USDE" + (" [con trade nel mezzo]" if an["con_trade"] else "")) print(f" delta : {an['delta']:+.6f} USDE dall'ultima lettura -> {attr}") print(f" verdetto : {v} โ€” {v_motivo}") if inviati: print(f" allarmi : {', '.join(inviati)}") return 0 if __name__ == "__main__": sys.exit(main())