usde: da patch d'emergenza a struttura — config, modulo unico, sorveglianza
- config/live.json sezione `usde` (unica autorita', P1): indice, haircut 10%, tetto allerta quota 50%, soglie depeg 0.99/0.95 coi criteri dichiarati (P6) - src/live/usde.py: config + catena di prezzo (indice pubblico -> ticker -> 1.0 dichiarato) + valutazione PURA; shadow._collaterale_usde ora deriva da qui - scripts/live/usde_watch.py + cron_usde.sh (12:35 UTC, dopo la finestra reward): reward per delta netto trade (P12: senza inventare attribuzioni), depeg (crit ripetuto, resto a transizione, P9), quota anche per deriva passiva (N4); applica il verdetto di eligibilita' pre-registrato (>=1 reward entro 29/08) - serie data/live/usde_watch.jsonl sotto monitor_health (max 30h, P5: un watch fermo non deve leggersi come "va tutto bene"); baseline 14:17Z registrata - GATE USDE-01 in CLAUDE.md §4; test 775 (+17 in tests/test_usde_watch.py) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B9UyJLHzR7EJzxR3iQ3RN1
This commit is contained in:
@@ -86,6 +86,12 @@ MONITORS: tuple[MonitorSpec, ...] = (
|
||||
MonitorSpec("collect_chain", "chain_collect", "runs.jsonl", 1.0, max_age_h=3.0,
|
||||
gate="serie irrecuperabile — la catena opzioni non si ricostruisce a posteriori",
|
||||
open_labeled=False),
|
||||
# Sorveglianza del collaterale USDE (cron 12:35 UTC): se si ferma, spariscono gli allarmi
|
||||
# depeg/quota e il silenzio si legge come "va tutto bene" (P5). max_age 30h = un giro
|
||||
# mancato + margine; timbri di esecuzione come collect_chain -> open_labeled=False.
|
||||
MonitorSpec("usde_watch", "live", "usde_watch.jsonl", 24.0, max_age_h=30.0,
|
||||
gate="allarmi depeg/quota USDE + verdetto eligibilita' 29/08",
|
||||
open_labeled=False),
|
||||
)
|
||||
|
||||
|
||||
|
||||
+11
-27
@@ -12,6 +12,7 @@ import pandas as pd
|
||||
|
||||
from src.backtest.harness import load
|
||||
from src.live.deribit import DISASTER_LABEL, INSTRUMENT, DeribitRead, build_rebalance_order
|
||||
from src.live import usde as _usde
|
||||
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
@@ -88,42 +89,25 @@ def _positions(client):
|
||||
return pos, note, err
|
||||
|
||||
|
||||
USDE_INDEX_URL = "https://www.deribit.com/api/v2/public/get_index_price?index_name=usde_usdc"
|
||||
# L'autorita' su config/prezzo/valutazione USDE e' src/live/usde.py (che legge la sezione
|
||||
# `usde` di config/live.json). L'URL resta un attributo di modulo perche' i test lo patchano
|
||||
# (tests/test_shadow_usde.py) per forzare il fallback al ticker senza toccare la rete.
|
||||
USDE_INDEX_URL = _usde.index_url()
|
||||
|
||||
|
||||
def _collaterale_usde(client) -> tuple[float, str]:
|
||||
"""Valore USD dell'USDE tenuto come collaterale (test di eligibilita' 2026-08-26).
|
||||
|
||||
PERCHE' ESISTE. `_equity` leggeva SOLO il conto USDC: convertire $500 in USDE ha fatto
|
||||
sparire il 24% dell'equity dalla vista del book — falso 'USCITA DI FONDI' e vendite
|
||||
indesiderate al giro successivo. L'equity del conto e' il TOTALE cross-collateral.
|
||||
|
||||
IL PREZZO viene dall'indice pubblico `usde_usdc` (mediana multi-exchange con clamp
|
||||
±0,5% per fonte) — NON dal book interno ne' da 1.0 cablato: il 10/10/2025 su Binance
|
||||
l'oracle sul proprio book ha marcato USDe a $0,65 mentre l'indice mondo diceva ~$0,99.
|
||||
Sotto la pari il prezzo PASSA (un depeg vero deve ridurre il sizing, non sparire);
|
||||
sopra la pari si clampa a 1.0 (conservativo). Se l'indice non e' leggibile: fallback al
|
||||
last del ticker via gateway, poi 1.0 DICHIARATO — contare 0 ricreerebbe esattamente il
|
||||
falso -24% che questa funzione elimina (P5: il fallback si sceglie sul danno)."""
|
||||
"""Valore USD dell'USDE tenuto come collaterale. Il PERCHE' e le proprieta' (depeg che
|
||||
PASSA nel sizing, clamp sopra la pari, fallback 1.0 dichiarato mai 0) stanno in
|
||||
src/live/usde.py — difetto d'origine 2026-08-26: `_equity` leggeva solo il conto USDC,
|
||||
-24% fantasma dopo la prima conversione."""
|
||||
try:
|
||||
eq = float(client.account_summary("USDE").get("equity") or 0)
|
||||
except Exception:
|
||||
return 0.0, ""
|
||||
if eq <= 0:
|
||||
return 0.0, ""
|
||||
px = None
|
||||
try:
|
||||
import requests
|
||||
r = requests.get(USDE_INDEX_URL, timeout=5).json()
|
||||
px = float(r["result"]["index_price"])
|
||||
except Exception:
|
||||
try:
|
||||
px = float(client.ticker("USDE_USDC").get("last_price") or 0) or None
|
||||
except Exception:
|
||||
px = None
|
||||
if px is None or not (0.0 < px < 2.0):
|
||||
return eq, f"USDE {eq:,.0f} valutato 1.0000 (indice non leggibile)"
|
||||
return eq * min(px, 1.0), f"USDE {eq:,.0f} @ {min(px, 1.0):.4f}"
|
||||
px, _fonte = _usde.prezzo(client, url=USDE_INDEX_URL)
|
||||
return _usde.valuta(eq, px)
|
||||
|
||||
|
||||
def _equity(client, marks):
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""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,
|
||||
"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}"
|
||||
Reference in New Issue
Block a user