feat(live): reconcile resting + orphan single-leg + circuit-breaker venue-lock + FEED_BOOK_GAP

Codice della tornata v1.1.27/28 (gia' in produzione, mai committato):
- reconcile_account: estensione ordini RESTING (FILLED_UNBOOKED/MISSING/STALE,
  caso MR02_BTC: TP fillato di notte scoperto ore dopo) + expected_resting in books
- strategy_worker: orphan_legs su REAL_CLOSE_PARTIAL anche single-leg, persistito
- execution: circuit-breaker su venue-lock admin (stop ordini dopo errori ripetuti)
- runner/hourly_report: alert FEED_BOOK_GAP + timestamp closed trades
- cerbero_client: get_open_orders (merge all + trigger_all)
Test: 12 nuovi, suite completa 126 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-12 20:29:02 +00:00
parent a2d581691a
commit 612f2bfced
11 changed files with 628 additions and 25 deletions
+73
View File
@@ -0,0 +1,73 @@
"""Guard FEED_BOOK_GAP (2026-06-12): il feed candele e il book d'esecuzione possono
divergere (MR02_BTC: fill reale a -443 bps dal sim; wick TP_PHANTOM il caso opposto).
Alert per episodio con isteresi a soglia/2, fail-open su errori di rete."""
import sys
from pathlib import Path
import pandas as pd
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from src.portfolio import runner
class _Client:
def __init__(self, marks):
self._marks = marks
self.calls = 0
def get_ticker_batch(self, instruments):
self.calls += 1
return {"tickers": [{"instrument_name": i, "mark_price": self._marks[i]}
for i in instruments if i in self._marks]}
class _Boom:
def get_ticker_batch(self, instruments):
raise RuntimeError("rete giu'")
INSTR = {"BTC": "BTC_USDC-PERPETUAL"}
def _feed(close):
return {"BTC": pd.DataFrame({"close": [100.0, close]})}
def _events(monkeypatch):
sent = []
monkeypatch.setattr("src.live.telegram_notifier.notify_event",
lambda ev, data: sent.append((ev, data)))
return sent
def test_gap_alerts_once_per_episode(monkeypatch):
sent = _events(monkeypatch)
cl = _Client({"BTC_USDC-PERPETUAL": 60481.0})
alerted = set()
# feed a 63285 vs mark 60481 = il caso MR02 (~464 bps) -> alert, una volta sola
for _ in range(3):
runner._check_feed_book_gap(cl, _feed(63285.5), INSTR, 150.0, alerted)
assert len(sent) == 1 and sent[0][0] == "FEED_BOOK_GAP"
assert sent[0][1]["asset"] == "BTC" and sent[0][1]["gap_bps"] > 400
def test_recovery_with_hysteresis(monkeypatch):
sent = _events(monkeypatch)
cl = _Client({"BTC_USDC-PERPETUAL": 60000.0})
alerted = {"BTC"}
# gap fra soglia/2 e soglia: NESSUN recovery (isteresi)
runner._check_feed_book_gap(cl, _feed(60000 * 1.0090), INSTR, 150.0, alerted)
assert sent == [] and "BTC" in alerted
# gap sotto soglia/2 -> RIENTRATO
runner._check_feed_book_gap(cl, _feed(60010.0), INSTR, 150.0, alerted)
assert len(sent) == 1 and sent[0][1].get("status") == "RIENTRATO"
assert "BTC" not in alerted
def test_fail_open_and_no_instruments(monkeypatch):
sent = _events(monkeypatch)
runner._check_feed_book_gap(_Boom(), _feed(63285.5), INSTR, 150.0, set())
cl = _Client({"BTC_USDC-PERPETUAL": 60481.0})
runner._check_feed_book_gap(cl, {}, INSTR, 150.0, set()) # nessun feed
assert sent == [] and cl.calls == 0