book_execute: integrazione U1+U2 — un solo Telegram in solo protezione, stato unknown, set_open_cap diretto (#18, #21, #22, #24)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -155,9 +155,9 @@ def _run():
|
|||||||
|
|
||||||
if r.get("skh_error"): # SKH feed fallito -> book.py ha forzato flat IN SILENZIO
|
if r.get("skh_error"): # SKH feed fallito -> book.py ha forzato flat IN SILENZIO
|
||||||
# (#22) Non e' piu' "SKH forzato flat!": book_execute non manda PIU' ordini basati su
|
# (#22) Non e' piu' "SKH forzato flat!": book_execute non manda PIU' ordini basati su
|
||||||
# quel segnale ignoto (modo SOLO PROTEZIONE, sotto) — i target restano informativi.
|
# quel segnale ignoto (modo SOLO PROTEZIONE, sotto, che porta anche questo errore su
|
||||||
|
# Telegram — qui solo il print, per non mandare due notify per lo stesso guasto).
|
||||||
print(f" ⚠️ SKH FEED ERRORE (SKH non calcolato: nessun ordine, solo protezione): {r['skh_error']}")
|
print(f" ⚠️ SKH FEED ERRORE (SKH non calcolato: nessun ordine, solo protezione): {r['skh_error']}")
|
||||||
notify("⚠️ BOOK — SKH feed fallito (sleeve forzato flat)", {"error": r["skh_error"]})
|
|
||||||
|
|
||||||
# --- FRESCHEZZA del feed 5m usato per il segnale SKH (2026-07-26) --------------------------
|
# --- FRESCHEZZA del feed 5m usato per il segnale SKH (2026-07-26) --------------------------
|
||||||
# `fresh_5m` ricade sul feed certificato IN SILENZIO se il fetch pubblico Deribit fallisce, e
|
# `fresh_5m` ricade sul feed certificato IN SILENZIO se il fetch pubblico Deribit fallisce, e
|
||||||
@@ -248,9 +248,9 @@ def _run():
|
|||||||
return
|
return
|
||||||
|
|
||||||
if r.get("eq_fallback"): # equity reale non leggibile -> niente sizing sul conto
|
if r.get("eq_fallback"): # equity reale non leggibile -> niente sizing sul conto
|
||||||
|
# (#21) qui solo il print: il notify e' nel blocco SOLO PROTEZIONE sotto, che porta anche
|
||||||
|
# questo errore su Telegram — un solo avviso per lo stesso guasto, non due.
|
||||||
print(f" ⚠️ EQUITY FALLBACK (nessun ordine, solo protezione): {r['eq_fallback']}")
|
print(f" ⚠️ EQUITY FALLBACK (nessun ordine, solo protezione): {r['eq_fallback']}")
|
||||||
if do_execute: # (#21) segnalato: il modo SOLO PROTEZIONE e' sotto
|
|
||||||
notify("⚠️ BOOK LIVE — equity fallback (sizing su paper_cap)", {"nota": r["eq_fallback"]})
|
|
||||||
|
|
||||||
# --- SOLO PROTEZIONE (#21 equity ignota, #22 SKH ignoto) ------------------------------------
|
# --- SOLO PROTEZIONE (#21 equity ignota, #22 SKH ignoto) ------------------------------------
|
||||||
# Principio unico: quando il giro non sa QUANTO vale il conto o QUALE e' il segnale, NON manda
|
# Principio unico: quando il giro non sa QUANTO vale il conto o QUALE e' il segnale, NON manda
|
||||||
@@ -259,6 +259,10 @@ def _run():
|
|||||||
# da posizione e mark, non da equity o segnale. Stesso principio che pos_error applica gia'
|
# da posizione e mark, non da equity o segnale. Stesso principio che pos_error applica gia'
|
||||||
# bloccando l'esecuzione, qui senza saltare la protezione.
|
# bloccando l'esecuzione, qui senza saltare la protezione.
|
||||||
solo_protezione: str | None = r.get("eq_fallback") or r.get("skh_error")
|
solo_protezione: str | None = r.get("eq_fallback") or r.get("skh_error")
|
||||||
|
# Calcolato una volta sola: serve sia nell'azione per-asset del ciclo sotto sia nel
|
||||||
|
# riepilogo finale (#21, #22) — un solo motivo, non due troncamenti che potrebbero divergere.
|
||||||
|
motivo_breve = (solo_protezione if solo_protezione and len(solo_protezione) <= 60
|
||||||
|
else (solo_protezione[:60] + "…" if solo_protezione else None))
|
||||||
if solo_protezione and do_execute:
|
if solo_protezione and do_execute:
|
||||||
titolo = ("⚠️ BOOK LIVE — equity non leggibile: nessun ordine, solo protezione"
|
titolo = ("⚠️ BOOK LIVE — equity non leggibile: nessun ordine, solo protezione"
|
||||||
if r.get("eq_fallback") else
|
if r.get("eq_fallback") else
|
||||||
@@ -290,6 +294,7 @@ def _run():
|
|||||||
# non deve togliere la rete di sicurezza all'altro.
|
# non deve togliere la rete di sicurezza all'altro.
|
||||||
falliti: list[str] = []
|
falliti: list[str] = []
|
||||||
scoperti: list[str] = []
|
scoperti: list[str] = []
|
||||||
|
non_verificati: list[str] = []
|
||||||
for a in r["assets"]:
|
for a in r["assets"]:
|
||||||
asset, inst = a["asset"], a["instrument"]
|
asset, inst = a["asset"], a["instrument"]
|
||||||
net, cur, mark = a["net_target"], a["position_usd"], a["mark"]
|
net, cur, mark = a["net_target"], a["position_usd"], a["mark"]
|
||||||
@@ -299,7 +304,6 @@ def _run():
|
|||||||
if solo_protezione:
|
if solo_protezione:
|
||||||
# (#21, #22) nessun rebalance: l'azione stampata lo dice, non e' "a target" — e'
|
# (#21, #22) nessun rebalance: l'azione stampata lo dice, non e' "a target" — e'
|
||||||
# "non tocco perche' non so quanto vale il conto / qual e' il segnale".
|
# "non tocco perche' non so quanto vale il conto / qual e' il segnale".
|
||||||
motivo_breve = solo_protezione if len(solo_protezione) <= 60 else solo_protezione[:60] + "…"
|
|
||||||
act = f"HOLD (solo protezione: {motivo_breve})"
|
act = f"HOLD (solo protezione: {motivo_breve})"
|
||||||
elif order is None:
|
elif order is None:
|
||||||
act = "HOLD (a target)"
|
act = "HOLD (a target)"
|
||||||
@@ -314,11 +318,10 @@ def _run():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if do_execute and order is not None and not solo_protezione:
|
if do_execute and order is not None and not solo_protezione:
|
||||||
# U1/U2 si uniscono dopo questo rilascio: `set_open_cap` e' di U1 (execution.py,
|
# (#18) tetto di apertura derivato dal cap di config del libro. Se cap_per_asset*scala
|
||||||
# stesso file, non toccato qui). hasattr perche' oggi DeribitTrader non lo ha
|
# o mark sono <=0 solleva ValueError: e' giusto, finisce nell'isolamento per asset
|
||||||
# ancora — l'hasattr si toglie al merge, quando entrambi vivono nello stesso ramo.
|
# (except sotto), non deve fermare il giro intero.
|
||||||
if hasattr(trader, "set_open_cap"):
|
trader.set_open_cap(inst, r["cap_per_asset"] * r.get("scala", 1.0), mark)
|
||||||
trader.set_open_cap(inst, r["cap_per_asset"] * r.get("scala", 1.0), mark)
|
|
||||||
try:
|
try:
|
||||||
fills = trader.rebalance_signed(inst, net, mark, min_usd=min_order)
|
fills = trader.rebalance_signed(inst, net, mark, min_usd=min_order)
|
||||||
except RebalanceParziale as rp:
|
except RebalanceParziale as rp:
|
||||||
@@ -360,6 +363,15 @@ def _run():
|
|||||||
"amount": round(ds.get("amount") or 0, 4),
|
"amount": round(ds.get("amount") or 0, 4),
|
||||||
"azione": "ripiazzare il bracket a mano, oppure chiudere la posizione",
|
"azione": "ripiazzare il bracket a mano, oppure chiudere la posizione",
|
||||||
"notes": str(ds.get("notes"))[:300]})
|
"notes": str(ds.get("notes"))[:300]})
|
||||||
|
elif stato == "unknown":
|
||||||
|
# (#24) posizione o open_orders illeggibili: NON e' "naked" (non si e' tolto
|
||||||
|
# nulla, ensure_disaster_sl non cancella ne' piazza su dati a meta'). E' un
|
||||||
|
# giro degradato lo stesso: non si sa se la protezione esistente e' ancora
|
||||||
|
# coerente con la posizione attuale.
|
||||||
|
non_verificati.append(asset)
|
||||||
|
print(f" ⚠️ {asset}: protezione NON VERIFICATA — {ds.get('notes')}")
|
||||||
|
notify("⚠️ BOOK — disaster-SL non verificabile (lettura posizione/ordini fallita)",
|
||||||
|
{"asset": asset, "notes": str(ds.get("notes"))[:300]})
|
||||||
except Exception as e: # noqa: BLE001 — isolamento per asset: l'altro deve continuare
|
except Exception as e: # noqa: BLE001 — isolamento per asset: l'altro deve continuare
|
||||||
d = diagnose(errori_dal_report(r) + [f"{type(e).__name__}: {e}"])
|
d = diagnose(errori_dal_report(r) + [f"{type(e).__name__}: {e}"])
|
||||||
falliti.append(asset)
|
falliti.append(asset)
|
||||||
@@ -379,9 +391,15 @@ def _run():
|
|||||||
print(f" 🚨 POSIZIONI SCOPERTE (nessun disaster-SL on-book): {', '.join(scoperti)}")
|
print(f" 🚨 POSIZIONI SCOPERTE (nessun disaster-SL on-book): {', '.join(scoperti)}")
|
||||||
if falliti:
|
if falliti:
|
||||||
print(f" 🛑 asset falliti in questo giro: {', '.join(falliti)}")
|
print(f" 🛑 asset falliti in questo giro: {', '.join(falliti)}")
|
||||||
|
if non_verificati:
|
||||||
|
print(f" ⚠️ disaster-SL NON VERIFICATO (lettura posizione/ordini fallita): {', '.join(non_verificati)}")
|
||||||
if not do_execute:
|
if not do_execute:
|
||||||
print(" => DRY-RUN: nessun ordine inviato." +
|
print(" => DRY-RUN: nessun ordine inviato." +
|
||||||
("" if enabled else " Per armare: config/live.json execution_enabled=true + --execute."))
|
("" if enabled else " Per armare: config/live.json execution_enabled=true + --execute."))
|
||||||
|
elif solo_protezione:
|
||||||
|
# (#21, #22) non e' "Nessuna azione: conto gia' al target" — il conto potrebbe NON esserci
|
||||||
|
# per niente al target, semplicemente non lo si sa. Distinto dal ramo HOLD sotto.
|
||||||
|
print(f" => SOLO PROTEZIONE: nessun ordine di ribilancio ({motivo_breve}), disaster-SL controllato.")
|
||||||
elif all(x.startswith("HOLD") for x in actions):
|
elif all(x.startswith("HOLD") for x in actions):
|
||||||
print(" => Nessuna azione: conto gia' al target netto del book.")
|
print(" => Nessuna azione: conto gia' al target netto del book.")
|
||||||
else:
|
else:
|
||||||
@@ -391,7 +409,9 @@ def _run():
|
|||||||
# (#21, #22) SOLO PROTEZIONE e' degradato anche senza asset falliti/scoperti: il giro non ha
|
# (#21, #22) SOLO PROTEZIONE e' degradato anche senza asset falliti/scoperti: il giro non ha
|
||||||
# ribilanciato nulla perche' non sapeva quanto vale il conto o qual era il segnale. Solo se
|
# ribilanciato nulla perche' non sapeva quanto vale il conto o qual era il segnale. Solo se
|
||||||
# il giro era ARMATO: in DRY-RUN non si e' comunque tentato nulla, e' solo un'anteprima.
|
# il giro era ARMATO: in DRY-RUN non si e' comunque tentato nulla, e' solo un'anteprima.
|
||||||
if scoperti or falliti or (solo_protezione and do_execute):
|
# (#24) `non_verificati` e' degradato allo stesso modo: la protezione esistente non e' stata
|
||||||
|
# controllata, indipendentemente da solo_protezione.
|
||||||
|
if scoperti or falliti or non_verificati or (solo_protezione and do_execute):
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,9 @@ class DeribitTrader(DeribitRead):
|
|||||||
if side not in ("buy", "sell"):
|
if side not in ("buy", "sell"):
|
||||||
raise GuardrailError(f"side non valido: {side}")
|
raise GuardrailError(f"side non valido: {side}")
|
||||||
if not reduce_only: # cap SOLO sulle aperture; le chiusure si tentano sempre
|
if not reduce_only: # cap SOLO sulle aperture; le chiusure si tentano sempre
|
||||||
cap = self._open_cap.get(instrument, MAX_AMOUNT[instrument])
|
# getattr, non self._open_cap diretto: un DeribitTrader creato con object.__new__
|
||||||
|
# (test che saltano __init__) non ha l'attributo, e qui non deve dare AttributeError.
|
||||||
|
cap = getattr(self, "_open_cap", {}).get(instrument, MAX_AMOUNT[instrument])
|
||||||
if amount <= 0 or amount > cap:
|
if amount <= 0 or amount > cap:
|
||||||
raise GuardrailError(f"size {amount} fuori dal cap apertura (0, {cap}]")
|
raise GuardrailError(f"size {amount} fuori dal cap apertura (0, {cap}]")
|
||||||
if amount <= 0:
|
if amount <= 0:
|
||||||
|
|||||||
+25
-9
@@ -415,7 +415,10 @@ def test_book_report_flags_skh_feed_error(monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
def test_book_execute_surfaces_skh_error(monkeypatch, capsys):
|
def test_book_execute_surfaces_skh_error(monkeypatch, capsys):
|
||||||
"""Se il report porta skh_error, _run() lo stampa E chiama notify (niente flat silenzioso)."""
|
"""Se il report porta skh_error, _run() lo stampa SEMPRE (anche in dry-run); quando il giro e'
|
||||||
|
ARMATO l'alert parte UNA sola volta, nel modo SOLO PROTEZIONE (#22) — niente flat silenzioso,
|
||||||
|
ma nemmeno due notify per lo stesso guasto (il vecchio notify incondizionato e' stato tolto,
|
||||||
|
U6 #22)."""
|
||||||
from helpers import carica_script
|
from helpers import carica_script
|
||||||
mod = carica_script("scripts/live/book_execute.py")
|
mod = carica_script("scripts/live/book_execute.py")
|
||||||
|
|
||||||
@@ -426,17 +429,27 @@ def test_book_execute_surfaces_skh_error(monkeypatch, capsys):
|
|||||||
skh_state="flat", net_target=0.0, position_usd=0.0, mark=60000.0, order=None)],
|
skh_state="flat", net_target=0.0, position_usd=0.0, mark=60000.0, order=None)],
|
||||||
orders=[],
|
orders=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class _TraderOk:
|
||||||
|
def ensure_disaster_sl(self, inst, sl_pct):
|
||||||
|
return {"state": "ok", "stop": 1.0, "amount": 0.0}
|
||||||
|
|
||||||
alerts = []
|
alerts = []
|
||||||
monkeypatch.setattr(mod, "book_report", lambda **k: canned)
|
monkeypatch.setattr(mod, "book_report", lambda **k: canned)
|
||||||
monkeypatch.setattr(mod, "notify", lambda title, det=None: alerts.append((title, det)))
|
monkeypatch.setattr(mod, "notify", lambda title, det=None: alerts.append((title, det)))
|
||||||
|
monkeypatch.setattr(mod, "DeribitTrader", lambda *a, **k: _TraderOk())
|
||||||
monkeypatch.setattr(mod, "load_config",
|
monkeypatch.setattr(mod, "load_config",
|
||||||
lambda: dict(execution_enabled=False, min_order_usd=5.0, disaster_sl_pct=0.30))
|
lambda: dict(execution_enabled=True, min_order_usd=5.0, disaster_sl_pct=0.30))
|
||||||
monkeypatch.setattr(sys, "argv", ["book_execute.py"]) # niente --execute -> dry-run
|
monkeypatch.setattr(sys, "argv", ["book_execute.py", "--execute"]) # ARMATO: qui l'alert conta
|
||||||
mod._run()
|
try:
|
||||||
|
mod._run()
|
||||||
|
except SystemExit:
|
||||||
|
pass
|
||||||
|
|
||||||
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, sempre
|
||||||
assert any("SKH feed fallito" in title for title, _ in alerts) # alert inviato
|
assert len(alerts) == 1 # UNA sola notify, non due
|
||||||
|
assert "solo protezione" in alerts[0][0].lower()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -576,8 +589,11 @@ def test_book_execute_eq_fallback_e_SOLO_PROTEZIONE(monkeypatch, capsys):
|
|||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
uscita = e.code
|
uscita = e.code
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "EQUITY FALLBACK" in out # avvisato
|
assert "EQUITY FALLBACK" in out # avvisato nel log (print, resta)
|
||||||
assert any("equity fallback" in t for t, _ in alerts) # alert diagnostico esistente
|
# (#21, U6) UNA sola notify, non due: il vecchio alert "equity fallback (sizing su paper_cap)"
|
||||||
assert any("solo protezione" in t.lower() for t, _ in alerts) # NUOVO alert di modo
|
# e' stato tolto perche' diceva il falso (qui non si dimensiona alcun ordine) — resta solo
|
||||||
|
# quello del modo SOLO PROTEZIONE.
|
||||||
|
assert len(alerts) == 1
|
||||||
|
assert "solo protezione" in alerts[0][0].lower()
|
||||||
assert trader.rebalance_chiamato is False # NESSUN ordine dimensionato
|
assert trader.rebalance_chiamato is False # NESSUN ordine dimensionato
|
||||||
assert uscita == 2 # giro degradato
|
assert uscita == 2 # giro degradato
|
||||||
|
|||||||
+107
-15
@@ -21,7 +21,7 @@ sys.path.insert(0, str(PROJECT_ROOT))
|
|||||||
|
|
||||||
from helpers import carica_script
|
from helpers import carica_script
|
||||||
from src.live import shadow as S
|
from src.live import shadow as S
|
||||||
from src.live.execution import DeribitTrader, Fill, RebalanceParziale
|
from src.live.execution import MAX_AMOUNT, DeribitTrader, Fill, RebalanceParziale
|
||||||
|
|
||||||
|
|
||||||
def _fresh_bar() -> str:
|
def _fresh_bar() -> str:
|
||||||
@@ -125,7 +125,7 @@ class _TraderSoloProtezione:
|
|||||||
return {"state": "ok", "stop": 1.0, "amount": 0.0}
|
return {"state": "ok", "stop": 1.0, "amount": 0.0}
|
||||||
|
|
||||||
|
|
||||||
def _run_solo_protezione(canned, monkeypatch):
|
def _run_solo_protezione(canned, monkeypatch, capsys):
|
||||||
mod = carica_script("scripts/live/book_execute.py")
|
mod = carica_script("scripts/live/book_execute.py")
|
||||||
trader = _TraderSoloProtezione()
|
trader = _TraderSoloProtezione()
|
||||||
alerts = []
|
alerts = []
|
||||||
@@ -142,25 +142,34 @@ def _run_solo_protezione(canned, monkeypatch):
|
|||||||
mod._run()
|
mod._run()
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
uscita = e.code
|
uscita = e.code
|
||||||
return trader, alerts, uscita
|
out = capsys.readouterr().out
|
||||||
|
return trader, alerts, uscita, out
|
||||||
|
|
||||||
|
|
||||||
def test_eq_fallback_e_solo_protezione(monkeypatch):
|
def test_eq_fallback_e_solo_protezione(monkeypatch, capsys):
|
||||||
canned = _canned_report(
|
canned = _canned_report(
|
||||||
eq_fallback="equity reale non leggibile (conto flat) -> sizing su paper_cap $2,000")
|
eq_fallback="equity reale non leggibile (conto flat) -> sizing su paper_cap $2,000")
|
||||||
trader, alerts, uscita = _run_solo_protezione(canned, monkeypatch)
|
trader, alerts, uscita, out = _run_solo_protezione(canned, monkeypatch, capsys)
|
||||||
assert trader.rebalance_chiamato is False # nessun ordine dimensionato
|
assert trader.rebalance_chiamato is False # nessun ordine dimensionato
|
||||||
assert trader.visti_sl == ["BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"] # protezione per TUTTI
|
assert trader.visti_sl == ["BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"] # protezione per TUTTI
|
||||||
assert any("solo protezione" in t.lower() for t, _ in alerts)
|
# (#21) UNA sola notify per il guasto, non due: il vecchio testo diceva "sizing su paper_cap",
|
||||||
|
# falso in modo SOLO PROTEZIONE (nessun ordine dimensionato affatto).
|
||||||
|
assert len(alerts) == 1
|
||||||
|
assert "solo protezione" in alerts[0][0].lower()
|
||||||
|
assert "paper_cap" not in alerts[0][0]
|
||||||
|
assert "SOLO PROTEZIONE: nessun ordine di ribilancio" in out # riepilogo finale corretto
|
||||||
assert uscita == 2
|
assert uscita == 2
|
||||||
|
|
||||||
|
|
||||||
def test_skh_error_e_solo_protezione(monkeypatch):
|
def test_skh_error_e_solo_protezione(monkeypatch, capsys):
|
||||||
canned = _canned_report(skh_error="RuntimeError: feed 5m giu")
|
canned = _canned_report(skh_error="RuntimeError: feed 5m giu")
|
||||||
trader, alerts, uscita = _run_solo_protezione(canned, monkeypatch)
|
trader, alerts, uscita, out = _run_solo_protezione(canned, monkeypatch, capsys)
|
||||||
assert trader.rebalance_chiamato is False
|
assert trader.rebalance_chiamato is False
|
||||||
assert trader.visti_sl == ["BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"]
|
assert trader.visti_sl == ["BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"]
|
||||||
assert any("solo protezione" in t.lower() for t, _ in alerts)
|
# (#22) UNA sola notify, non due ("SKH feed fallito" + "SKH non calcolato").
|
||||||
|
assert len(alerts) == 1
|
||||||
|
assert "solo protezione" in alerts[0][0].lower()
|
||||||
|
assert "SOLO PROTEZIONE: nessun ordine di ribilancio" in out
|
||||||
assert uscita == 2
|
assert uscita == 2
|
||||||
|
|
||||||
|
|
||||||
@@ -230,6 +239,9 @@ def test_fill_del_close_non_si_perde_se_lopen_del_flip_fallisce(tmp_path, monkey
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.visti_sl: list[str] = []
|
self.visti_sl: list[str] = []
|
||||||
|
|
||||||
|
def set_open_cap(self, inst, cap_usd, mark):
|
||||||
|
pass
|
||||||
|
|
||||||
def rebalance_signed(self, inst, net, mark, min_usd=5.0):
|
def rebalance_signed(self, inst, net, mark, min_usd=5.0):
|
||||||
if inst.startswith("BTC"):
|
if inst.startswith("BTC"):
|
||||||
raise RebalanceParziale([close_fill], RuntimeError("open KO dopo close"))
|
raise RebalanceParziale([close_fill], RuntimeError("open KO dopo close"))
|
||||||
@@ -285,6 +297,9 @@ def test_position_usd_illeggibile_dopo_un_fill_non_perde_il_fill(tmp_path, monke
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.visti_sl: list[str] = []
|
self.visti_sl: list[str] = []
|
||||||
|
|
||||||
|
def set_open_cap(self, inst, cap_usd, mark):
|
||||||
|
pass
|
||||||
|
|
||||||
def rebalance_signed(self, inst, net, mark, min_usd=5.0):
|
def rebalance_signed(self, inst, net, mark, min_usd=5.0):
|
||||||
return [fill] if inst.startswith("BTC") else []
|
return [fill] if inst.startswith("BTC") else []
|
||||||
|
|
||||||
@@ -330,6 +345,9 @@ def test_parse_cron_log_legge_un_giro_normale_prodotto_dal_nuovo_codice(monkeypa
|
|||||||
fill_btc = Fill("BTC_USDC-PERPETUAL", "buy", 0.003, 0.003, 60000.0, 0.02, "o1", "filled", True, "")
|
fill_btc = Fill("BTC_USDC-PERPETUAL", "buy", 0.003, 0.003, 60000.0, 0.02, "o1", "filled", True, "")
|
||||||
|
|
||||||
class _TraderOk:
|
class _TraderOk:
|
||||||
|
def set_open_cap(self, inst, cap_usd, mark):
|
||||||
|
pass
|
||||||
|
|
||||||
def rebalance_signed(self, inst, net, mark, min_usd=5.0):
|
def rebalance_signed(self, inst, net, mark, min_usd=5.0):
|
||||||
return [fill_btc] if inst.startswith("BTC") else []
|
return [fill_btc] if inst.startswith("BTC") else []
|
||||||
|
|
||||||
@@ -368,6 +386,9 @@ def test_parse_cron_log_legge_il_fill_di_un_RebalanceParziale(monkeypatch, capsy
|
|||||||
"filled", True, "")
|
"filled", True, "")
|
||||||
|
|
||||||
class _TraderFlip:
|
class _TraderFlip:
|
||||||
|
def set_open_cap(self, inst, cap_usd, mark):
|
||||||
|
pass
|
||||||
|
|
||||||
def rebalance_signed(self, inst, net, mark, min_usd=5.0):
|
def rebalance_signed(self, inst, net, mark, min_usd=5.0):
|
||||||
if inst.startswith("BTC"):
|
if inst.startswith("BTC"):
|
||||||
raise RebalanceParziale([close_fill], RuntimeError("open KO"))
|
raise RebalanceParziale([close_fill], RuntimeError("open KO"))
|
||||||
@@ -402,8 +423,8 @@ def test_parse_cron_log_legge_il_fill_di_un_RebalanceParziale(monkeypatch, capsy
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# set_open_cap (da U1, stesso rilascio): chiamato SOLO se il trader lo espone (hasattr — U1 e
|
# set_open_cap (#18): chiamato SEMPRE, diretto (l'hasattr era solo per il periodo prima del
|
||||||
# U2 si uniscono dopo questo rilascio), subito prima di rebalance_signed.
|
# merge con U1, in cui DeribitTrader non lo aveva ancora — tolto qui).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
def test_set_open_cap_chiamato_se_il_trader_lo_espone(monkeypatch):
|
def test_set_open_cap_chiamato_se_il_trader_lo_espone(monkeypatch):
|
||||||
mod = carica_script("scripts/live/book_execute.py")
|
mod = carica_script("scripts/live/book_execute.py")
|
||||||
@@ -435,8 +456,10 @@ def test_set_open_cap_chiamato_se_il_trader_lo_espone(monkeypatch):
|
|||||||
assert trader.cap_visti == [("BTC_USDC-PERPETUAL", 300.0 * 1.25, 60000.0)] # solo BTC ha un ordine
|
assert trader.cap_visti == [("BTC_USDC-PERPETUAL", 300.0 * 1.25, 60000.0)] # solo BTC ha un ordine
|
||||||
|
|
||||||
|
|
||||||
def test_set_open_cap_non_richiesto_se_il_trader_non_lo_espone(monkeypatch):
|
def test_trader_senza_set_open_cap_fallisce_isolato_non_lascia_morire_il_giro(monkeypatch, capsys):
|
||||||
"""hasattr: un trader senza set_open_cap (oggi, prima del merge con U1) non deve rompersi."""
|
"""Post-merge: `set_open_cap` si chiama diretto, senza hasattr (era solo transitorio prima
|
||||||
|
del merge con U1). Un trader che non lo espone ora fa fallire QUELL'asset (isolamento per
|
||||||
|
asset, gia' esistente) — non l'intero giro: ETH resta elaborato normalmente."""
|
||||||
mod = carica_script("scripts/live/book_execute.py")
|
mod = carica_script("scripts/live/book_execute.py")
|
||||||
|
|
||||||
class _TraderSenzaCap:
|
class _TraderSenzaCap:
|
||||||
@@ -447,7 +470,7 @@ def test_set_open_cap_non_richiesto_se_il_trader_non_lo_espone(monkeypatch):
|
|||||||
def ensure_disaster_sl(self, inst, sl_pct):
|
def ensure_disaster_sl(self, inst, sl_pct):
|
||||||
return {"state": "ok", "stop": 1.0, "amount": 0.0}
|
return {"state": "ok", "stop": 1.0, "amount": 0.0}
|
||||||
|
|
||||||
canned = _canned_report()
|
canned = _canned_report() # BTC ha un ordine (set_open_cap si chiama), ETH order=None
|
||||||
monkeypatch.setattr(mod, "book_report", lambda **k: canned)
|
monkeypatch.setattr(mod, "book_report", lambda **k: canned)
|
||||||
monkeypatch.setattr(mod, "notify", lambda t, d=None: None)
|
monkeypatch.setattr(mod, "notify", lambda t, d=None: None)
|
||||||
monkeypatch.setattr(mod, "DeribitTrader", lambda *a, **k: _TraderSenzaCap())
|
monkeypatch.setattr(mod, "DeribitTrader", lambda *a, **k: _TraderSenzaCap())
|
||||||
@@ -456,4 +479,73 @@ def test_set_open_cap_non_richiesto_se_il_trader_non_lo_espone(monkeypatch):
|
|||||||
disaster_sl_pct=0.30, max_data_age_days=2.0,
|
disaster_sl_pct=0.30, max_data_age_days=2.0,
|
||||||
skh_feed_max_age_min=30.0))
|
skh_feed_max_age_min=30.0))
|
||||||
monkeypatch.setattr(sys, "argv", ["book_execute.py", "--execute"])
|
monkeypatch.setattr(sys, "argv", ["book_execute.py", "--execute"])
|
||||||
mod._run() # non deve sollevare AttributeError
|
uscita = None
|
||||||
|
try:
|
||||||
|
mod._run()
|
||||||
|
except SystemExit as e:
|
||||||
|
uscita = e.code
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "AttributeError" in out # isolato: si vede nel log, non muore il giro
|
||||||
|
assert "asset falliti in questo giro: BTC" in out
|
||||||
|
assert uscita == 2
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# #24 — ensure_disaster_sl "unknown" in book_execute: NON e' "naked" (non si e' tolto nulla),
|
||||||
|
# ma il giro resta degradato -> notify dedicata e exit 2.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def test_ensure_disaster_sl_unknown_notifica_e_degrada_il_giro(monkeypatch, capsys):
|
||||||
|
mod = carica_script("scripts/live/book_execute.py")
|
||||||
|
|
||||||
|
class _TraderUnknown:
|
||||||
|
def set_open_cap(self, inst, cap_usd, mark):
|
||||||
|
pass
|
||||||
|
def rebalance_signed(self, inst, net, mark, min_usd=5.0):
|
||||||
|
return []
|
||||||
|
def position_usd(self, inst):
|
||||||
|
return 0.0
|
||||||
|
def ensure_disaster_sl(self, inst, sl_pct):
|
||||||
|
if inst.startswith("ETH"):
|
||||||
|
return {"state": "unknown", "notes": "RuntimeError: 502 su get_positions"}
|
||||||
|
return {"state": "ok", "stop": 1.0, "amount": 0.0}
|
||||||
|
|
||||||
|
trader = _TraderUnknown()
|
||||||
|
canned = _canned_report() # BTC ha un ordine, ETH order=None -> ETH e' l'asset "unknown"
|
||||||
|
alerts = []
|
||||||
|
monkeypatch.setattr(mod, "book_report", lambda **k: canned)
|
||||||
|
monkeypatch.setattr(mod, "notify", lambda t, d=None: alerts.append((t, d)))
|
||||||
|
monkeypatch.setattr(mod, "DeribitTrader", lambda *a, **k: trader)
|
||||||
|
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,
|
||||||
|
skh_feed_max_age_min=30.0))
|
||||||
|
monkeypatch.setattr(sys, "argv", ["book_execute.py", "--execute"])
|
||||||
|
|
||||||
|
uscita = None
|
||||||
|
try:
|
||||||
|
mod._run()
|
||||||
|
except SystemExit as e:
|
||||||
|
uscita = e.code
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
|
||||||
|
assert uscita == 2
|
||||||
|
assert "ETH: protezione NON VERIFICATA" in out
|
||||||
|
notifiche = [d for t, d in alerts if "non verificabile" in t.lower()]
|
||||||
|
assert len(notifiche) == 1
|
||||||
|
assert notifiche[0]["asset"] == "ETH"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# execution.py::_submit (#21) — legge il tetto con getattr: un DeribitTrader creato senza
|
||||||
|
# passare da __init__ (object.__new__, come qui) non deve rompersi con AttributeError perche'
|
||||||
|
# gli manca l'attributo _open_cap.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def test_submit_su_trader_senza_open_cap_non_solleva_attributeerror(monkeypatch):
|
||||||
|
t = object.__new__(DeribitTrader) # bypassa __init__: niente self._open_cap
|
||||||
|
monkeypatch.setattr(t, "_post", lambda path, payload: {
|
||||||
|
"order": {"order_id": "1", "order_state": "filled",
|
||||||
|
"filled_amount": payload["amount"]},
|
||||||
|
"trades": [{"price": 1.0, "amount": payload["amount"], "fee": 0.0}]})
|
||||||
|
dentro = MAX_AMOUNT["ETH_USDC-PERPETUAL"] * 0.5
|
||||||
|
f = t._submit("ETH_USDC-PERPETUAL", "buy", dentro, reduce_only=False, label="book-open")
|
||||||
|
assert f.verified is True # arrivato in fondo: niente AttributeError sul cap
|
||||||
|
|||||||
Reference in New Issue
Block a user