merge lavoro/goldrake-u9 (secondo giro 27/09)

This commit is contained in:
2026-09-27 15:40:42 +00:00
10 changed files with 217 additions and 87 deletions
+21
View File
@@ -150,6 +150,27 @@ def test_weights_match_backtest_sleeves():
assert abs((W_TP01 + W_SKH) - 1.0) < 1e-12
def test_deribit_book_sleeves_deriva_i_pesi_da_book_non_li_ridichiara():
"""#35 B12 — P1: stesso pattern di test_r0909_libro_nei_crolli.test_P1_i_pesi_sono_QUELLI_del_book_live_non_una_copia.
`test_weights_match_backtest_sleeves` sopra passerebbe anche con due letterali per coincidenza
di valore (0.75/0.25 riscritti a mano): qui si verifica l'IDENTITA' con B.W_TP01/B.W_SKH e
che il letterale non sia piu' nella fonte di `deribit_book_sleeves`."""
import inspect
import re
import src.live.book as B
from src.portfolio.sleeves import deribit_book_sleeves
pesi = {s.name.split("_")[0]: s.weight for s in deribit_book_sleeves()}
assert pesi["TP01"] is B.W_TP01 or pesi["TP01"] == B.W_TP01
assert pesi["SKH01"] is B.W_SKH or pesi["SKH01"] == B.W_SKH
src = inspect.getsource(deribit_book_sleeves)
assert "from src.live import book" in src, "i pesi devono derivare da src.live.book (P1)"
assert not re.search(r"weight\s*=\s*0\.75", src), "peso TP01 ancora cablato"
assert not re.search(r"weight\s*=\s*0\.25", src), "peso SKH01 ancora cablato"
# ---------------------------------------------------------------------------
# Costruzione ordine: side, reduce_only, flip, close, soglia minima.
# ---------------------------------------------------------------------------
+60 -1
View File
@@ -155,7 +155,15 @@ def test_un_tentativo_ogni_sei_ore():
assert W.puo_riconvertire([{"ts": now - 1000, "riconversione": None}], now, True, 1.0, 0.99)[0]
def _finto_lettore_quota(valori):
"""Ritorna i valori dati in ordine, uno per chiamata (per monkeypatch di _leggi_quota_usde)."""
it = iter(valori)
return lambda: next(it)
def test_riconverti_lancia_lo_script_vero_con_esegui(monkeypatch):
# rc==0: "ok" a prescindere dalla quota (lo script vero ha gia' le sue guardie, P15) — la
# quota qui non si muove, per mostrare che il caso "ok" non dipende dalla rilettura.
chiamate = []
class R:
@@ -166,10 +174,61 @@ def test_riconverti_lancia_lo_script_vero_con_esegui(monkeypatch):
return R()
monkeypatch.setattr(W.subprocess, "run", finto_run)
monkeypatch.setattr(W, "_leggi_quota_usde", _finto_lettore_quota([0.67, 0.67]))
e = W.riconverti(0.67)
assert e["ok"] and chiamate and chiamate[0][1].endswith("usde_convert.py")
assert e["ok"] and e["esito"] == "ok" and chiamate and chiamate[0][1].endswith("usde_convert.py")
assert "--esegui" in chiamate[0] and "0.6700" in chiamate[0]
assert e["coda"][-1].startswith(" ESITO")
assert e["quota_prima"] == 0.67 and e["quota_dopo"] == 0.67
def test_riconverti_rc_diverso_da_zero_con_quota_mossa_e_parziale(monkeypatch):
# #35 B10: rc!=0 (es. ordine parzialmente fillato poi rifiutato) ma la quota si e' mossa
# per davvero -> "parziale", non "fallita": la vendita/acquisto e' avvenuto, solo non finito.
class R:
returncode, stdout, stderr = 1, "", "book illeggibile a meta' piano\n"
monkeypatch.setattr(W.subprocess, "run", lambda cmd, **kw: R())
monkeypatch.setattr(W, "_leggi_quota_usde", _finto_lettore_quota([0.75, 0.70]))
e = W.riconverti(0.64)
assert e["esito"] == "parziale" and e["ok"] is False
assert e["quota_prima"] == 0.75 and e["quota_dopo"] == 0.70
def test_riconverti_rc_diverso_da_zero_con_quota_ferma_e_fallita(monkeypatch):
class R:
returncode, stdout, stderr = 1, "", "prezzo fuori banda: niente ordini\n"
monkeypatch.setattr(W.subprocess, "run", lambda cmd, **kw: R())
monkeypatch.setattr(W, "_leggi_quota_usde", _finto_lettore_quota([0.75, 0.751]))
e = W.riconverti(0.64)
assert e["esito"] == "fallita" and e["ok"] is False
def test_riconverti_timeout_con_quota_mossa_e_parziale(monkeypatch):
# #35 B10: il subprocess viene ucciso a TIMEOUT_CONVERT_S ma alcuni ordini erano gia' fillati
# prima del kill -> oggi (ok=rc==0) diventava "FALLITA", corretto e' "parziale".
def finto_run(cmd, **kw):
raise W.subprocess.TimeoutExpired(cmd, kw.get("timeout"))
monkeypatch.setattr(W.subprocess, "run", finto_run)
monkeypatch.setattr(W, "_leggi_quota_usde", _finto_lettore_quota([0.80, 0.72]))
e = W.riconverti(0.64)
assert e["rc"] is None and e["esito"] == "parziale" and e["ok"] is False
assert e["coda"][0].startswith("timeout dopo")
def test_riconverti_rilettura_dopo_fallita_e_fallita_esplicita(monkeypatch):
# se non si riesce a rileggere la quota dopo, non si puo' dire "parziale": si dichiara
# l'incertezza invece di dedurre un movimento zero che non si e' verificato.
class R:
returncode, stdout, stderr = 1, "", "errore\n"
monkeypatch.setattr(W.subprocess, "run", lambda cmd, **kw: R())
monkeypatch.setattr(W, "_leggi_quota_usde", _finto_lettore_quota([0.75, None]))
e = W.riconverti(0.64)
assert e["esito"] == "fallita (quota dopo non leggibile)" and e["ok"] is False
assert e["quota_dopo"] is None
def test_il_monitor_e_registrato_in_monitor_health():
+12 -32
View File
@@ -8,17 +8,17 @@ Cosa difendono, in ordine di gravita':
da `place-failed`. Sono due guasti diversi (P5) e solo uno lascia la posizione scoperta.
2. **isolamento per asset**: un guasto su BTC non deve impedire ad ETH di essere protetto.
3. **la diagnosi**: manutenzione, gateway e "non vedo" devono essere distinguibili (P4/P5).
4. **P9**: `atteso` declassa la gravita' SOLO su evidenza di manutenzione, e SOLO dentro lo slot.
4. **`atteso` e' sempre False** (#35 B11): il ramo che lo declassava (finestra di release del
martedi') e' stato rimosso perche' l'unico chiamante (cron :47) non ci cadeva mai dentro.
"""
import sys
from datetime import datetime, timezone
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
from src.live.venue_probe import (V_GATEWAY, V_IGNOTO, V_MANUTENZIONE, V_VENUE_GIU, diagnose,
errori_dal_report, in_release_window)
errori_dal_report)
MANUTENZIONE_GREZZA = ('HTTP 503 {"jsonrpc":"2.0","error":'
'{"message":"system_maintenance","code":11051}}')
@@ -94,39 +94,19 @@ def test_la_prova_viaggia_sempre_con_il_verdetto():
# ---------------------------------------------------------------------------
# 2. P9 — `atteso` declassa, ma solo su EVIDENZA e solo DENTRO lo slot.
# 2. `atteso` e' sempre False (#35 B11) — nessun verdetto, manutenzione compresa, si declassa.
# ---------------------------------------------------------------------------
def test_finestra_di_release_martedi_nove_utc():
dentro = datetime(2026, 8, 25, 9, 7, tzinfo=timezone.utc) # martedi', l'episodio vero
assert in_release_window(dentro) is True
assert in_release_window(datetime(2026, 8, 25, 9, 0, tzinfo=timezone.utc)) is True
assert in_release_window(datetime(2026, 8, 25, 9, 29, tzinfo=timezone.utc)) is True
assert in_release_window(datetime(2026, 8, 25, 9, 30, tzinfo=timezone.utc)) is False # sforata
assert in_release_window(datetime(2026, 8, 25, 8, 59, tzinfo=timezone.utc)) is False
assert in_release_window(datetime(2026, 8, 26, 9, 7, tzinfo=timezone.utc)) is False # mercoledi'
def test_manutenzione_dentro_lo_slot_e_attesa_e_declassa():
d = diagnose([], now=datetime(2026, 8, 25, 9, 7, tzinfo=timezone.utc),
sonda=_sonda_manutenzione)
assert d.verdetto == V_MANUTENZIONE and d.atteso is True
assert d.gravita == "ℹ️" # declassato, NON silenziato
assert "ATTESO" in d.riga()
def test_manutenzione_che_sfora_lo_slot_torna_rumorosa():
"""Il 18/08 Deribit annuncio' 15-30 minuti e resto' giu' oltre l'ora: alle 10:07 la
manutenzione non era piu' l'evento annunciato, ed e' di nuovo una notizia."""
d = diagnose([], now=datetime(2026, 8, 18, 10, 7, tzinfo=timezone.utc),
sonda=_sonda_manutenzione)
def test_manutenzione_non_si_declassa_piu_atteso_sempre_falso():
"""Il ramo che declassava (finestra di release del martedi') e' rimosso: una manutenzione
riconosciuta dalla sonda resta comunque `atteso is False` e alla gravita' piena."""
d = diagnose([], sonda=_sonda_manutenzione)
assert d.verdetto == V_MANUTENZIONE and d.atteso is False
assert d.gravita != "ℹ️"
assert d.gravita == "⚠️"
assert "ATTESO" not in d.riga()
def test_un_guasto_vero_di_martedi_mattina_non_viene_declassato():
"""Il rischio della finestra: costruire il silenzio proprio nell'ora in cui serve. Un gateway
rotto dentro lo slot di release deve restare al massimo della gravita'."""
d = diagnose([], now=datetime(2026, 8, 25, 9, 7, tzinfo=timezone.utc), sonda=_sonda_ok)
def test_un_guasto_vero_non_e_mai_atteso():
d = diagnose(["BTC: fallback close (HTTPError)"], sonda=_sonda_ok)
assert d.verdetto == V_GATEWAY and d.atteso is False and d.gravita == "🛑"