Files
PythagorasGoal/tests/test_book_resilienza_venue.py
T
Adriano Dal Pastro de83909db9 un flag sconosciuto non e' l'azione di default: guardia su 18 script, indice USDE orario, pulizia
MISURATO oggi durante la revisione: `trades_db.py --help` non stampava l'uso, cadeva in sync()
e riscriveva meta.ultimo_sync. Nessuno dei 18 script di scripts/live/ usava argparse: un flag
sbagliato era il ramo else. Su journal.py avrebbe scritto pagina e riga di DB, su analista.py
avrebbe speso una chiamata al modello e mandato un Telegram.

- src/live/cli.valida: prima istruzione di ogni __main__, prima di connect()/sync/rete.
  --help -> 0 con l'uso; flag ignoto, valore mancante o posizionale -> 2 con l'elenco dei
  previsti (P4). NIENTE argparse: cambierebbe messaggi, codici d'uscita e --help di script
  che il cron gia' chiama.
- 18 script cablati (i 3 che scrivono + 14 + cc01), flag invariati.
- tests/test_cli_flag.py (30): elenco DERIVATO dalla cartella (P1), valida come prima
  istruzione, uso che documenta i flag, e i flag che il CRON usa davvero restano accettati
  (P15/P16); end-to-end su --help e flag ignoto con trades.db non toccato (M15).
  Verificato a mano: monitor_health --quiet, trades_db --sync --quiet, book_execute dry-run.

Debito §5.15, primo passo: balance_watch (orario) registra `usde_usdc` a ogni campione — None
con la ragione se illeggibile, mai 1,0. Il cablaggio nel bound quando la serie ha storia.

Pulizia dalla revisione: tests/helpers.carica_script al posto della 15a copia del loader
importlib (5 file del libro live); il fill di prova via upsert_fills invece di un INSERT che
lasciava verified NULL; asserzioni non ancorate al padding; movimenti_capitale accetta le
righe gia' lette (una SELECT invece di due ai due lati di una scrittura del cron).

Test 910 verdi (+52). Diario 2026-09-02c.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDJsH3iDSaBns3ccpPBwiu
2026-09-02 15:49:40 +00:00

233 lines
9.7 KiB
Python

