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:
@@ -67,6 +67,15 @@ lorda massima 1,0x**, e nessun livello di capitale cambia il profilo di rischio
|
||||
`min_order_usd` 5 · `disaster_sl_pct` 0.30 on-book sulla posizione **netta** · staleness-gate feed
|
||||
2 giorni (blocca) · `skh_feed_max_age_min` 30 (allerta, **non** blocca) · alert Telegram.
|
||||
|
||||
📌 **Dal 2026-08-26 l'equity del book e' il TOTALE cross-collateral** (USDC + USDE valutato
|
||||
all'indice pubblico `usde_usdc`): il conto tiene **500 USDE** (test di eligibilita' reward, verdetto
|
||||
29/08 — GATE USDE-01 in §4). Autorita' dei parametri: sezione `usde` di `config/live.json`
|
||||
(haircut 10%, tetto ALLERTA quota 50%, soglie depeg 0,99/0,95 coi criteri in `_nota_usde`), letta
|
||||
da `src/live/usde.py` — shadow (sizing orario) e `usde_watch` (sorveglianza 12:35 UTC, cron
|
||||
proprio, sotto `monitor_health`) derivano entrambi da li'. ⚠️ Il difetto d'origine: `shadow._equity`
|
||||
leggeva SOLO il conto USDC — la prima conversione avrebbe causato un falso "USCITA DI FONDI −24%"
|
||||
e vendite indesiderate. Chi aggiunge una valuta di collaterale DEVE passare da `usde.py`/shadow.
|
||||
|
||||
⚠️ **Il nome "disaster-SL −30%" inganna:** e' **rotolante e simmetrico**, non un massimo di perdita
|
||||
per trade. Su cicli a ingresso proprio lascia passare perdite oltre −30% senza scattare, e il
|
||||
peggior DD dall'ingresso misurato e' **−60,6% BTC / −61,5% ETH**. Dettaglio in `40-produzione`.
|
||||
@@ -161,6 +170,7 @@ sull'hold-out; cambiarne le soglie guardando l'esito, pure.
|
||||
|
||||
| gate | data | criterio | stato |
|
||||
|---|---|---|---|
|
||||
| **USDE-01 (eligibilita' reward)** | 2026-08-29 | ≥1 reward entro il 29/08 (rilevati da `usde_watch` per delta di equity USDE al netto dei trade spot) → IDONEO e si apre la decisione di QUOTA (dell'operatore; tetto allerta 50%, mai 100% — R1 emittente non recuperabile); zero → si riconverte e la pista si chiude | ✅ pre-registrato il 26/08 PRIMA dell'esito (diario `2026-08-26-usde-analisi`); 500 USDE in conto dalle 13:04Z, baseline watch 14:17Z, verdetto applicato dal cron 12:35 |
|
||||
| **STATARB** | 2026-09-27 | soglie 24/07 (non toccate) + diagnostica statica sempre-short | ✅ monitor riparato e rigenerato il 26/08: la serie vera da' Sharpe **−1,61** a oggi → coi criteri invariati punta al **RITIRO**. Si legge il 27/09, non prima |
|
||||
| **XSR01** | 2026-10-23 | Sharpe ≥1,0 **E** haircut ≤40% — dal 26/08 il numero decisivo e' `r0826_xsr_haircut.py` a **pavimento $10**, citato con la frazione di ordini eseguiti — poi `weights_tilt_null` e **capitale ≥$20k** | ✅ **riscritto il 26/08 su decisione dell'operatore** (dichiarato prima dell'esito, stringe: diario `2026-08-26-xsr01-gate-riscritto`): capitale allineato a "100% Deribit fino a $20k"; serie forward rigenerata (31 barre vere, Sharpe −2,72). Sotto la lente RENDITA il sleeve resta **eguagliato da un conto remunerato** (📌 sotto) |
|
||||
| **DVOLSPREAD** | kill 2026-10-24 se Sh<−0,50; decisione 2027-01-24 | (a) Sharpe>0 (b) marginale ADDS+robust_oos+insample_edge (c) **DSR ≥0,95** (d) `weights_tilt_null`; **veto** se barre attive <80% → si estende, non si decide | ✅ serie rigenerata il 26/08 (−4,41 a oggi, era −14,73 sulla serie rotta); il veto barre-attive ora conta barre VERE e la guardia PREMATURO sorveglia il monitor |
|
||||
@@ -229,7 +239,7 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per
|
||||
finestra hold-out che si allunga — e 📌 **la SOLA settimana 15-22/08 vale +0,35 di Sharpe
|
||||
hold-out** (rally ETH con SKH01 long): il numero e' fragile, coerente con §2. Il test ora
|
||||
**taglia il feed al 02/07** e verifica la riproduzione stretta (±0,02): si rompe solo se
|
||||
cambiano codice o dato sulla finestra congelata. Suite: **751 passati, 0 falliti** (26/08).
|
||||
cambiano codice o dato sulla finestra congelata. Suite: **775 passati, 0 falliti** (26/08).
|
||||
10. ⚠️ **Nessuna tabella pubblicata prima del 22/08 contiene fisco E funding insieme.** L'unica
|
||||
congiunta e' quella in `30-piano-capitale-fisco.md` — le altre vanno lette con lo sconto.
|
||||
11. 🚨 **Le credenziali Deribit esistono SOLO dentro il gateway** (`cerbero-mcp`): in locale c'e'
|
||||
@@ -466,7 +476,7 @@ uv run python scripts/live/trades_db.py --report # stato libro d
|
||||
uv run python scripts/live/trades_db.py --reconcile # incrocio delle 3 fonti sui fill
|
||||
uv run python scripts/live/journal.py # voce del giorno (numeri + lettura)
|
||||
uv run python scripts/live/analista.py --secco # analisi del giorno, senza salvare
|
||||
uv run pytest # test (751, tutti verdi dal 26/08)
|
||||
uv run pytest # test (775, tutti verdi dal 26/08)
|
||||
```
|
||||
|
||||
```python
|
||||
|
||||
+9
-1
@@ -9,5 +9,13 @@
|
||||
"_nota_stale": "Staleness-gate (2026-07-25): se l'ultima barra del feed certificato e' piu' vecchia di max_data_age_days, book_execute NON invia ordini e allerta su Telegram. Il 2026-07-14 il book compro' ETH con il feed fermo da 6 giorni (conto online e posizione leggibile -> gli altri due gate non scattavano). Follow-up raccomandato nel diario 2026-07-15-feed-freeze, ora cablato.",
|
||||
"max_data_age_days": 2,
|
||||
"_nota_skh_feed": "Freschezza del feed 5m usato per il segnale SKH01 (2026-07-26). fresh_5m ricade sul feed certificato IN SILENZIO se il fetch pubblico Deribit fallisce, e il certificato si rigenera 1x/giorno: senza controllo la latenza d'uscita di SKH01 passa da ~1h a ~1 giorno senza segnalazione. Sopra soglia book_execute ALLERTA e NON blocca (bloccare fermerebbe anche TP01, nettato sullo stesso strumento, per un guasto di rete). Diario 2026-07-26-t1-esecuzione-skh-live.md.",
|
||||
"skh_feed_max_age_min": 30
|
||||
"skh_feed_max_age_min": 30,
|
||||
"_nota_usde": "Collaterale USDE a rendimento (test di eligibilita' 2026-08-26: 500 USDE, verdetto entro il 29/08 — diario 2026-08-26-usde-analisi). L'autorita' che LEGGE questa sezione e' src/live/usde.py; la usano shadow._collaterale_usde (equity oraria del book) e scripts/live/usde_watch.py (sorveglianza giornaliera 12:35 UTC). Criteri delle soglie, dichiarati (P6): depeg_warn 0.99 = fuori dalla banda operativa dello spot (~3 bps) e oltre il clamp +-0.5% per fonte dell'indice usde_usdc — a quel prezzo non e' rumore di book; depeg_crit 0.95 = meta' del buffer di haircut (10%) consumata. quota_max_frac 0.50 = tetto di ALLERTA sulla quota USDE/equity totale (N4: la quota e' l'unica leva contro il rischio emittente, -100% = 25 anni di resa, non recuperabile); la quota EFFETTIVA la decide l'operatore, e vale anche per deriva PASSIVA (il libro perde -> la quota sale da sola). haircut 0.10 verificato sul venue il 26/08 (USDC 0%): il 90% dell'USDE fa margine.",
|
||||
"usde": {
|
||||
"index_name": "usde_usdc",
|
||||
"haircut": 0.10,
|
||||
"quota_max_frac": 0.50,
|
||||
"depeg_warn": 0.99,
|
||||
"depeg_crit": 0.95
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,3 +72,34 @@ al cross-collateral: se la prima conversione fosse stata fatta a quota vera, l'e
|
||||
sarebbe costato caro. I test piccoli esistono per questo.*
|
||||
|
||||
*Esito eligibilita': (da scrivere entro il 29/08)*
|
||||
|
||||
## STRUTTURA (stesso giorno, ~14:20 UTC — "struttura il progetto per gestire subito usde")
|
||||
|
||||
La patch d'emergenza delle 13:07 e' diventata struttura di prima classe:
|
||||
|
||||
- **`config/live.json` sezione `usde`** — l'unica autorita' sui parametri (P1): `index_name`,
|
||||
`haircut` 0.10, `quota_max_frac` 0.50 (tetto di ALLERTA — la quota effettiva la decide
|
||||
l'operatore), `depeg_warn` 0.99 / `depeg_crit` 0.95 coi **criteri dichiarati** in `_nota_usde`
|
||||
(P6: warn = fuori dalla banda spot ~3 bps e oltre il clamp ±0,5% per fonte dell'indice;
|
||||
crit = meta' del buffer di haircut consumata).
|
||||
- **`src/live/usde.py`** — modulo unico per config, catena di prezzo (indice pubblico → ticker
|
||||
spot → 1,0 dichiarato) e valutazione `valuta()` PURA. `shadow._collaterale_usde` ora DERIVA
|
||||
da qui (niente ridichiarazioni); i 6 test di shadow passano invariati.
|
||||
- **`scripts/live/usde_watch.py`** + `cron_usde.sh` alle **12:35 UTC** (dopo la finestra reward
|
||||
~12:00, fuori da :00/:25/:47) — sola lettura: (1) **reward per DELTA di equity al netto dei
|
||||
trade spot** (il gateway non espone il Transaction Log; un delta con trade nel mezzo si
|
||||
dichiara, trade illeggibili → delta NON attribuito, P12); (2) **depeg**: 🚨 solo sotto crit
|
||||
e ripetuto, ⚠️ warn/quota/BLIND solo alla transizione (P9); (3) **quota sul totale**, che
|
||||
allerta anche per deriva PASSIVA (il libro perde → la quota sale da sola, N4). Applica il
|
||||
**verdetto pre-registrato** (≥1 reward entro il 29/08 → IDONEO; zero → riconvertire) invece
|
||||
di lasciarlo a un promemoria. Limite dichiarato: cadenza giornaliera, un depeg intraday puo'
|
||||
sfuggire all'ALLERTA — ma non al SIZING, che legge l'indice a ogni giro orario del book.
|
||||
- **Serie `data/live/usde_watch.jsonl`** — dentro il perimetro di backup, `ts` in ms epoch →
|
||||
sorvegliata da **`monitor_health`** (max_age 30h, `open_labeled=False`): un watch fermo
|
||||
spegnerebbe gli allarmi depeg e il silenzio si leggerebbe come "va tutto bene" (P5).
|
||||
- **Baseline registrata alle 14:17:52Z**: 500 USDE @ 1,0000 (indice) = $500,00 · quota 24,3%
|
||||
(tetto 50%) · margine utilizzabile ~$2.008 · verdetto IN_ATTESA. E' la lettura che rende
|
||||
misurabile il delta del 28/08.
|
||||
- **Test: 775 passati** (+17 nuovi in `tests/test_usde_watch.py`: valutazione condivisa,
|
||||
rilevatore reward, verdetto — incluso che NON anticipa la scadenza del 29/08 —, condizioni
|
||||
di allerta, `px=None` che non finge un depeg).
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# Sorveglianza collaterale USDE (reward/depeg/quota) — v2.0.0+. GIORNALIERA alle 12:35 UTC:
|
||||
# DOPO la finestra reward Deribit (~12:00), fuori dal minuto :00 (rate-limit per-IP, misura
|
||||
# 2026-07-30), dal :25 (catena opzioni) e dal :47 (book). Sola lettura, mai blocca nulla.
|
||||
export PATH="/home/adriano/.local/bin:$PATH"
|
||||
cd /opt/docker/PythagorasGoal || exit 1
|
||||
mkdir -p logs
|
||||
{
|
||||
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_usde ====="
|
||||
uv run python scripts/live/usde_watch.py --quiet || true
|
||||
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||
} >> logs/cron_usde.log 2>&1
|
||||
Executable
+258
@@ -0,0 +1,258 @@
|
||||
#!/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<crit, ripetuto finche'
|
||||
attivo; ⚠️ per warn/quota/BLIND SOLO alla transizione (il giorno che compaiono); 📌 per il
|
||||
verdetto IDONEO. BLIND = conto non leggibile, registrato e detto (P5: "non vedo" non e'
|
||||
"va tutto bene"). La serie data/live/usde_watch.jsonl e' dentro il perimetro di backup ed
|
||||
e' sorvegliata da monitor_health (un watch fermo = niente allarmi depeg, e il silenzio
|
||||
si legge come "va tutto bene").
|
||||
|
||||
uv run python scripts/live/usde_watch.py # report completo
|
||||
uv run python scripts/live/usde_watch.py --quiet # stampa/allerta solo transizioni
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.live import usde as U # noqa: E402
|
||||
from src.live.notifier import notify # noqa: E402
|
||||
|
||||
STATE = ROOT / "data" / "live" / "usde_watch.jsonl"
|
||||
EPS_REWARD = 1e-3 # USDE: sotto e' rumore di lettura; il reward atteso a $500 e' ~0.055/giorno
|
||||
|
||||
# --- finestra di eligibilita', DALLA REGOLA PRE-REGISTRATA (diario 2026-08-26-usde-analisi) ---
|
||||
FILL_TS = "2026-08-26T13:04:32Z" # conversione eseguita: 500 USDE @ 1.0003
|
||||
VERDETTO_DOPO = "2026-08-29T12:30:00Z" # ultima finestra reward del 29/08 conclusa
|
||||
|
||||
|
||||
def _parse(ts: str) -> 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())
|
||||
@@ -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}"
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Il collaterale USDE ha tre sorveglianze (reward/depeg/quota) e una valutazione condivisa.
|
||||
|
||||
Blindano: (a) le proprieta' della valutazione in src/live/usde.py (le stesse gia' provate
|
||||
end-to-end in test_shadow_usde.py, qui sulla funzione PURA che shadow e usde_watch condividono);
|
||||
(b) il rilevatore di reward per delta — che NON inventa attribuzioni quando i trade non sono
|
||||
leggibili (P12); (c) il verdetto di eligibilita', che applica la regola pre-registrata il 26/08
|
||||
PRIMA dell'esito; (d) le condizioni di allerta con le soglie DERIVATE dalla config (P1)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "live"))
|
||||
|
||||
import usde_watch as W # noqa: E402
|
||||
from src.live import usde as U # noqa: E402
|
||||
|
||||
CFG = {"haircut": 0.10, "quota_max_frac": 0.50, "depeg_warn": 0.99, "depeg_crit": 0.95}
|
||||
|
||||
|
||||
# ------------------------------ valutazione (pura, condivisa) ------------------------------
|
||||
|
||||
def test_valuta_depeg_passa_nel_sizing():
|
||||
usd, nota = U.valuta(500.0, 0.97)
|
||||
assert abs(usd - 485.0) < 1e-9 and "0.9700" in nota
|
||||
|
||||
|
||||
def test_valuta_sopra_la_pari_clampa():
|
||||
usd, _ = U.valuta(500.0, 1.02)
|
||||
assert abs(usd - 500.0) < 1e-9
|
||||
|
||||
|
||||
def test_valuta_prezzo_illeggibile_e_1_dichiarato_mai_0():
|
||||
usd, nota = U.valuta(500.0, None)
|
||||
assert abs(usd - 500.0) < 1e-9 and "non leggibile" in nota
|
||||
|
||||
|
||||
def test_cfg_viene_dalla_config_di_produzione():
|
||||
c = U.cfg()
|
||||
assert c["fonte"] == "config/live.json" # la sezione esiste davvero in config
|
||||
assert 0 < c["depeg_crit"] < c["depeg_warn"] < 1.0
|
||||
assert 0 < c["quota_max_frac"] <= 1.0
|
||||
|
||||
|
||||
# ------------------------------ rilevatore di reward ------------------------------
|
||||
|
||||
def test_baseline_senza_prev_non_attribuisce_nulla():
|
||||
an = W.analizza(None, 500.0, 0.0)
|
||||
assert an["delta"] is None and not an["reward_rilevato"]
|
||||
|
||||
|
||||
def test_reward_rilevato_dal_delta_senza_trade():
|
||||
an = W.analizza({"eq_usde": 500.0}, 500.0548, 0.0)
|
||||
assert an["reward_rilevato"] and abs(an["reward_stimato"] - 0.0548) < 1e-9
|
||||
assert an["con_trade"] is False
|
||||
|
||||
|
||||
def test_delta_con_trade_nel_mezzo_si_netta_e_si_dichiara():
|
||||
# comprati altri 100 USDE + reward 0.05: il reward emerge al netto, ma con_trade lo dice
|
||||
an = W.analizza({"eq_usde": 500.0}, 600.05, 100.0)
|
||||
assert an["reward_rilevato"] and abs(an["reward_stimato"] - 0.05) < 1e-9
|
||||
assert an["con_trade"] is True
|
||||
|
||||
|
||||
def test_trade_illeggibili_il_delta_NON_si_attribuisce():
|
||||
# P12: meglio un giorno di latenza che un reward inventato
|
||||
an = W.analizza({"eq_usde": 500.0}, 500.0548, None)
|
||||
assert an["delta"] is not None and an["reward_stimato"] is None
|
||||
assert not an["reward_rilevato"]
|
||||
|
||||
|
||||
def test_rumore_di_lettura_sotto_eps_non_e_un_reward():
|
||||
an = W.analizza({"eq_usde": 500.0}, 500.0000001, 0.0)
|
||||
assert not an["reward_rilevato"]
|
||||
|
||||
|
||||
# ------------------------------ verdetto (regola pre-registrata 26/08) ------------------------------
|
||||
|
||||
def _rec(**kw):
|
||||
base = dict(reward_rilevato=False, data="2026-08-27T12:35:00Z")
|
||||
base.update(kw)
|
||||
return base
|
||||
|
||||
|
||||
def test_verdetto_in_attesa_prima_della_scadenza():
|
||||
now = datetime(2026, 8, 28, 12, 35, tzinfo=timezone.utc)
|
||||
v, _ = W.verdetto([_rec()], now)
|
||||
assert v == "IN_ATTESA"
|
||||
|
||||
|
||||
def test_verdetto_idoneo_al_primo_reward():
|
||||
now = datetime(2026, 8, 28, 12, 35, tzinfo=timezone.utc)
|
||||
v, motivo = W.verdetto([_rec(), _rec(reward_rilevato=True, reward_stimato=0.0548,
|
||||
data="2026-08-28T12:35:00Z")], now)
|
||||
assert v == "IDONEO" and "2026-08-28" in motivo
|
||||
|
||||
|
||||
def test_verdetto_non_idoneo_solo_DOPO_la_finestra_del_29():
|
||||
prima = datetime(2026, 8, 29, 12, 0, tzinfo=timezone.utc)
|
||||
dopo = datetime(2026, 8, 29, 12, 35, tzinfo=timezone.utc)
|
||||
assert W.verdetto([_rec()], prima)[0] == "IN_ATTESA" # anticiparlo = selezione sull'esito
|
||||
assert W.verdetto([_rec()], dopo)[0] == "NON_IDONEO"
|
||||
|
||||
|
||||
def test_un_reward_gia_visto_resta_idoneo_anche_dopo_la_scadenza():
|
||||
dopo = datetime(2026, 9, 15, 0, 0, tzinfo=timezone.utc)
|
||||
v, _ = W.verdetto([_rec(reward_rilevato=True, reward_stimato=0.05)], dopo)
|
||||
assert v == "IDONEO"
|
||||
|
||||
|
||||
# ------------------------------ condizioni di allerta ------------------------------
|
||||
|
||||
def test_depeg_warn_e_crit_dalle_soglie_di_config():
|
||||
assert W.condizioni({"stato": "OK", "px": 0.985, "quota": 0.2}, CFG) == {"DEPEG_WARN"}
|
||||
assert W.condizioni({"stato": "OK", "px": 0.94, "quota": 0.2}, CFG) == {"DEPEG_CRIT"}
|
||||
assert W.condizioni({"stato": "OK", "px": 0.9999, "quota": 0.2}, CFG) == set()
|
||||
|
||||
|
||||
def test_quota_sopra_il_tetto_allerta_anche_per_deriva_passiva():
|
||||
assert W.condizioni({"stato": "OK", "px": 1.0, "quota": 0.51}, CFG) == {"QUOTA_OVER"}
|
||||
|
||||
|
||||
def test_blind_e_una_condizione_non_un_va_tutto_bene():
|
||||
assert W.condizioni({"stato": "BLIND", "px": None, "quota": None}, CFG) == {"BLIND"}
|
||||
|
||||
|
||||
def test_px_none_non_finge_un_depeg():
|
||||
# indice illeggibile: la valutazione degrada a 1.0 dichiarato, l'allerta depeg NON scatta
|
||||
assert W.condizioni({"stato": "OK", "px": None, "quota": 0.2}, CFG) == set()
|
||||
|
||||
|
||||
# ------------------------------ persistenza jsonl ------------------------------
|
||||
|
||||
def test_leggi_jsonl_roundtrip(tmp_path):
|
||||
import json
|
||||
p = tmp_path / "usde_watch.jsonl"
|
||||
p.write_text(json.dumps({"ts": 1, "eq_usde": 500.0}) + "\n"
|
||||
+ json.dumps({"ts": 2, "eq_usde": 500.05}) + "\n")
|
||||
rows = W.leggi(p)
|
||||
assert len(rows) == 2 and rows[-1]["eq_usde"] == 500.05
|
||||
Reference in New Issue
Block a user