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:
@@ -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())
|
||||
|
||||
+36
-2
@@ -15,7 +15,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from src.live.deribit import DeribitRead, notional_to_amount, quantize_price
|
||||
from src.live.deribit import DeribitRead, disaster_stop_price, notional_to_amount, quantize_price
|
||||
|
||||
# Conto USDC -> perp LINEARE USDC (amount in base-coin). MAX_AMOUNT = TETTO HARD anti-fat-finger
|
||||
# (~$630/$430 su un conto ~$600): backstop sopra il sizing di TP01, non il sizing operativo (quello
|
||||
@@ -148,4 +148,38 @@ class DeribitTrader(DeribitRead):
|
||||
opp = "sell" if side_held == "buy" else "buy"
|
||||
return self._submit(instrument, opp, amount, reduce_only=True, label=label,
|
||||
order_type="stop_market", price=quantize_price(instrument, stop_price))
|
||||
# trade_history e' ereditato da DeribitRead (read-only)
|
||||
|
||||
def cancel_order(self, order_id: str) -> dict:
|
||||
return self._unwrap(self._post("/mcp-deribit/tools/cancel_order", {"order_id": order_id})) or {}
|
||||
|
||||
DISASTER_LABEL = "tp01-disaster"
|
||||
|
||||
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)."""
|
||||
pos = self.position_usd(instrument)
|
||||
brackets = [o for o in self.open_orders(instrument) if (o.get("label") or "") == self.DISASTER_LABEL]
|
||||
if abs(pos) < FLAT_USD:
|
||||
for o in brackets:
|
||||
self.cancel_order(o.get("order_id"))
|
||||
return {"state": "flat", "cancelled": len(brackets)}
|
||||
mark = self.mark_price(instrument)
|
||||
long = pos > 0
|
||||
want_amount = notional_to_amount(instrument, abs(pos), price=mark)
|
||||
want_stop = disaster_stop_price(instrument, mark, sl_pct, long=long)
|
||||
if len(brackets) == 1:
|
||||
o = brackets[0]
|
||||
amt = float(o.get("amount") or 0)
|
||||
stp = float(o.get("trigger_price") or o.get("stop_price") or o.get("price") or 0)
|
||||
if want_amount and abs(amt - want_amount) < want_amount * 0.1 and stp > 0 \
|
||||
and abs(stp - want_stop) / want_stop < 0.05:
|
||||
return {"state": "ok", "stop": stp, "amount": amt}
|
||||
for o in brackets: # incoerente o multipli -> ricostruisci UN bracket
|
||||
self.cancel_order(o.get("order_id"))
|
||||
f = self.place_disaster_sl(instrument, "buy" if long else "sell", want_amount, want_stop,
|
||||
label=self.DISASTER_LABEL)
|
||||
return {"state": "placed" if f.verified else "place-failed", "stop": want_stop,
|
||||
"amount": want_amount, "notes": f.notes}
|
||||
# trade_history / open_orders ereditati da DeribitRead (read-only)
|
||||
|
||||
Reference in New Issue
Block a user