de83909db9
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
115 lines
5.3 KiB
Python
115 lines
5.3 KiB
Python
"""Lock dello STALENESS-GATE dell'esecutore live (cablato 2026-07-25).
|
|
|
|
PERCHE'. Il 2026-07-14 alle 14:00 UTC il book ha comprato ETH ($75) con l'ultima barra del feed
|
|
certificato ferma al 2026-07-08: feed congelato da 6 giorni (diario 2026-07-15-feed-freeze). I due
|
|
gate esistenti non potevano vederlo — il conto ERA online e la posizione ERA leggibile: proteggono
|
|
dai problemi di CONTO, non da un feed morto. Il diario raccomandava un alert; qui il gate e'
|
|
BLOCCANTE, coerente con gli altri due (`non opero a cieco`).
|
|
|
|
Questi test bloccano la soglia e il comportamento del calcolo d'eta'. Non toccano la rete.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
sys.path.insert(0, str(ROOT / "scripts" / "live"))
|
|
|
|
book_execute = pytest.importorskip("book_execute")
|
|
|
|
|
|
def test_soglia_presente_nel_config_live():
|
|
cfg = json.loads((ROOT / "config" / "live.json").read_text())
|
|
assert cfg.get("max_data_age_days") == 2, "soglia di staleness assente o cambiata"
|
|
assert cfg.get("execution_enabled") is True
|
|
assert cfg.get("disaster_sl_pct") == 0.30
|
|
|
|
|
|
def test_default_sicuro_se_il_config_non_la_indica():
|
|
cfg = book_execute.load_config()
|
|
assert cfg["max_data_age_days"] > 0
|
|
|
|
|
|
def test_eta_barra_fresca_e_sotto_soglia():
|
|
oggi = pd.Timestamp.now(tz="UTC").normalize()
|
|
eta = book_execute._data_age_days(oggi)
|
|
assert eta is not None and eta < 2.0
|
|
|
|
|
|
def test_eta_del_caso_reale_del_14_luglio_supera_la_soglia():
|
|
"""Il caso che ha motivato il gate: barra 07-08, esecuzione il 07-14 -> 6 giorni."""
|
|
eta = (pd.Timestamp("2026-07-14 14:00", tz="UTC") - pd.Timestamp("2026-07-08", tz="UTC"))
|
|
assert eta.total_seconds() / 86400.0 > 2.0, "il gate non avrebbe fermato l'incidente reale"
|
|
|
|
|
|
def test_data_illeggibile_e_trattata_come_stantia():
|
|
"""None = non so leggere la data -> meglio non operare che operare su ignoto."""
|
|
for cattivo in (None, "", "non-una-data", object()):
|
|
assert book_execute._data_age_days(cattivo) is None
|
|
|
|
|
|
def test_funzionale_feed_stantio_blocca_lesecuzione_e_allerta(monkeypatch, capsys):
|
|
"""Il test che conta: conto ONLINE, posizione LEGGIBILE, ordine PRESENTE — cioe' la situazione
|
|
esatta del 2026-07-14 — ma feed vecchio. Non deve partire nessun ordine e deve arrivare l'alert.
|
|
"""
|
|
from helpers import carica_script
|
|
mod = carica_script("scripts/live/book_execute.py", "book_execute_iso")
|
|
|
|
canned = dict(
|
|
last_data="2026-07-08", online=True, real_equity=598.0, equity=598.0,
|
|
eq_basis="mainnet USDC", cap_per_asset=300.0, skh_error=None, pos_error=None,
|
|
assets=[dict(asset="ETH", instrument="ETH_USDC-PERPETUAL", tp_frac=0.0, skh_sign=1,
|
|
skh_state="flat", net_target=75.0, position_usd=0.0, mark=1869.0,
|
|
order=dict(side="buy"))],
|
|
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, max_data_age_days=2.0))
|
|
monkeypatch.setattr(sys, "argv", ["book_execute.py", "--execute"]) # ARMATO + execute
|
|
|
|
def boom(*a, **k):
|
|
raise AssertionError("DeribitTrader costruito: il gate NON ha fermato il feed stantio")
|
|
monkeypatch.setattr(mod, "DeribitTrader", boom)
|
|
|
|
mod._run() # non deve sollevare: il gate esce prima
|
|
|
|
out = capsys.readouterr().out
|
|
assert "FEED STANTIO" in out
|
|
assert "rebuild_history" in out, "il messaggio deve dire come sbloccare"
|
|
assert any("FEED STANTIO" in t for t, _ in alerts), "alert Telegram non inviato"
|
|
|
|
|
|
def test_config_senza_la_chiave_non_esplode(monkeypatch, capsys):
|
|
"""Una config priva di max_data_age_days deve ricadere sul default, non sollevare KeyError
|
|
dentro il percorso con soldi veri."""
|
|
from helpers import carica_script
|
|
mod = carica_script("scripts/live/book_execute.py", "book_execute_iso2")
|
|
canned = dict(last_data=str(pd.Timestamp.now(tz="UTC").normalize().date()), online=True,
|
|
real_equity=598.0, equity=598.0, eq_basis="t", cap_per_asset=300.0,
|
|
skh_error=None, pos_error=None, assets=[], orders=[])
|
|
monkeypatch.setattr(mod, "book_report", lambda **k: canned)
|
|
monkeypatch.setattr(mod, "notify", lambda title, det=None: None)
|
|
monkeypatch.setattr(mod, "load_config",
|
|
lambda: dict(execution_enabled=False, min_order_usd=5.0, disaster_sl_pct=0.3))
|
|
monkeypatch.setattr(sys, "argv", ["book_execute.py"])
|
|
mod._run() # nessun KeyError
|
|
assert "FEED STANTIO" not in capsys.readouterr().out
|
|
|
|
|
|
def test_il_gate_blocca_prima_di_creare_il_trader():
|
|
"""Ordine dei controlli: il blocco per feed stantio deve stare PRIMA di DeribitTrader(),
|
|
altrimenti si aprirebbe comunque una sessione autenticata verso il conto reale."""
|
|
src = (ROOT / "scripts" / "live" / "book_execute.py").read_text()
|
|
i_gate = src.index("FEED STANTIO")
|
|
i_trader = src.index("DeribitTrader() if do_execute")
|
|
assert i_gate < i_trader, "il gate di staleness deve precedere la creazione del trader"
|