fix(book): esponi skh_error nel book live — niente flat SKH silenzioso
book_report scriveva skh_error in un dict locale MAI incluso nel return ->
r.get("skh_error") era sempre None. E book_execute non lo leggeva comunque.
Risultato: se il feed SKH (fresh_5m) fallisse, il book forzava SKH a flat in
silenzio, indistinguibile da un flat legittimo -> entry SKH reale mancato,
nessun alert.
Fix:
- src/live/book.py: skh_error come variabile esplicita, esposta nel dict di
ritorno (None normalmente). Il fail-safe (SKH->flat, TP01 indipendente sul
suo feed certificato) resta invariato.
- scripts/live/book_execute.py: emette la riga di log + alert Telegram quando
skh_error e' presente.
- tests: book_report cattura-e-flagga l'errore; book_execute lo fa emergere
(log + notify). Suite 148/148.
Contesto: verifica del gate SKH01 dopo 193 run flat dal 06-23 (gate SANO —
335/341 entry storici, shorta i crash; flat legittimo: ultimo entry 06-06
chiuso ~06-08 pre-arming). Questo chiude il rischio latente scoperto durante
la verifica.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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" 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")
|
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"]:
|
if not r["online"]:
|
||||||
print(" conto non leggibile (offline) -> stop, non eseguo a cieco.")
|
print(" conto non leggibile (offline) -> stop, non eseguo a cieco.")
|
||||||
if do_execute:
|
if do_execute:
|
||||||
|
|||||||
+3
-2
@@ -105,11 +105,12 @@ def book_report(offline: bool = False, equity_override: float | None = None,
|
|||||||
if live_feed:
|
if live_feed:
|
||||||
from src.live.livefeed import fresh_5m
|
from src.live.livefeed import fresh_5m
|
||||||
load5m = fresh_5m
|
load5m = fresh_5m
|
||||||
|
skh_error = None
|
||||||
try:
|
try:
|
||||||
skh = _skyhook_positions(load5m=load5m)
|
skh = _skyhook_positions(load5m=load5m)
|
||||||
except Exception as e: # non bloccare il report se il feed SKH fallisce
|
except Exception as e: # non bloccare il report se il feed SKH fallisce
|
||||||
skh = {a: "flat" for a in ASSETS}
|
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 = [], []
|
assets, orders = [], []
|
||||||
for a_rec in sh["assets"]:
|
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"],
|
last_data=sh["last_data"], online=sh["online"],
|
||||||
real_equity=sh["real_equity"], equity=equity, eq_basis=sh["eq_basis"],
|
real_equity=sh["real_equity"], equity=equity, eq_basis=sh["eq_basis"],
|
||||||
cap_per_asset=cap, weights=dict(TP01=W_TP01, SKH01=W_SKH),
|
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),
|
flat=all(abs(x["net_target"]) < FLAT_USD for x in assets),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -228,3 +228,42 @@ def test_book_report_uses_pure_formula_offline():
|
|||||||
assert a["skh_sign"] in (-1, 0, 1)
|
assert a["skh_sign"] in (-1, 0, 1)
|
||||||
# offline -> conto assunto flat -> nessuna posizione reale, report deterministico
|
# offline -> conto assunto flat -> nessuna posizione reale, report deterministico
|
||||||
assert all(a["position_usd"] == 0.0 for a in r["assets"])
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user