merge lavoro/goldrake-u9 (secondo giro 27/09)

This commit is contained in:
2026-09-27 15:40:42 +00:00
10 changed files with 217 additions and 87 deletions
+61 -12
View File
@@ -80,7 +80,9 @@ CONVERT = ROOT / "scripts" / "live" / "usde_convert.py"
# dell'ordine imposto (revisione fable 10/09: a margine == preavviso ogni 🚨 era seguito da un ⚠️).
RIPRISTINO_MIN_ORE = 6.0 # un tentativo di conversione (in qualunque verso) ogni 6 ore
RITENTATIVO_FALLITO_ORE = 24.0 # da ECCEDENTE, dopo un fallimento: un tentativo al giorno
TIMEOUT_CONVERT_S = 900 # usde_convert spazia gli ordini di 12s: 60 ordini = 12 min
TIMEOUT_CONVERT_S = 900 # usde_convert: 12s di pausa fra ordini + le GET per giro (~14s/giro
# coi 12s di pausa); 60 ordini ~= 840s: il tetto e' stretto sul caso
# massimo, non largo come "60 ordini = 12 min" lasciava credere (#35 B10)
def leggi(path: Path = STATE) -> list[dict]:
@@ -158,17 +160,56 @@ def puo_riconvertire(prev_records: list[dict], now_ms: int, execution_enabled: b
return True, ""
def _leggi_quota_usde() -> float | None:
"""Quota USDE sul totale, STESSA formula di usde_watch.py:275 (usd/(usd+eq_usdc)), stessi
mattoni di main() qui sotto (_safe_client, account_summary, U.prezzo, U.valuta): nessuna
lettura nuova. None se il conto non e' leggibile — chi chiama tratta None come "non lo so",
non come zero (#35 B10)."""
client = _safe_client()
if client is None:
return None
try:
eq_usdc = float(client.account_summary("USDC")["equity"])
eq_usde = float(client.account_summary("USDE").get("equity") or 0)
except Exception:
return None
px, _ = U.prezzo(client)
usd, _ = U.valuta(eq_usde, px)
try:
return round(usd / (usd + eq_usdc), 4)
except ZeroDivisionError:
return None
def riconverti(quota: float, timeout_s: float = TIMEOUT_CONVERT_S) -> dict:
"""Lancia lo script VERO con le sue guardie. Ritorna esito + coda dell'output (P3)."""
"""Lancia lo script VERO con le sue guardie. Ritorna esito + coda dell'output (P3).
Rilegge la quota USDE prima e dopo il subprocess, ANCHE su timeout o eccezione: un rc!=0 con
la quota mossa e' una conversione PARZIALE (fill gia' avvenuti prima del kill a timeout_s),
non "FALLITA" come diceva `ok = rc==0` (#35 B10). `esito` in "ok" | "parziale" | "fallita"* —
"ok" solo per compatibilita' resta anche in `ok` (bool)."""
quota_prima = _leggi_quota_usde()
cmd = [sys.executable, str(CONVERT), "--quota", f"{quota:.4f}", "--esegui"]
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_s, cwd=str(ROOT))
rc = r.returncode
coda = (r.stdout + r.stderr).strip().splitlines()[-12:]
return dict(rc=r.returncode, ok=r.returncode == 0, coda=coda, cmd=" ".join(cmd[1:]))
except subprocess.TimeoutExpired:
return dict(rc=None, ok=False, coda=[f"timeout dopo {timeout_s:.0f}s"], cmd=" ".join(cmd[1:]))
rc, coda = None, [f"timeout dopo {timeout_s:.0f}s"]
except Exception as e: # noqa: BLE001 — si registra, non si ingoia
return dict(rc=None, ok=False, coda=[f"{type(e).__name__}: {e}"], cmd=" ".join(cmd[1:]))
rc, coda = None, [f"{type(e).__name__}: {e}"]
quota_dopo = _leggi_quota_usde()
if rc == 0:
esito = "ok"
elif quota_dopo is None:
esito = "fallita (quota dopo non leggibile)"
else:
mossa = quota_prima is not None and abs(quota_dopo - quota_prima) > 0.005
esito = "parziale" if mossa else "fallita"
return dict(rc=rc, ok=esito == "ok", esito=esito, quota_prima=quota_prima, quota_dopo=quota_dopo,
coda=coda, cmd=" ".join(cmd[1:]))
def _safe_client():
@@ -239,17 +280,23 @@ def main() -> int:
rec["riconversione"] = dict(tentata=False, verso=AZIONE[stato], quota=quota,
motivo=f"--secco: avrei lanciato usde_convert --quota {quota:.4f} --esegui ({AZIONE[stato]})")
else:
esito = riconverti(quota)
rec["riconversione"] = dict(tentata=True, verso=AZIONE[stato], quota=quota, **esito)
ris = riconverti(quota)
rec["riconversione"] = dict(tentata=True, verso=AZIONE[stato], quota=quota, **ris)
# --- allarmi: alla transizione, marcatore scritto con l'esito dell'invio ---
# ⚠️ Telegram e' in parse_mode HTML: un '<' nudo nel testo fa fallire l'invio (successo al
# primo giro vero, 13:53Z del 10/09: «(< 10% del cuscino)»). Qui non si scrive mai '<'.
def _esito_azione(r: dict) -> str:
if r.get("tentata"):
return (f"{r['verso']} " + ("ESEGUITA" if r.get("ok") else "FALLITA")
+ f" (quota bersaglio {r['quota']:.0%})")
return f"{r.get('verso', 'conversione')} NON tentata: {r.get('motivo')}"
if not r.get("tentata"):
return f"{r.get('verso', 'conversione')} NON tentata: {r.get('motivo')}"
esito = r.get("esito", "ok" if r.get("ok") else "fallita")
if esito == "parziale":
qp, qd = r.get("quota_prima"), r.get("quota_dopo")
qp_s = f"{qp:.0%}" if qp is not None else "n/d"
qd_s = f"{qd:.0%}" if qd is not None else "n/d"
return f"{r['verso']} PARZIALE: quota da {qp_s} a {qd_s}"
return (f"{r['verso']} " + ("ESEGUITA" if esito == "ok" else "FALLITA")
+ f" (quota bersaglio {r['quota']:.0%})")
if transizione(prev, stato):
if secco:
@@ -275,7 +322,9 @@ def main() -> int:
elif rec["riconversione"] and rec["riconversione"].get("tentata") and not secco:
# non e' una transizione ma e' un ordine mandato: si dice sempre
r = rec["riconversione"]
rec["allertato"] = notify(f"📌 cuscino USDC: {r['verso']} " + ("eseguita" if r["ok"] else "FALLITA"),
esito = r.get("esito", "ok" if r.get("ok") else "fallita")
verbo = {"ok": "eseguita", "parziale": "PARZIALE"}.get(esito, "FALLITA")
rec["allertato"] = notify(f"📌 cuscino USDC: {r['verso']} {verbo}",
{"quota bersaglio": f"{r['quota']:.0%}", "esito": " | ".join(r["coda"][-3:])})
if rec["allertato"] is False and not secco:
from src.live.notifier import ultimo_errore
+1 -1
View File
@@ -140,7 +140,7 @@ def main() -> int:
print(f"\n {n_ok}/{len(r['protection'])} anni di sinistro superati · "
f"fallimenti consecutivi in corso: {r['consec_fail']}")
if r["alerts"]:
notify("🚨 EDGE WATCH — criterio di kill scattato sul book LIVE",
notify("🚨 EDGE WATCH — criterio di kill scattato sul book (serie backtest dei sleeve, non il P&L eseguito)",
{f"alert {i+1}": a for i, a in enumerate(r["alerts"])})
for a in r["alerts"]:
print(f" ALERT: {a}")