Files
Adriano Dal Pastro 426735448e 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
2026-08-25 10:27:36 +00:00

151 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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({}) == []