"""Le due riparazioni del 2026-08-25 sul percorso con soldi veri, provate contro il codice che dovrebbe eseguirle (P15) invece che discusse: A. **La finestra scoperta del disaster-SL.** `ensure_disaster_sl` cancella i bracket incoerenti PRIMA di ripiazzarne uno: se il venue cade in mezzo, la posizione resta senza alcuno stop. Prima l'eccezione risaliva fino a `main()`, e il guasto peggiore (posizione SCOPERTA) aveva la stessa faccia di un errore qualunque. Ora ha uno stato suo: `naked`. B. **L'isolamento per asset.** Il 2026-07-21 alle 09:00 UTC un 502 dentro `ensure_disaster_sl` su BTC ha ucciso il giro intero: ETH non e' stato nemmeno guardato — niente ribilancio e, soprattutto, nessuna verifica della sua protezione. Nessun test tocca la rete. """ import importlib.util import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(PROJECT_ROOT)) from src.live.execution import DISASTER_LABEL, DeribitTrader, Fill def _fresh_bar() -> str: import pandas as _pd return str(_pd.Timestamp.now(tz="UTC").normalize().date()) # --------------------------------------------------------------------------- # A. Il disaster-SL: `naked` esiste, ed e' distinto da `place-failed`. # --------------------------------------------------------------------------- class _FakeSL(DeribitTrader): """DeribitTrader senza rete: si controlla quante volte il piazzamento fallisce.""" def __init__(self, pos_usd, brackets, fallimenti_piazzamento=0, verified=True): self._pos = float(pos_usd) self._brackets = list(brackets) self._da_fallire = int(fallimenti_piazzamento) self._verified = verified self.cancellati = [] self.piazzamenti = 0 def position_usd(self, instrument): return self._pos def open_orders(self, instrument): return self._brackets def cancel_order(self, order_id): self.cancellati.append(order_id) return {} def mark_price(self, instrument): return 80000.0 def place_disaster_sl(self, instrument, side_held, amount, stop_price, label=DISASTER_LABEL): self.piazzamenti += 1 if self._da_fallire > 0: self._da_fallire -= 1 raise RuntimeError("502 Server Error: Bad Gateway") return Fill(instrument=instrument, side="sell", amount=amount, filled=amount, price=stop_price, fee_usdc=0.0, order_id="sl1", state="open", verified=self._verified, notes="") def _bracket_incoerente(): """Un bracket con size sbagliata -> forza il ramo 'cancella e ricostruisci'.""" return [{"order_id": "vecchio", "label": DISASTER_LABEL, "amount": 0.0001, "trigger_price": 1.0}] def test_ripiazzamento_ok_al_primo_colpo_resta_placed(): """Il percorso sano non deve essere cambiato dalla riparazione.""" t = _FakeSL(pos_usd=112.0, brackets=_bracket_incoerente()) ds = t.ensure_disaster_sl("BTC_USDC-PERPETUAL", 0.30) assert ds["state"] == "placed" assert t.cancellati == ["vecchio"] and t.piazzamenti == 1 def test_un_fallimento_isolato_viene_ritentato_e_la_posizione_resta_protetta(): """La posizione e' scoperta fra il cancel e il place: si riprova SUBITO, non al giro dopo.""" t = _FakeSL(pos_usd=112.0, brackets=_bracket_incoerente(), fallimenti_piazzamento=1) ds = t.ensure_disaster_sl("BTC_USDC-PERPETUAL", 0.30) assert ds["state"] == "placed" # il secondo tentativo ha funzionato assert t.piazzamenti == 2 def test_due_fallimenti_lasciano_lo_stato_naked_non_una_eccezione(): """IL TEST CHE CONTA. Prima qui volava un'eccezione e moriva il giro (e l'altro asset). Ora: stato `naked`, con dentro il fatto che i bracket erano gia' stati cancellati.""" t = _FakeSL(pos_usd=112.0, brackets=_bracket_incoerente(), fallimenti_piazzamento=2) ds = t.ensure_disaster_sl("BTC_USDC-PERPETUAL", 0.30) assert ds["state"] == "naked" assert ds["cancelled"] == 1 # la protezione era stata TOLTA assert "502" in ds["notes"] # il motivo si registra (P3) assert t.piazzamenti == 2 def test_naked_e_place_failed_sono_guasti_DIVERSI(): """P5: distinguere guasti diversi anche quando l'azione e' la stessa. `place-failed` = l'ordine e' partito ma non e' verificato; `naked` = la protezione e' stata rimossa e non rimessa.""" t = _FakeSL(pos_usd=112.0, brackets=_bracket_incoerente(), verified=False) assert t.ensure_disaster_sl("BTC_USDC-PERPETUAL", 0.30)["state"] == "place-failed" t2 = _FakeSL(pos_usd=112.0, brackets=_bracket_incoerente(), fallimenti_piazzamento=2) assert t2.ensure_disaster_sl("BTC_USDC-PERPETUAL", 0.30)["state"] == "naked" def test_posizione_flat_non_puo_diventare_naked(): """A libro flat non c'e' niente da proteggere: il ramo non deve nemmeno provarci.""" t = _FakeSL(pos_usd=0.0, brackets=_bracket_incoerente(), fallimenti_piazzamento=2) ds = t.ensure_disaster_sl("BTC_USDC-PERPETUAL", 0.30) assert ds["state"] == "flat" and t.piazzamenti == 0 # --------------------------------------------------------------------------- # B. Isolamento per asset dentro book_execute._run(). # --------------------------------------------------------------------------- def _carica_book_execute(): spec = importlib.util.spec_from_file_location( "book_execute", PROJECT_ROOT / "scripts/live/book_execute.py") mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def _report_due_asset(): def _asset(nome, inst): return dict(asset=nome, instrument=inst, tp_frac=0.45, skh_sign=0, skh_state="flat", net_target=113.0, position_usd=112.0, mark=80000.0, order=None) return dict(last_data=_fresh_bar(), online=True, real_equity=667.0, equity=667.0, eq_basis="test", cap_per_asset=334.0, assets=[_asset("BTC", "BTC_USDC-PERPETUAL"), _asset("ETH", "ETH_USDC-PERPETUAL")], orders=[]) class _DiagnosiFinta: verdetto = "GATEWAY" perche = "l'API pubblica Deribit risponde: il guasto e' nel nostro gateway" atteso = False prova = "prova" gravita = "🛑" riparabile_da_noi = True def riga(self): return f"{self.verdetto} — {self.perche}" def _diagnosi_finta(*a, **k): return _DiagnosiFinta() class _TraderCheEsplodeSuBTC: """Riproduce il 2026-07-21: 502 dentro ensure_disaster_sl su BTC, ETH sano.""" def __init__(self): self.visti = [] def rebalance_signed(self, *a, **k): return [] def position_usd(self, instrument): return 112.0 def ensure_disaster_sl(self, instrument, sl_pct): self.visti.append(instrument) if instrument.startswith("BTC"): raise RuntimeError("502 Server Error: Bad Gateway for url: .../get_positions") return {"state": "ok", "stop": 1755.8, "amount": 0.06} def test_un_asset_che_esplode_non_impedisce_all_altro_di_essere_protetto(monkeypatch, capsys): """IL TEST CHE CONTA. Prima ETH non veniva nemmeno guardato.""" mod = _carica_book_execute() trader = _TraderCheEsplodeSuBTC() alerts = [] monkeypatch.setattr(mod, "book_report", lambda **k: _report_due_asset()) monkeypatch.setattr(mod, "notify", lambda t, d=None: alerts.append((t, d))) monkeypatch.setattr(mod, "DeribitTrader", lambda *a, **k: trader) monkeypatch.setattr(mod, "diagnose", _diagnosi_finta) monkeypatch.setattr(mod, "load_config", lambda: dict(execution_enabled=True, min_order_usd=5.0, disaster_sl_pct=0.30, max_data_age_days=2.0, skh_feed_max_age_min=30.0)) monkeypatch.setattr(sys, "argv", ["book_execute.py", "--execute"]) try: mod._run() except SystemExit as e: assert e.code == 2 # giro DEGRADATO, dichiarato all'uscita out = capsys.readouterr().out assert trader.visti == ["BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"] # ETH e' stato guardato assert "PASSO ALL'ASSET SUCCESSIVO" in out assert "asset falliti in questo giro: BTC" in out assert any("asset BTC fallito" in t for t, _ in alerts) def test_posizione_scoperta_esce_con_codice_due_e_allarme_massimo(monkeypatch, capsys): """Una posizione senza stop on-book non e' MAI un evento atteso: gravita' massima sempre.""" mod = _carica_book_execute() class _TraderNaked: def rebalance_signed(self, *a, **k): return [] def position_usd(self, instrument): return 112.0 def ensure_disaster_sl(self, instrument, sl_pct): return {"state": "naked", "stop": 56000.0, "amount": 0.0014, "cancelled": 1, "notes": "bracket cancellati (1) e ripiazzamento fallito 2 volte"} alerts = [] monkeypatch.setattr(mod, "book_report", lambda **k: _report_due_asset()) monkeypatch.setattr(mod, "notify", lambda t, d=None: alerts.append((t, d))) monkeypatch.setattr(mod, "DeribitTrader", lambda *a, **k: _TraderNaked()) monkeypatch.setattr(mod, "diagnose", _diagnosi_finta) monkeypatch.setattr(mod, "load_config", lambda: dict(execution_enabled=True, min_order_usd=5.0, disaster_sl_pct=0.30, max_data_age_days=2.0, skh_feed_max_age_min=30.0)) monkeypatch.setattr(sys, "argv", ["book_execute.py", "--execute"]) uscita = None try: mod._run() except SystemExit as e: uscita = e.code out = capsys.readouterr().out assert uscita == 2 assert "POSIZIONI SCOPERTE" in out titoli = [t for t, _ in alerts] assert any("POSIZIONE SCOPERTA" in t and t.startswith("🚨") for t in titoli) assert sum("POSIZIONE SCOPERTA" in t for t in titoli) == 2 # uno per asset, nessuno perso