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:
@@ -0,0 +1,58 @@
|
|||||||
|
# 2026-07-01 — Book live: verifica gate SKH01 + emersione errori silenziosi
|
||||||
|
|
||||||
|
## Contesto
|
||||||
|
Il BOOK DERIBIT armato (TP01 0.75 + SKH01 0.25 nettati) è **flat da quando è stato armato
|
||||||
|
(2026-06-23)**: 193 run orari, tutte HOLD, zero ordini reali. Domanda: il flat è legittimo o il
|
||||||
|
gate di SKH01 è muto per un bug?
|
||||||
|
|
||||||
|
## Verifica del gate SKH01 — SANO
|
||||||
|
- **Logica viva:** `skyhook_entries` produce 335 entry (BTC, 222 long/113 short) e 341 (ETH,
|
||||||
|
226/115) su tutta la storia. Non è degenere.
|
||||||
|
- **Shorta davvero i crash:** ha aperto SHORT sulla gamba di crollo di fine maggio/inizio giugno —
|
||||||
|
BTC 05-28→06-04 ($73k→$64k), ETH 06-02→06-06 ($1865→$1555).
|
||||||
|
- **Flat da 06-23 = legittimo:** ultimo entry 06-06, `max_bars=16 × 230m ≈ 2,5g` → chiuso ~06-08/09,
|
||||||
|
**prima** dell'arming (06-23). Dal grind post-crash nessun setup breakout+regime valido → flat
|
||||||
|
corretto per design ("NON trend-follower"). Contesto prezzo al 01-07: BTC −53% dal picco, ETH
|
||||||
|
−67,5%, momentum negativo su 1/3/6/12 mesi → anche TP01 (long-flat TSMOM) è a 0x.
|
||||||
|
- **Entrambi i feed valutano senza crash:** certificato e live `fresh_5m` → `skh_error=None`.
|
||||||
|
|
||||||
|
## Bug scoperto e corretto — pattern "eccezione ingoiata → stato safe silenzioso"
|
||||||
|
`src/live/` aveva più `try/except` che, in errore, producono uno stato safe (flat/fallback) **senza
|
||||||
|
farlo emergere**. Rischio: quando l'errore è transitorio ma reale, l'esecutore opera su dati sbagliati
|
||||||
|
credendoli legittimi.
|
||||||
|
|
||||||
|
### (1) `skh_error` — CORRETTO
|
||||||
|
`book_report` scriveva `skh_error` in un dict locale **mai incluso nel return** → `r.get("skh_error")`
|
||||||
|
sempre `None`; e `book_execute` non lo leggeva comunque. Un feed SKH rotto avrebbe forzato SKH→flat in
|
||||||
|
silenzio (entry mancato, nessun alert). Fix: `skh_error` esposto nel return + `book_execute` emette
|
||||||
|
log `⚠️ SKH FEED ERRORE` + alert Telegram. (commit 31369b3)
|
||||||
|
|
||||||
|
### (2) Gate fail-safe posizione (Opzione A) — CORRETTO
|
||||||
|
`src/live/shadow.py::_positions`: se la read della posizione reale Deribit lancia, assume flat (0.0)
|
||||||
|
con una `note`, MAI propagata (`pos_src`) a `book_report`/`book_execute`. Severità mitigata a valle
|
||||||
|
da `rebalance_signed` che **rilegge la posizione fresca** (no doppia-posizione), ma resta il caso
|
||||||
|
"target≈0 + posizione aperta + read fallita → close saltato quel run" (transitorio, backstoppato dal
|
||||||
|
disaster-SL −30%). **Scelta A = gate fail-safe:** `_positions` ritorna un `pos_error` esplicito →
|
||||||
|
propagato `shadow_report → book_report → book_execute`; se presente (ONLINE ma posizione IGNOTA)
|
||||||
|
l'esecutore **NON opera a cieco** (return + alert), come già fa per `online`.
|
||||||
|
|
||||||
|
### (3) Diagnostica equity (Opzione B) — CORRETTO
|
||||||
|
`src/live/shadow.py::_equity`: se `account_summary('USDC')` e tutte le per-asset falliscono,
|
||||||
|
`real_eq=None` e `shadow_report` ripiega su `paper_cap` (~$2000 dallo state paper) mentre il conto
|
||||||
|
reale è ~$598 → il book dimensionerebbe su $2000 invece di $598 (**sovradimensiona ~33%**). Il danno
|
||||||
|
è limitato dall'**hard-cap $300/asset** in `book_net_target`. **Scelta B = SOLO DIAGNOSTICA** (niente
|
||||||
|
gate/halt, l'hard-cap già protegge il downside): `shadow_report` calcola `eq_fallback` (ONLINE +
|
||||||
|
equity reale non leggibile + no override), propagato `→ book_report → book_execute`, che stampa
|
||||||
|
`⚠️ EQUITY FALLBACK` + alert Telegram **ma prosegue** (a differenza del gate posizione, che blocca).
|
||||||
|
|
||||||
|
## Test
|
||||||
|
Suite completa **156/156** (era 148; +4 gate A + 4 diagnostica B). Dry-run reale di `book_execute`
|
||||||
|
invariato (nessuna riga d'errore, nessun ordine, conto $598.06 al target). Path live: `pos_error=None`,
|
||||||
|
`skh_error=None`, `eq_fallback=None`.
|
||||||
|
|
||||||
|
## Sintesi finale — i 3 pattern "errore silenzioso" del path live sono ora tutti tracciati
|
||||||
|
| # | Punto | Comportamento | Emersione |
|
||||||
|
|---|-------|---------------|-----------|
|
||||||
|
| 1 | `book_report` feed SKH | SKH → flat | log `⚠️ SKH FEED ERRORE` + alert |
|
||||||
|
| 2 | `_positions` read fallita | posizione → flat | **HALT** (non esegue) + alert (gate A) |
|
||||||
|
| 3 | `_equity` non leggibile | sizing → paper_cap | log `⚠️ EQUITY FALLBACK` + alert (diagnostica B) |
|
||||||
@@ -84,6 +84,18 @@ def _run():
|
|||||||
notify("⚠️ BOOK LIVE — conto offline", {"nota": "salto l'esecuzione, non opero a cieco"})
|
notify("⚠️ BOOK LIVE — conto offline", {"nota": "salto l'esecuzione, non opero a cieco"})
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if r.get("pos_error"): # ONLINE ma posizione IGNOTA (read fallita -> assunta flat)
|
||||||
|
print(f" 🛑 POSIZIONE NON LEGGIBILE -> NON eseguo a cieco: {r['pos_error']}")
|
||||||
|
if do_execute:
|
||||||
|
notify("🛑 BOOK LIVE — posizione non leggibile", {"error": r["pos_error"],
|
||||||
|
"nota": "salto l'esecuzione, non opero a cieco"})
|
||||||
|
return
|
||||||
|
|
||||||
|
if r.get("eq_fallback"): # equity reale non leggibile -> sizing su paper_cap
|
||||||
|
print(f" ⚠️ EQUITY FALLBACK (sizing su paper_cap, NON blocco): {r['eq_fallback']}")
|
||||||
|
if do_execute: # solo diagnostica: l'hard-cap $/asset limita il downside
|
||||||
|
notify("⚠️ BOOK LIVE — equity fallback (sizing su paper_cap)", {"nota": r["eq_fallback"]})
|
||||||
|
|
||||||
trader = DeribitTrader() if do_execute else None
|
trader = DeribitTrader() if do_execute else None
|
||||||
actions = []
|
actions = []
|
||||||
for a in r["assets"]:
|
for a in r["assets"]:
|
||||||
|
|||||||
+2
-1
@@ -136,6 +136,7 @@ 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, skh_error=skh_error,
|
assets=assets, orders=orders, skh_error=skh_error, pos_error=sh.get("pos_error"),
|
||||||
|
eq_fallback=sh.get("eq_fallback"),
|
||||||
flat=all(abs(x["net_target"]) < FLAT_USD for x in assets),
|
flat=all(abs(x["net_target"]) < FLAT_USD for x in assets),
|
||||||
)
|
)
|
||||||
|
|||||||
+17
-5
@@ -72,14 +72,18 @@ def _marks(client, dfs):
|
|||||||
|
|
||||||
def _positions(client):
|
def _positions(client):
|
||||||
if client is None:
|
if client is None:
|
||||||
return {a: 0.0 for a in ASSETS}, "offline -> assunto flat"
|
return {a: 0.0 for a in ASSETS}, "offline -> assunto flat", None
|
||||||
pos, note = {}, "mainnet"
|
pos, note, failed = {}, "mainnet", []
|
||||||
for a in ASSETS:
|
for a in ASSETS:
|
||||||
try:
|
try:
|
||||||
pos[a] = client.position_usd(INSTRUMENT[a])
|
pos[a] = client.position_usd(INSTRUMENT[a])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pos[a], note = 0.0, f"read fallito ({type(e).__name__}) -> assunto flat"
|
pos[a], note = 0.0, f"read fallito ({type(e).__name__}) -> assunto flat"
|
||||||
return pos, note
|
failed.append(f"{a} ({type(e).__name__}: {e})")
|
||||||
|
# pos_error != None SOLO se ONLINE ma la read ha lanciato: la posizione e' IGNOTA (assunta flat) ->
|
||||||
|
# l'esecutore NON deve operare a cieco. L'offline (client=None) e' gestito a parte dal gate 'online'.
|
||||||
|
err = ("posizione non leggibile, assunta FLAT: " + ", ".join(failed)) if failed else None
|
||||||
|
return pos, note, err
|
||||||
|
|
||||||
|
|
||||||
def _equity(client, marks):
|
def _equity(client, marks):
|
||||||
@@ -101,6 +105,8 @@ def _equity(client, marks):
|
|||||||
pass
|
pass
|
||||||
if any_ok and tot > 1:
|
if any_ok and tot > 1:
|
||||||
return tot, "mainnet coin-margined"
|
return tot, "mainnet coin-margined"
|
||||||
|
# real_eq=None qui -> shadow_report ripiega su paper_cap e alza eq_fallback (diagnostica B):
|
||||||
|
# segnalato a book_execute (alert), non blocca (l'hard-cap $300/asset limita il downside).
|
||||||
return None, "conto flat / non finanziato"
|
return None, "conto flat / non finanziato"
|
||||||
|
|
||||||
|
|
||||||
@@ -113,7 +119,7 @@ def shadow_report(offline: bool = False, equity_override: float | None = None) -
|
|||||||
|
|
||||||
client = None if offline else _safe_client()
|
client = None if offline else _safe_client()
|
||||||
marks, marks_src = _marks(client, dfs)
|
marks, marks_src = _marks(client, dfs)
|
||||||
positions, pos_src = _positions(client)
|
positions, pos_src, pos_error = _positions(client)
|
||||||
real_eq, eq_src = _equity(client, marks)
|
real_eq, eq_src = _equity(client, marks)
|
||||||
|
|
||||||
paper = json.loads(PAPER_STATE.read_text()) if PAPER_STATE.exists() else None
|
paper = json.loads(PAPER_STATE.read_text()) if PAPER_STATE.exists() else None
|
||||||
@@ -124,6 +130,11 @@ def shadow_report(offline: bool = False, equity_override: float | None = None) -
|
|||||||
equity = equity_override if equity_override is not None else (real_eq if real_eq else paper_cap)
|
equity = equity_override if equity_override is not None else (real_eq if real_eq else paper_cap)
|
||||||
eq_basis = ("override" if equity_override is not None
|
eq_basis = ("override" if equity_override is not None
|
||||||
else eq_src if real_eq else "paper capital (ipotetico: conto non finanziato)")
|
else eq_src if real_eq else "paper capital (ipotetico: conto non finanziato)")
|
||||||
|
# Opzione B (diagnostica): ONLINE ma equity reale non leggibile -> sizing su paper_cap invece del
|
||||||
|
# conto reale. NON blocca (l'hard-cap $300/asset limita il downside), ma va SEGNALATO.
|
||||||
|
eq_fallback = (f"equity reale non leggibile ({eq_src}) -> sizing su paper_cap ${paper_cap:,.0f} "
|
||||||
|
f"invece del conto reale") if (equity_override is None and client is not None
|
||||||
|
and not real_eq) else None
|
||||||
|
|
||||||
assets, orders = [], []
|
assets, orders = [], []
|
||||||
for a in ASSETS:
|
for a in ASSETS:
|
||||||
@@ -172,7 +183,8 @@ def shadow_report(offline: bool = False, equity_override: float | None = None) -
|
|||||||
last_data=str(pd.Timestamp(last_ts, unit="ms", tz="UTC").date()),
|
last_data=str(pd.Timestamp(last_ts, unit="ms", tz="UTC").date()),
|
||||||
online=(client is not None and marks_src.get("BTC") == "mainnet"),
|
online=(client is not None and marks_src.get("BTC") == "mainnet"),
|
||||||
real_equity=real_eq, equity=equity, eq_basis=eq_basis,
|
real_equity=real_eq, equity=equity, eq_basis=eq_basis,
|
||||||
pos_src=pos_src, assets=assets, orders=orders, live_trades=live_trades,
|
pos_src=pos_src, pos_error=pos_error, eq_fallback=eq_fallback,
|
||||||
|
assets=assets, orders=orders, live_trades=live_trades,
|
||||||
disaster_sls=disaster_sls,
|
disaster_sls=disaster_sls,
|
||||||
flat=all(abs(targets[a]) < 1e-9 for a in ASSETS),
|
flat=all(abs(targets[a]) < 1e-9 for a in ASSETS),
|
||||||
paper_aligned=(paper_ts == last_ts),
|
paper_aligned=(paper_ts == last_ts),
|
||||||
|
|||||||
@@ -267,3 +267,134 @@ def test_book_execute_surfaces_skh_error(monkeypatch, capsys):
|
|||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "SKH FEED ERRORE" in out # stampato nel log
|
assert "SKH FEED ERRORE" in out # stampato nel log
|
||||||
assert any("SKH feed fallito" in title for title, _ in alerts) # alert inviato
|
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