"""Le due riparazioni del 2026-08-25 sul percorso con soldi veri, provate contro il codice che
dovrebbe eseguirle (P15) invece che discusse:
A. **La finestra scoperta del disaster-SL.** `ensure_disaster_sl` cancella i bracket incoerenti
PRIMA di ripiazzarne uno: se il venue cade in mezzo, la posizione resta senza alcuno stop.
Prima l'eccezione risaliva fino a `main()`, e il guasto peggiore (posizione SCOPERTA) aveva
la stessa faccia di un errore qualunque. Ora ha uno stato suo: `naked`.
B. **L'isolamento per asset.** Il 2026-07-21 alle 09:00 UTC un 502 dentro `ensure_disaster_sl`
su BTC ha ucciso il giro intero: ETH non e' stato nemmeno guardato — niente ribilancio e,
soprattutto, nessuna verifica della sua protezione.
Nessun test tocca la rete.
"""
import importlib.util
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
from src.live.execution import DISASTER_LABEL, DeribitTrader, Fill
def _fresh_bar() -> str:
import pandas as _pd
return str(_pd.Timestamp.now(tz="UTC").normalize().date())
# ---------------------------------------------------------------------------
# A. Il disaster-SL: `naked` esiste, ed e' distinto da `place-failed`.
# ---------------------------------------------------------------------------
class _FakeSL(DeribitTrader):
"""DeribitTrader senza rete: si controlla quante volte il piazzamento fallisce."""
def __init__(self, pos_usd, brackets, fallimenti_piazzamento=0, verified=True):
self._pos = float(pos_usd)
self._brackets = list(brackets)
self._da_fallire = int(fallimenti_piazzamento)
self._verified = verified
self.cancellati = []
self.piazzamenti = 0
def position_usd(self, instrument):
return self._pos
def open_orders(self, instrument):
return self._brackets
def cancel_order(self, order_id):
self.cancellati.append(order_id)
return {}
def mark_price(self, instrument):
return 80000.0
def place_disaster_sl(self, instrument, side_held, amount, stop_price, label=DISASTER_LABEL):
self.piazzamenti += 1
if self._da_fallire > 0:
self._da_fallire -= 1
raise RuntimeError("502 Server Error: Bad Gateway")
return Fill(instrument=instrument, side="sell", amount=amount, filled=amount,
price=stop_price, fee_usdc=0.0, order_id="sl1", state="open",
verified=self._verified, notes="")
def _bracket_incoerente():
"""Un bracket con size sbagliata -> forza il ramo 'cancella e ricostruisci'."""
return [{"order_id": "vecchio", "label": DISASTER_LABEL, "amount": 0.0001,
"trigger_price": 1.0}]
def test_ripiazzamento_ok_al_primo_colpo_resta_placed():
"""Il percorso sano non deve essere cambiato dalla riparazione."""
t = _FakeSL(pos_usd=112.0, brackets=_bracket_incoerente())
ds = t.ensure_disaster_sl("BTC_USDC-PERPETUAL", 0.30)
assert ds["state"] == "placed"
assert t.cancellati == ["vecchio"] and t.piazzamenti == 1
def test_un_fallimento_isolato_viene_ritentato_e_la_posizione_resta_protetta():
"""La posizione e' scoperta fra il cancel e il place: si riprova SUBITO, non al giro dopo."""
t = _FakeSL(pos_usd=112.0, brackets=_bracket_incoerente(), fallimenti_piazzamento=1)
ds = t.ensure_disaster_sl("BTC_USDC-PERPETUAL", 0.30)
assert ds["state"] == "placed" # il secondo tentativo ha funzionato
assert t.piazzamenti == 2
def test_due_fallimenti_lasciano_lo_stato_naked_non_una_eccezione():
"""IL TEST CHE CONTA. Prima qui volava un'eccezione e moriva il giro (e l'altro asset).
Ora: stato `naked`, con dentro il fatto che i bracket erano gia' stati cancellati."""
t = _FakeSL(pos_usd=112.0, brackets=_bracket_incoerente(), fallimenti_piazzamento=2)
ds = t.ensure_disaster_sl("BTC_USDC-PERPETUAL", 0.30)
assert ds["state"] == "naked"
assert ds["cancelled"] == 1 # la protezione era stata TOLTA
assert "502" in ds["notes"] # il motivo si registra (P3)
assert t.piazzamenti == 2
def test_naked_e_place_failed_sono_guasti_DIVERSI():
"""P5: distinguere guasti diversi anche quando l'azione e' la stessa. `place-failed` = l'ordine
e' partito ma non e' verificato; `naked` = la protezione e' stata rimossa e non rimessa."""
t = _FakeSL(pos_usd=112.0, brackets=_bracket_incoerente(), verified=False)
assert t.ensure_disaster_sl("BTC_USDC-PERPETUAL", 0.30)["state"] == "place-failed"
t2 = _FakeSL(pos_usd=112.0, brackets=_bracket_incoerente(), fallimenti_piazzamento=2)
assert t2.ensure_disaster_sl("BTC_USDC-PERPETUAL", 0.30)["state"] == "naked"
def test_posizione_flat_non_puo_diventare_naked():
"""A libro flat non c'e' niente da proteggere: il ramo non deve nemmeno provarci."""
t = _FakeSL(pos_usd=0.0, brackets=_bracket_incoerente(), fallimenti_piazzamento=2)
ds = t.ensure_disaster_sl("BTC_USDC-PERPETUAL", 0.30)
assert ds["state"] == "flat" and t.piazzamenti == 0
# ---------------------------------------------------------------------------
# B. Isolamento per asset dentro book_execute._run().
# ---------------------------------------------------------------------------
def _carica_book_execute():
from helpers import carica_script
return carica_script("scripts/live/book_execute.py")
def _report_due_asset():
def _asset(nome, inst):
return dict(asset=nome, instrument=inst, tp_frac=0.45, skh_sign=0, skh_state="flat",
net_target=113.0, position_usd=112.0, mark=80000.0, order=None)
return dict(last_data=_fresh_bar(), online=True, real_equity=667.0, equity=667.0,
eq_basis="test", cap_per_asset=334.0,
assets=[_asset("BTC", "BTC_USDC-PERPETUAL"), _asset("ETH", "ETH_USDC-PERPETUAL")],
orders=[])
class _DiagnosiFinta:
verdetto = "GATEWAY"
perche = "l'API pubblica Deribit risponde: il guasto e' nel nostro gateway"
atteso = False
prova = "prova"
gravita = "🛑"
riparabile_da_noi = True
def riga(self):
return f"{self.verdetto}{self.perche}"
def _diagnosi_finta(*a, **k):
return _DiagnosiFinta()
class _TraderCheEsplodeSuBTC:
"""Riproduce il 2026-07-21: 502 dentro ensure_disaster_sl su BTC, ETH sano."""
def __init__(self):
self.visti = []
def rebalance_signed(self, *a, **k):
return []
def position_usd(self, instrument):
return 112.0
def ensure_disaster_sl(self, instrument, sl_pct):
self.visti.append(instrument)
if instrument.startswith("BTC"):
raise RuntimeError("502 Server Error: Bad Gateway for url: .../get_positions")
return {"state": "ok", "stop": 1755.8, "amount": 0.06}
def test_un_asset_che_esplode_non_impedisce_all_altro_di_essere_protetto(monkeypatch, capsys):
"""IL TEST CHE CONTA. Prima ETH non veniva nemmeno guardato."""
mod = _carica_book_execute()
trader = _TraderCheEsplodeSuBTC()
alerts = []
monkeypatch.setattr(mod, "book_report", lambda **k: _report_due_asset())
monkeypatch.setattr(mod, "notify", lambda t, d=None: alerts.append((t, d)))
monkeypatch.setattr(mod, "DeribitTrader", lambda *a, **k: trader)
monkeypatch.setattr(mod, "diagnose", _diagnosi_finta)
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"])
try:
mod._run()
except SystemExit as e:
assert e.code == 2 # giro DEGRADATO, dichiarato all'uscita
out = capsys.readouterr().out
assert trader.visti == ["BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"] # ETH e' stato guardato
assert "PASSO ALL'ASSET SUCCESSIVO" in out
assert "asset falliti in questo giro: BTC" in out
assert any("asset BTC fallito" in t for t, _ in alerts)
def test_posizione_scoperta_esce_con_codice_due_e_allarme_massimo(monkeypatch, capsys):
"""Una posizione senza stop on-book non e' MAI un evento atteso: gravita' massima sempre."""
mod = _carica_book_execute()
class _TraderNaked:
def rebalance_signed(self, *a, **k):
return []
def position_usd(self, instrument):
return 112.0
def ensure_disaster_sl(self, instrument, sl_pct):
return {"state": "naked", "stop": 56000.0, "amount": 0.0014, "cancelled": 1,
"notes": "bracket cancellati (1) e ripiazzamento fallito 2 volte"}
alerts = []
monkeypatch.setattr(mod, "book_report", lambda **k: _report_due_asset())
monkeypatch.setattr(mod, "notify", lambda t, d=None: alerts.append((t, d)))
monkeypatch.setattr(mod, "DeribitTrader", lambda *a, **k: _TraderNaked())
monkeypatch.setattr(mod, "diagnose", _diagnosi_finta)
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 "POSIZIONI SCOPERTE" in out
titoli = [t for t, _ in alerts]
assert any("POSIZIONE SCOPERTA" in t and t.startswith("🚨") for t in titoli)
assert sum("POSIZIONE SCOPERTA" in t for t in titoli) == 2 # uno per asset, nessuno perso