live/deribit,execution: open_orders non ingoia gli errori; ensure_disaster_sl -> unknown (#24)

open_orders solleva RuntimeError se 'all' o 'trigger_all' falliscono, invece di ritornare []
(un bracket disaster-SL perso su 'trigger_all' e' lo stesso difetto di una posizione persa su
'all'). ensure_disaster_sl avvolge la lettura di posizione+open_orders in try/except: su
eccezione ritorna {"state": "unknown", "notes": ...} senza cancellare ne' piazzare, distinto
da "naked" (protezione tolta e non rimessa). shadow.shadow_report chiama open_orders gia'
dentro try/except (non toccato).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 13:40:50 +00:00
parent f79fb54221
commit 7c72308852
3 changed files with 96 additions and 13 deletions
+13 -7
View File
@@ -262,17 +262,23 @@ class DeribitRead:
def open_orders(self, instrument: str) -> list[dict]:
"""Ordini APERTI su `instrument` (limit resting + trigger). Deribit puo' omettere i trigger
untriggered da type='all' -> interroga anche 'trigger_all' e fa merge per order_id."""
untriggered da type='all' -> interroga anche 'trigger_all' e fa merge per order_id.
⚠️ Se UNA delle due chiamate solleva, NON si ingoia (#24): prima ritornava [] anche a meta'
letta, e un bracket disaster-SL perso su 'trigger_all' e' lo stesso difetto di una posizione
persa su 'all' — il chiamante (ensure_disaster_sl) deve saperlo, non decidere su dati a
meta'. Il chiamante che vuole restare in piedi comunque (shadow.shadow_report) si mette il
try/except da solo."""
cur = _CONTRACT[instrument]["settle"]
seen: dict = {}
for typ in ("all", "trigger_all"):
try:
out = self._unwrap(self._post("/mcp-deribit/tools/get_open_orders",
{"currency": cur, "type": typ}))
lst = out if isinstance(out, list) else (out.get("orders", []) if isinstance(out, dict) else [])
for o in lst:
if o.get("instrument_name") == instrument or o.get("instrument") == instrument:
seen[o.get("order_id")] = o
except Exception:
pass
except Exception as e: # noqa: BLE001 — si registra il tipo (P3), non si ingoia (#24)
raise RuntimeError(f"open_orders illeggibile: {type(e).__name__}: {e}") from e
lst = out if isinstance(out, list) else (out.get("orders", []) if isinstance(out, dict) else [])
for o in lst:
if o.get("instrument_name") == instrument or o.get("instrument") == instrument:
seen[o.get("order_id")] = o
return list(seen.values())
+19 -6
View File
@@ -215,10 +215,20 @@ class DeribitTrader(DeribitRead):
return self._unwrap(self._post("/mcp-deribit/tools/cancel_order", {"order_id": order_id})) or {}
def ensure_disaster_sl(self, instrument: str, sl_pct: float) -> dict:
"""Garantisce UN disaster-SL coerente con la posizione (lifecycle completo, idempotente):
- flat -> cancella eventuali bracket orfani;
- long -> assicura UN solo STOP_MARKET reduce_only a ~-sl_pct, size = posizione;
- gia' coerente (1 bracket, amount~=, stop entro 5%) -> lascia com'e' (niente churn/gap).
"""Garantisce UN disaster-SL coerente con la posizione (lifecycle completo, idempotente).
Stati possibili (`dict["state"]`):
- `"flat"` -> nessuna posizione: cancella eventuali bracket orfani;
- `"ok"` -> gia' coerente (1 bracket, amount~=, stop entro 5%): lascia com'e';
- `"placed"` -> ricostruito (cancellato l'incoerente e ripiazzato) al primo o secondo
tentativo, e il piazzamento e' verificato;
- `"place-failed"` -> il ripiazzamento e' partito ma NON e' verificato (state/trade non
confermano): l'ordine potrebbe non essere a book;
- `"naked"` -> il bracket incoerente e' stato cancellato e il ripiazzamento e'
fallito (eccezione) 2 volte di fila: la posizione e' SENZA stop;
- `"unknown"` -> posizione o open_orders illeggibili (#24): NON cancella ne' piazza —
decidere alla cieca su meta' dei dati e' peggio che aspettare il giro
dopo. `notes` porta tipo ed errore della lettura fallita.
⚠️ LA FINESTRA SCOPERTA (riparata 2026-08-25). Il ramo di ricostruzione **cancella prima
e ripiazza dopo**: fra le due chiamate la posizione e' senza alcuno stop. Finche' il
@@ -237,8 +247,11 @@ class DeribitTrader(DeribitRead):
non basta per cambiare il ciclo di vita dei bracket su un percorso con soldi veri senza
misurarlo. Riparato il silenzio, non toccata la sequenza.
"""
pos = self.position_usd(instrument)
brackets = [o for o in self.open_orders(instrument) if (o.get("label") or "") == DISASTER_LABEL]
try:
pos = self.position_usd(instrument)
brackets = [o for o in self.open_orders(instrument) if (o.get("label") or "") == DISASTER_LABEL]
except Exception as e: # noqa: BLE001 — si registra il motivo (P3); non si cancella ne' piazza (#24)
return {"state": "unknown", "notes": f"{type(e).__name__}: {e}"}
if abs(pos) < FLAT_USD:
for o in brackets:
self.cancel_order(o.get("order_id"))