"""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