fix(book): gate fail-safe posizione + diagnostica equity — niente errori silenziosi nel path live
Terza+quarta chiusura del pattern "eccezione ingoiata -> stato safe silenzioso"
in src/live, dopo skh_error (31369b3). Emerse durante la verifica del gate SKH01.
Opzione A (gate fail-safe posizione):
- shadow._positions: se la read della posizione reale Deribit lancia, assume flat
MA ora ritorna un pos_error esplicito (prima solo una note mai propagata).
- Propagato shadow_report -> book_report -> book_execute: se ONLINE ma posizione
IGNOTA, l'esecutore NON opera a cieco (return + alert), come gia' per 'online'.
Opzione B (diagnostica equity, no halt):
- shadow._equity/shadow_report: se ONLINE ma equity reale non leggibile, il book
ripiega su paper_cap (~$2000) invece del conto reale (~$598) -> sovradimensiona
~33%, ma l'hard-cap $300/asset limita il downside. Nuovo flag eq_fallback ->
book_execute stampa warning + alert Telegram MA prosegue (niente gate: la scelta
e' solo diagnostica, l'hard-cap gia' protegge).
Test: +8 (4 gate A, 4 diagnostica B). Suite 156/156. Path live pulito
(skh_error/pos_error/eq_fallback = None). Diario 2026-07-01-book-live-error-surfacing.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -267,3 +267,134 @@ def test_book_execute_surfaces_skh_error(monkeypatch, capsys):
|
||||
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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GATE FAIL-SAFE posizione: se la read della posizione reale fallisce (ONLINE ma
|
||||
# posizione IGNOTA -> assunta flat), l'esecutore NON deve operare a cieco.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_positions_flags_read_failure():
|
||||
from src.live.shadow import ASSETS, _positions
|
||||
|
||||
class BadClient:
|
||||
def position_usd(self, inst):
|
||||
raise RuntimeError("api 500")
|
||||
|
||||
pos, note, err = _positions(BadClient())
|
||||
assert all(pos[a] == 0.0 for a in ASSETS) # assunta flat (fail-safe)
|
||||
assert err is not None and "non leggibile" in err # ma SEGNALATA (non silenziosa)
|
||||
|
||||
|
||||
def test_positions_ok_and_offline_have_no_error():
|
||||
from src.live.shadow import ASSETS, _positions
|
||||
|
||||
class GoodClient:
|
||||
def position_usd(self, inst):
|
||||
return 123.0
|
||||
|
||||
_, _, err_ok = _positions(GoodClient())
|
||||
_, _, err_off = _positions(None) # offline -> gestito dal gate 'online'
|
||||
assert err_ok is None and err_off is None
|
||||
|
||||
|
||||
def test_book_report_propagates_pos_error(monkeypatch):
|
||||
import src.live.book as book
|
||||
base = book.shadow_report(offline=True, equity_override=600.0)
|
||||
monkeypatch.setattr(book, "shadow_report", lambda **k: {**base, "pos_error": "IGNOTA"})
|
||||
r = book.book_report(offline=True, equity_override=600.0)
|
||||
assert r.get("pos_error") == "IGNOTA" # propagato fino al book
|
||||
|
||||
|
||||
def test_book_execute_halts_on_unreadable_position(monkeypatch, capsys):
|
||||
"""ARMATO + --execute + ordine presente: il gate DEVE fermarsi PRIMA di costruire il trader."""
|
||||
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=598.0, equity=598.0, eq_basis="mainnet USDC",
|
||||
cap_per_asset=300.0, skh_error=None,
|
||||
pos_error="posizione non leggibile, assunta FLAT: BTC (RuntimeError: api 500)",
|
||||
assets=[dict(asset="BTC", instrument="BTC_USDC-PERPETUAL", tp_frac=1.0, skh_sign=1,
|
||||
skh_state="flat", net_target=225.0, position_usd=0.0, mark=60000.0,
|
||||
order=dict(side="buy"))], # ordine presente: senza gate proverebbe a eseguire
|
||||
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))
|
||||
monkeypatch.setattr(sys, "argv", ["book_execute.py", "--execute"]) # ARMATO + execute
|
||||
|
||||
def boom(*a, **k):
|
||||
raise AssertionError("DeribitTrader NON deve essere costruito: il gate deve fermarsi prima")
|
||||
monkeypatch.setattr(mod, "DeribitTrader", boom)
|
||||
|
||||
mod._run()
|
||||
out = capsys.readouterr().out
|
||||
assert "POSIZIONE NON LEGGIBILE" in out # stampato
|
||||
assert any("posizione non leggibile" in t for t, _ in alerts) # alert inviato
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DIAGNOSTICA equity (Opzione B): ONLINE ma equity reale non leggibile -> sizing su
|
||||
# paper_cap. NON blocca (l'hard-cap $/asset protegge), ma DEVE essere segnalato.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_shadow_flags_eq_fallback_when_online_but_equity_unreadable(monkeypatch):
|
||||
import src.live.shadow as sh
|
||||
monkeypatch.setattr(sh, "_safe_client", lambda: object()) # online
|
||||
monkeypatch.setattr(sh, "_marks", lambda c, d: ({a: 60000.0 for a in sh.ASSETS},
|
||||
{a: "mainnet" for a in sh.ASSETS}))
|
||||
monkeypatch.setattr(sh, "_positions", lambda c: ({a: 0.0 for a in sh.ASSETS}, "mainnet", None))
|
||||
monkeypatch.setattr(sh, "_equity", lambda c, m: (None, "conto flat / non finanziato")) # equity IGNOTA
|
||||
r = sh.shadow_report() # online, no override
|
||||
assert r["online"] is True and r["real_equity"] is None
|
||||
assert r.get("eq_fallback") and "paper_cap" in r["eq_fallback"] # fallback SEGNALATO
|
||||
|
||||
|
||||
def test_shadow_no_eq_fallback_offline_or_override():
|
||||
from src.live.shadow import shadow_report
|
||||
assert shadow_report(offline=True, equity_override=600.0).get("eq_fallback") is None # override
|
||||
assert shadow_report(offline=True).get("eq_fallback") is None # offline != fallback-online
|
||||
|
||||
|
||||
def test_book_report_propagates_eq_fallback(monkeypatch):
|
||||
import src.live.book as book
|
||||
base = book.shadow_report(offline=True, equity_override=600.0)
|
||||
monkeypatch.setattr(book, "shadow_report", lambda **k: {**base, "eq_fallback": "PAPER_CAP"})
|
||||
assert book.book_report(offline=True, equity_override=600.0).get("eq_fallback") == "PAPER_CAP"
|
||||
|
||||
|
||||
def test_book_execute_eq_fallback_warns_but_proceeds(monkeypatch, capsys):
|
||||
"""eq_fallback: avvisa + notify MA PROSEGUE (non e' un halt, a differenza di pos_error)."""
|
||||
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=None, equity=2000.0,
|
||||
eq_basis="paper capital (ipotetico)", cap_per_asset=300.0, skh_error=None, pos_error=None,
|
||||
eq_fallback="equity reale non leggibile (conto flat) -> sizing su paper_cap $2,000",
|
||||
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 = []
|
||||
|
||||
class FakeDT:
|
||||
def ensure_disaster_sl(self, inst, sl_pct): return {"state": "flat"}
|
||||
def position_usd(self, inst): return 0.0
|
||||
|
||||
monkeypatch.setattr(mod, "book_report", lambda **k: canned)
|
||||
monkeypatch.setattr(mod, "notify", lambda title, det=None: alerts.append((title, det)))
|
||||
monkeypatch.setattr(mod, "DeribitTrader", lambda: FakeDT())
|
||||
monkeypatch.setattr(mod, "load_config",
|
||||
lambda: dict(execution_enabled=True, min_order_usd=5.0, disaster_sl_pct=0.30))
|
||||
monkeypatch.setattr(sys, "argv", ["book_execute.py", "--execute"])
|
||||
|
||||
mod._run()
|
||||
out = capsys.readouterr().out
|
||||
assert "EQUITY FALLBACK" in out # avvisato
|
||||
assert any("equity fallback" in t for t, _ in alerts) # alert inviato
|
||||
assert "Nessuna azione" in out # HA PROSEGUITO (non halt)
|
||||
|
||||
Reference in New Issue
Block a user