"""Lock dello STALENESS-GATE dell'esecutore live (cablato 2026-07-25). PERCHE'. Il 2026-07-14 alle 14:00 UTC il book ha comprato ETH ($75) con l'ultima barra del feed certificato ferma al 2026-07-08: feed congelato da 6 giorni (diario 2026-07-15-feed-freeze). I due gate esistenti non potevano vederlo — il conto ERA online e la posizione ERA leggibile: proteggono dai problemi di CONTO, non da un feed morto. Il diario raccomandava un alert; qui il gate e' BLOCCANTE, coerente con gli altri due (`non opero a cieco`). Questi test bloccano la soglia e il comportamento del calcolo d'eta'. Non toccano la rete. """ from __future__ import annotations import json import sys from pathlib import Path import pandas as pd import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "live")) book_execute = pytest.importorskip("book_execute") def test_soglia_presente_nel_config_live(): cfg = json.loads((ROOT / "config" / "live.json").read_text()) assert cfg.get("max_data_age_days") == 2, "soglia di staleness assente o cambiata" assert cfg.get("execution_enabled") is True assert cfg.get("disaster_sl_pct") == 0.30 def test_default_sicuro_se_il_config_non_la_indica(): cfg = book_execute.load_config() assert cfg["max_data_age_days"] > 0 def test_eta_barra_fresca_e_sotto_soglia(): oggi = pd.Timestamp.now(tz="UTC").normalize() eta = book_execute._data_age_days(oggi) assert eta is not None and eta < 2.0 def test_eta_del_caso_reale_del_14_luglio_supera_la_soglia(): """Il caso che ha motivato il gate: barra 07-08, esecuzione il 07-14 -> 6 giorni.""" eta = (pd.Timestamp("2026-07-14 14:00", tz="UTC") - pd.Timestamp("2026-07-08", tz="UTC")) assert eta.total_seconds() / 86400.0 > 2.0, "il gate non avrebbe fermato l'incidente reale" def test_data_illeggibile_e_trattata_come_stantia(): """None = non so leggere la data -> meglio non operare che operare su ignoto.""" for cattivo in (None, "", "non-una-data", object()): assert book_execute._data_age_days(cattivo) is None def test_funzionale_feed_stantio_blocca_lesecuzione_e_allerta(monkeypatch, capsys): """Il test che conta: conto ONLINE, posizione LEGGIBILE, ordine PRESENTE — cioe' la situazione esatta del 2026-07-14 — ma feed vecchio. Non deve partire nessun ordine e deve arrivare l'alert. """ from helpers import carica_script mod = carica_script("scripts/live/book_execute.py", "book_execute_iso") canned = dict( last_data="2026-07-08", online=True, real_equity=598.0, equity=598.0, eq_basis="mainnet USDC", cap_per_asset=300.0, skh_error=None, pos_error=None, assets=[dict(asset="ETH", instrument="ETH_USDC-PERPETUAL", tp_frac=0.0, skh_sign=1, skh_state="flat", net_target=75.0, position_usd=0.0, mark=1869.0, order=dict(side="buy"))], orders=[dict(side="buy")], ) alerts = [] monkeypatch.setattr(mod, "book_report", lambda **k: canned) monkeypatch.setattr(mod, "notify", lambda title, det=None: alerts.append((title, det))) 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)) monkeypatch.setattr(sys, "argv", ["book_execute.py", "--execute"]) # ARMATO + execute def boom(*a, **k): raise AssertionError("DeribitTrader costruito: il gate NON ha fermato il feed stantio") monkeypatch.setattr(mod, "DeribitTrader", boom) mod._run() # non deve sollevare: il gate esce prima out = capsys.readouterr().out assert "FEED STANTIO" in out assert "rebuild_history" in out, "il messaggio deve dire come sbloccare" assert any("FEED STANTIO" in t for t, _ in alerts), "alert Telegram non inviato" def test_config_senza_la_chiave_non_esplode(monkeypatch, capsys): """Una config priva di max_data_age_days deve ricadere sul default, non sollevare KeyError dentro il percorso con soldi veri.""" from helpers import carica_script mod = carica_script("scripts/live/book_execute.py", "book_execute_iso2") canned = dict(last_data=str(pd.Timestamp.now(tz="UTC").normalize().date()), online=True, real_equity=598.0, equity=598.0, eq_basis="t", cap_per_asset=300.0, skh_error=None, pos_error=None, assets=[], orders=[]) monkeypatch.setattr(mod, "book_report", lambda **k: canned) monkeypatch.setattr(mod, "notify", lambda title, det=None: None) monkeypatch.setattr(mod, "load_config", lambda: dict(execution_enabled=False, min_order_usd=5.0, disaster_sl_pct=0.3)) monkeypatch.setattr(sys, "argv", ["book_execute.py"]) mod._run() # nessun KeyError assert "FEED STANTIO" not in capsys.readouterr().out def test_il_gate_blocca_prima_di_creare_il_trader(): """Ordine dei controlli: il blocco per feed stantio deve stare PRIMA di DeribitTrader(), altrimenti si aprirebbe comunque una sessione autenticata verso il conto reale.""" src = (ROOT / "scripts" / "live" / "book_execute.py").read_text() i_gate = src.index("FEED STANTIO") i_trader = src.index("DeribitTrader() if do_execute") assert i_gate < i_trader, "il gate di staleness deve precedere la creazione del trader"