merge lavoro/goldrake-u9 (secondo giro 27/09)
This commit is contained in:
@@ -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.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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
@@ -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 == "🛑"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user