feat(live): disaster-SL on-book con lifecycle completo (idempotente) nel loop di esecuzione

ensure_disaster_sl(): garantisce UN solo STOP_MARKET reduce_only a ~-30% coerente con la posizione,
ad ogni run del loop, per asset:
- flat  -> cancella i bracket orfani;
- long  -> assicura lo stop (size = posizione, prezzo al tick);
- gia' coerente (1 bracket, amount~=, stop entro 5%) -> lascia com'e' (niente churn ne' gap di
  protezione fra cancel e place).

- deribit.py: open_orders (merge type all+trigger_all), disaster_stop_price.
- execution.py: cancel_order + ensure_disaster_sl.
- live_execute.py: gestione bracket ogni run, gated come l'esecuzione. Validato armato: flat ->
  disaster-SL 'flat' (cleanup), zero ordini. Test 28/28.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-20 15:47:30 +00:00
parent 4650aa71a2
commit e5e2d3ec9b
4 changed files with 81 additions and 13 deletions
+23
View File
@@ -68,6 +68,12 @@ def quantize_price(instrument: str, price: float) -> float:
return float(round(price / tick) * Decimal(str(tick)))
def disaster_stop_price(instrument: str, mark: float, sl_pct: float, long: bool = True) -> float:
"""Prezzo del disaster-stop: ~sl_pct SOTTO il mark per un long (SOPRA per uno short), al tick."""
raw = mark * (1 - sl_pct) if long else mark * (1 + sl_pct)
return quantize_price(instrument, raw)
def target_notional_usd(target_fraction: float, weight: float, equity_usd: float) -> float:
"""Notional bersaglio (USD) di un asset = peso nel book * frazione-di-equity TP01 * equity.
Coerente col paper trader (esposizione asset = WEIGHT * target * equity)."""
@@ -165,3 +171,20 @@ class DeribitRead:
out = self._unwrap(self._post("/mcp-deribit/tools/get_trade_history",
{"limit": limit, "instrument_name": instrument}))
return out if isinstance(out, list) else (out.get("trades", []) if isinstance(out, dict) else [])
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."""
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
return list(seen.values())