feat(live): disaster-bracket on-book sui fade reali + alert FEED_OUTAGE + osservabilita' multi-asset
Punti 5-6 dell'improvement-sweep 2026-06-06 (protezione capitale + osservabilita'): Punto 5 — disaster bracket: - ExecutionClient.place_disaster_sl: STOP_MARKET reduce-only a ~-30% dall'ingresso (trigger mark price), piazzato a ogni REAL_OPEN (MR01/MR02/MR07/DIP01) e cancellato in _real_close. Assicurazione outage: il poll-loop in except lascia le posizioni reali senza valutazione exit (ETH gap max storico 33%/1h). In operativita' normale non scatta mai -> 0 costo Sharpe. real_dsl_order_id persistito (resume-safe). Config overrides.execution.disaster_sl_pct (0.30). - NB: set_stop_loss di cerbero-mcp e' un private/edit Deribit (solo ordini APERTI) -> non usabile su market fillati; il bracket e' un trigger order autonomo via place_order(type=stop_market). Cancel di un trigger order risponde 'untriggered' (= successo, verificato testnet: re-cancel -> order_not_found). - Runner: alert Telegram FEED_OUTAGE dopo 5 poll falliti consecutivi (elenco posizioni reali aperte) + notifica RIPRESO con durata. Punto 6 — osservabilita': - in_position nei _save() di TR01/ROT02/TSM01; hourly_report: sezione MULTI-ASSET (book | ultimo flip | freschezza status) — prima i 3 worker erano invisibili (collect() filtra su event/in_position che non emettevano); esclusi dalla tabella IN CORSO (assume entry/bars single-leg). - live_shadow_smoke esteso: scenari C/D SHORT (TP-resting BUY mai esercitato prima) + disaster bracket in tutti gli scenari. Verifiche: 72/72 test; smoke testnet 4 scenari verdi (DSL piazzato/cancellato due lati, zero ordini orfani sul book, conto flat); multi_asset_section renderizza sui dati live. Diario docs/diary/2026-06-07-sweep-fixes.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,7 @@ class StrategyWorker:
|
||||
self.real_entry_notional = 0.0 # USD effettivi esposti all'entrata
|
||||
self.real_order_id = ""
|
||||
self.real_tp_order_id = "" # LIMIT reduce-only resting al TP (persistito per il resume)
|
||||
self.real_dsl_order_id = "" # STOP_MARKET disaster bracket on-book (persistito)
|
||||
self.real_trades = 0
|
||||
self.real_first_notified = False # alert Telegram "esecuzione viva" una tantum
|
||||
|
||||
@@ -126,6 +127,7 @@ class StrategyWorker:
|
||||
self.real_entry_notional = state.get("real_entry_notional", 0.0)
|
||||
self.real_order_id = state.get("real_order_id", "")
|
||||
self.real_tp_order_id = state.get("real_tp_order_id", "")
|
||||
self.real_dsl_order_id = state.get("real_dsl_order_id", "")
|
||||
self.real_trades = state.get("real_trades", 0)
|
||||
self.real_first_notified = state.get("real_first_notified", False)
|
||||
|
||||
@@ -159,6 +161,7 @@ class StrategyWorker:
|
||||
"real_entry_notional": self.real_entry_notional,
|
||||
"real_order_id": self.real_order_id,
|
||||
"real_tp_order_id": self.real_tp_order_id,
|
||||
"real_dsl_order_id": self.real_dsl_order_id,
|
||||
"real_trades": self.real_trades,
|
||||
"real_first_notified": self.real_first_notified,
|
||||
"last_update": datetime.now(timezone.utc).isoformat(),
|
||||
@@ -246,6 +249,7 @@ class StrategyWorker:
|
||||
self._notify("REAL_EXEC_LIVE", data)
|
||||
self.real_first_notified = True
|
||||
self._place_real_tp()
|
||||
self._place_disaster_sl()
|
||||
else:
|
||||
self._log("REAL_OPEN_FAIL", {**data, "note": fill.notes})
|
||||
self._notify("REAL_OPEN_FAIL", {**data, "note": fill.notes})
|
||||
@@ -274,6 +278,35 @@ class StrategyWorker:
|
||||
else:
|
||||
self._log("REAL_TP_FAIL", {**data, "note": rest.notes})
|
||||
|
||||
def _place_disaster_sl(self):
|
||||
"""Disaster-bracket on-book (improvement-sweep 2026-06-06 P1): STOP_MARKET
|
||||
reduce-only LONTANO (executor.disaster_sl_pct, ~30% dall'ingresso) sulla
|
||||
SOLA quota del worker. Pura assicurazione per gli outage (poll-loop fermo
|
||||
= posizione reale senza valutazione exit; ETH gap max storico 33% in 1h):
|
||||
in operativita' normale non scatta mai. Se il piazzamento fallisce si
|
||||
resta senza bracket (come prima del fix) — solo log, non fatale."""
|
||||
self.real_dsl_order_id = ""
|
||||
pct = getattr(self.executor, "disaster_sl_pct", None)
|
||||
if not (pct and self.real_amount > 0 and self.real_entry_price > 0):
|
||||
return
|
||||
stop = self.real_entry_price * (1 - pct if self.real_side == "buy" else 1 + pct)
|
||||
rest = self.executor.place_disaster_sl(self.exec_instrument, self.real_side,
|
||||
self.real_amount, stop,
|
||||
label=self.worker_id)
|
||||
data = {
|
||||
"instrument": self.exec_instrument,
|
||||
"order_id": rest.order_id,
|
||||
"stop": round(stop, 2),
|
||||
"pct": pct,
|
||||
"amount": self.real_amount,
|
||||
"state": rest.order_state,
|
||||
}
|
||||
if rest.verified and rest.order_id:
|
||||
self.real_dsl_order_id = rest.order_id
|
||||
self._log("REAL_DSL_RESTING", data)
|
||||
else:
|
||||
self._log("REAL_DSL_FAIL", {**data, "note": rest.notes})
|
||||
|
||||
def _real_close(self, sim_exit: float, reason: str, sim_pnl: float):
|
||||
"""Chiusura REALE (reduce-only della quota worker) + confronto col sim.
|
||||
|
||||
@@ -288,6 +321,19 @@ class StrategyWorker:
|
||||
from src.live.execution import contract_spec
|
||||
step = contract_spec(self.exec_instrument)["step"]
|
||||
|
||||
# 0) disaster bracket: via dal book PRIMA di chiudere (se la cancel fallisce
|
||||
# lo stop potrebbe essere SCATTATO durante un outage: quota gia' chiusa →
|
||||
# il market reduce-only a valle filla 0 e REAL_CLOSE esce verified=False)
|
||||
# NB: la cancel di un trigger order risponde con lo stato AL MOMENTO della
|
||||
# cancel ('untriggered' = successo, verificato su testnet: il re-cancel da'
|
||||
# order_not_found); 'error' = ordine non piu' in book (probabile trigger).
|
||||
if self.real_dsl_order_id:
|
||||
dres = self.executor.cancel_order(self.real_dsl_order_id)
|
||||
if dres.get("state") not in ("cancelled", "untriggered"):
|
||||
self._log("REAL_DSL_CANCEL_FAIL", {"order_id": self.real_dsl_order_id,
|
||||
"res": dres})
|
||||
self.real_dsl_order_id = ""
|
||||
|
||||
# 1) ordine TP resting: cancella, poi riconcilia i fill (order_id su history)
|
||||
tp_amt, tp_px, tp_fee = 0.0, None, 0.0
|
||||
tp_order_id = self.real_tp_order_id
|
||||
@@ -357,6 +403,7 @@ class StrategyWorker:
|
||||
self.real_entry_notional = 0.0
|
||||
self.real_order_id = ""
|
||||
self.real_tp_order_id = ""
|
||||
self.real_dsl_order_id = ""
|
||||
|
||||
def _close_position(self, current_price: float, reason: str):
|
||||
if not self.in_position:
|
||||
|
||||
Reference in New Issue
Block a user