diff --git a/scripts/live/book_execute.py b/scripts/live/book_execute.py index 9dd72be..ab5bb94 100644 --- a/scripts/live/book_execute.py +++ b/scripts/live/book_execute.py @@ -74,6 +74,10 @@ def _run(): print(f" sizing base : ${equity:,.2f} | cap/asset ${r['cap_per_asset']:.0f} | min ${min_order:.0f} | disaster-SL -{sl_pct*100:.0f}%") print(f" ultima barra : {r['last_data']}\n") + if r.get("skh_error"): # SKH feed fallito -> book.py ha forzato flat IN SILENZIO + print(f" ⚠️ SKH FEED ERRORE (SKH forzato flat!): {r['skh_error']}") + notify("⚠️ BOOK — SKH feed fallito (sleeve forzato flat)", {"error": r["skh_error"]}) + if not r["online"]: print(" conto non leggibile (offline) -> stop, non eseguo a cieco.") if do_execute: diff --git a/src/live/book.py b/src/live/book.py index 5641fe4..2918df9 100644 --- a/src/live/book.py +++ b/src/live/book.py @@ -105,11 +105,12 @@ def book_report(offline: bool = False, equity_override: float | None = None, if live_feed: from src.live.livefeed import fresh_5m load5m = fresh_5m + skh_error = None try: skh = _skyhook_positions(load5m=load5m) except Exception as e: # non bloccare il report se il feed SKH fallisce skh = {a: "flat" for a in ASSETS} - sh = {**sh, "skh_error": f"{type(e).__name__}: {e}"} + skh_error = f"{type(e).__name__}: {e}" # forzato flat -> ESPORRE (sennò flat silenzioso) assets, orders = [], [] for a_rec in sh["assets"]: @@ -135,6 +136,6 @@ def book_report(offline: bool = False, equity_override: float | None = None, last_data=sh["last_data"], online=sh["online"], real_equity=sh["real_equity"], equity=equity, eq_basis=sh["eq_basis"], cap_per_asset=cap, weights=dict(TP01=W_TP01, SKH01=W_SKH), - assets=assets, orders=orders, + assets=assets, orders=orders, skh_error=skh_error, flat=all(abs(x["net_target"]) < FLAT_USD for x in assets), ) diff --git a/tests/test_book_live.py b/tests/test_book_live.py index 9e7aac9..dbad820 100644 --- a/tests/test_book_live.py +++ b/tests/test_book_live.py @@ -228,3 +228,42 @@ def test_book_report_uses_pure_formula_offline(): assert a["skh_sign"] in (-1, 0, 1) # offline -> conto assunto flat -> nessuna posizione reale, report deterministico assert all(a["position_usd"] == 0.0 for a in r["assets"]) + + +# --------------------------------------------------------------------------- +# SKH feed fallito: book_report NON crasha (forza flat + flagga skh_error) e +# book_execute DEVE farlo emergere (log + alert), non ingoiarlo silenziosamente. +# --------------------------------------------------------------------------- +def test_book_report_flags_skh_feed_error(monkeypatch): + import src.live.book as book + monkeypatch.setattr(book, "_skyhook_positions", + lambda *a, **k: (_ for _ in ()).throw(RuntimeError("feed 5m giu"))) + r = book.book_report(offline=True, equity_override=600.0) + assert "skh_error" in r and "feed 5m giu" in r["skh_error"] # errore catturato + esposto + assert all(a["skh_sign"] == 0 for a in r["assets"]) # SKH forzato flat (fail-safe) + + +def test_book_execute_surfaces_skh_error(monkeypatch, capsys): + """Se il report porta skh_error, _run() lo stampa E chiama notify (niente flat silenzioso).""" + import importlib.util + 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) + + canned = dict( + last_data="2026-07-01", online=True, real_equity=600.0, equity=600.0, eq_basis="test", + cap_per_asset=300.0, skh_error="RuntimeError: feed 5m giu", + assets=[dict(asset="BTC", instrument="BTC_USDC-PERPETUAL", tp_frac=0.0, skh_sign=0, + skh_state="flat", net_target=0.0, position_usd=0.0, mark=60000.0, order=None)], + orders=[], + ) + 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=False, min_order_usd=5.0, disaster_sl_pct=0.30)) + monkeypatch.setattr(sys, "argv", ["book_execute.py"]) # niente --execute -> dry-run + mod._run() + + out = capsys.readouterr().out + assert "SKH FEED ERRORE" in out # stampato nel log + assert any("SKH feed fallito" in title for title, _ in alerts) # alert inviato