live: diagnosi del guasto venue, disaster-SL scoperto, isolamento per asset, cron :47
Nato da "stato trades" durante una manutenzione Deribit (system_maintenance 11051,
08:57-09:20 UTC). Contati i log invece che ricordarli: 1.499 giri dal 23/06, 3 con
manutenzione, 5 traceback duri — e 4 dei 5 sono il NOSTRO gateway, non il venue.
Le release Deribit escono il martedi' alle 09:00 UTC: il cron al :07 ci cadeva dentro
per costruzione (21/07, 11/08, 18/08, 25/08).
DIAGNOSI (src/live/venue_probe.py, nuovo)
Sonda l'API PUBBLICA Deribit senza gateway e senza credenziali, e classifica il guasto:
VENUE_MANUTENZIONE / VENUE_GIU / GATEWAY / IGNOTO. Prima ogni causa stampava la stessa
riga ("conto non leggibile (offline)") con nota di diagnosi CABLATA — P4 violata, e il
18/08 il codice 11051 era gia' dentro il processo senza arrivare a chi decideva la
gravita'. Parte solo dopo un guasto: sul percorso sano costa zero. P1 rispettata: la
firma 11051 si importa da venue_watch.is_maintenance, non si ridichiara.
P9: la manutenzione dentro lo slot declassa il titolo a info, ma solo su EVIDENZA della
sonda (mai sull'orologio) e solo dentro la durata annunciata — oltre, RIALZA. Un gateway
rotto di martedi' mattina resta al massimo.
DISASTER-SL SCOPERTO (src/live/execution.py)
ensure_disaster_sl cancella i bracket incoerenti PRIMA di ripiazzarne uno: fra le due
chiamate la posizione e' senza stop. Se il ripiazzamento sollevava, l'eccezione risaliva
a main() e il guasto peggiore aveva la faccia di un errore qualunque. Ora: due tentativi,
poi stato `naked`, distinto da `place-failed` (P5). Non e' "non sono riuscito a
proteggere": e' "ho tolto la protezione e non sono riuscito a rimetterla" -> allarme
massimo, mai declassato. La SEQUENZA non e' stata invertita: piazza-poi-cancella sembra
piu' sicuro ma "sembra" non basta con soldi veri senza misurarlo.
ISOLAMENTO PER ASSET (scripts/live/book_execute.py)
Il 21/07 un 502 dentro ensure_disaster_sl su BTC ha ucciso il giro intero: nel log ETH
non compare — ne' ribilanciato ne' verificato nella protezione. Ora un asset che esplode
non ferma il ciclo, e il giro degradato esce con codice 2.
CRON :07 -> :47
Il vincolo vero non era ":07" ma "fuori dai ~26s del minuto tondo" (rate-limit per-IP
auto-saturato dal collettore catena, misura del 30/07). Il :47 lo soddisfa e in piu' sta
fuori dallo slot di release. PREVISIONE DICHIARATA (M12): 3 delle 4 finestre osservate
sono rientrate entro l'ora -> il :47 ne avrebbe scavalcate 3 su 4; se martedi' prossimo
becca comunque la manutenzione, la previsione e' sbagliata.
WATERMARK AVVELENATO DAI TEST (tests/conftest.py, nuovo)
Trovato addosso: lanciando la suite, data/live/equity_seen.json passava a $5.000 e il giro
successivo mandava un allarme Telegram FALSO ("USCITA DI FONDI -86,6%"). Non cosmetico:
cap_fallback = min(cap_config, watermark x frac) sarebbe passato da $334 a $2.500/asset,
~7,5x di leva su un conto da $668, sul ramo eq_fallback che allerta e NON blocca — cioe'
i test potevano armare il pericolo che il watermark esiste per impedire. Riparato con una
fixture AUTOUSE, non per-test: chiedere a ogni autore di ricordarsene ha gia' perso il
26/07 e il 21/08. Resta esposto lo stesso errore su trades.db e book_executions.jsonl.
BLOCCATO, NON RINVIATO
Il fallback diretto ai privati Deribit richiede chiavi API create dall'operatore: in
locale esiste solo CERBERO_TOKEN. Il gateway resta un punto singolo di guasto non
aggirabile (CLAUDE.md 5.11). La sonda dice di chi e' il guasto, non lo aggira.
Test: 20 nuovi, nessuno tocca la rete; controllo positivo fatto (5 su 7 falliscono contro
il codice vecchio). Suite 730 passati, 1 fallito — quello gia' noto di 5.9 (deriva dati).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B9UyJLHzR7EJzxR3iQ3RN1
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
"""Test della diagnosi di guasto sul percorso Deribit (`src/live/venue_probe.py`) e delle due
|
||||
riparazioni che ne dipendono: la finestra scoperta del disaster-SL e l'isolamento per asset.
|
||||
|
||||
NESSUN TEST TOCCA LA RETE: la sonda pubblica e' iniettabile: `diagnose(..., sonda=...)`.
|
||||
|
||||
Cosa difendono, in ordine di gravita':
|
||||
1. **`naked`**: il disaster-SL cancellato e non ripiazzato deve avere uno stato PROPRIO, diverso
|
||||
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.
|
||||
"""
|
||||
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)
|
||||
|
||||
MANUTENZIONE_GREZZA = ('HTTP 503 {"jsonrpc":"2.0","error":'
|
||||
'{"message":"system_maintenance","code":11051}}')
|
||||
|
||||
|
||||
# --- sonde finte -----------------------------------------------------------------------------
|
||||
def _sonda_ok():
|
||||
return True, 'HTTP 200 {"jsonrpc":"2.0","result":{"version":"1.2.26"}}'
|
||||
|
||||
|
||||
def _sonda_manutenzione():
|
||||
return False, MANUTENZIONE_GREZZA
|
||||
|
||||
|
||||
def _sonda_rifiuta():
|
||||
return False, "HTTP 500 <html>bad gateway</html>"
|
||||
|
||||
|
||||
def _sonda_morta():
|
||||
return None, "ConnectionError: dns"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. La diagnosi distingue i tre guasti che vogliono azioni diverse.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_venue_sano_significa_che_il_guasto_e_nostro():
|
||||
"""Il caso che vale di piu': 4 dei 5 traceback in 63 giorni erano il NOSTRO gateway.
|
||||
Se l'API pubblica risponde, il venue sta bene e il pezzo rotto e' quello riparabile."""
|
||||
d = diagnose(["BTC: fallback close (HTTPError)"], sonda=_sonda_ok)
|
||||
assert d.verdetto == V_GATEWAY
|
||||
assert d.riparabile_da_noi is True
|
||||
assert d.atteso is False # un gateway rotto non e' MAI atteso
|
||||
assert d.gravita == "🛑"
|
||||
assert "gateway" in d.perche.lower()
|
||||
|
||||
|
||||
def test_manutenzione_riconosciuta_dalla_sonda():
|
||||
d = diagnose(["BTC: fallback close (HTTPError)"], sonda=_sonda_manutenzione)
|
||||
assert d.verdetto == V_MANUTENZIONE
|
||||
assert d.riparabile_da_noi is False # aspettare, non riparare
|
||||
|
||||
|
||||
def test_manutenzione_riconosciuta_dagli_errori_gia_raccolti_senza_sondare():
|
||||
"""Il 18/08 il codice 11051 era GIA' negli errori dello strato book, nello stesso minuto, e
|
||||
non arrivava a chi decideva la gravita'. Ora lo si guarda per primo: la sonda non parte
|
||||
nemmeno (qui lo verifichiamo con una sonda che esploderebbe)."""
|
||||
def _sonda_vietata():
|
||||
raise AssertionError("la sonda non doveva partire: l'11051 era gia' negli errori")
|
||||
|
||||
d = diagnose(['OnMaintenance: deribit {"error":{"code":11051}}'], sonda=_sonda_vietata)
|
||||
assert d.verdetto == V_MANUTENZIONE
|
||||
|
||||
|
||||
def test_venue_giu_non_e_manutenzione():
|
||||
d = diagnose([], sonda=_sonda_rifiuta)
|
||||
assert d.verdetto == V_VENUE_GIU
|
||||
assert d.atteso is False # non dichiara manutenzione -> resta rumoroso
|
||||
|
||||
|
||||
def test_sonda_morta_e_ignoto_non_va_tutto_bene():
|
||||
"""P5: «non vedo» non e' «va tutto bene». Il verdetto deve restare IGNOTO, mai GATEWAY."""
|
||||
d = diagnose([], sonda=_sonda_morta)
|
||||
assert d.verdetto == V_IGNOTO
|
||||
assert d.riparabile_da_noi is False
|
||||
assert d.atteso is False
|
||||
|
||||
|
||||
def test_la_prova_viaggia_sempre_con_il_verdetto():
|
||||
"""Un verdetto senza prova si legge come opinione. La riga deve portare il perche' (P4)."""
|
||||
d = diagnose(["BTC: fallback close (HTTPError)"], sonda=_sonda_ok)
|
||||
assert d.prova # non vuota
|
||||
assert "—" in d.riga() and d.verdetto in d.riga()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. P9 — `atteso` declassa, ma solo su EVIDENZA e solo DENTRO lo slot.
|
||||
# ---------------------------------------------------------------------------
|
||||
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)
|
||||
assert d.verdetto == V_MANUTENZIONE and d.atteso is False
|
||||
assert d.gravita != "ℹ️"
|
||||
|
||||
|
||||
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)
|
||||
assert d.verdetto == V_GATEWAY and d.atteso is False and d.gravita == "🛑"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Gli errori si DERIVANO dal report, non si reinventano (P1).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_errori_dal_report_raccoglie_le_misure_gia_fatte():
|
||||
r = {"assets": [{"asset": "BTC", "mark_src": "fallback close (HTTPError)"},
|
||||
{"asset": "ETH", "mark_src": "mainnet"}],
|
||||
"skh_feed_errors": {"BTC": "OnMaintenance: deribit 11051"},
|
||||
"pos_error": "posizione non leggibile: api 500"}
|
||||
errori = errori_dal_report(r)
|
||||
assert any("fallback close" in e for e in errori)
|
||||
assert any("11051" in e for e in errori)
|
||||
assert any("api 500" in e for e in errori)
|
||||
# e la catena completa deve concludere MANUTENZIONE senza nemmeno sondare
|
||||
assert diagnose(errori, sonda=_sonda_ok).verdetto == V_MANUTENZIONE
|
||||
|
||||
|
||||
def test_report_vuoto_non_esplode():
|
||||
assert errori_dal_report({}) == []
|
||||
Reference in New Issue
Block a user