venue_watch: disciplina degli allarmi, specifiche contratto controllate, terza referenza
Il 18/08 sono usciti quattro 🚨 identici con 'PRIMO PASSO: prelievo di prova' per una manutenzione Deribit annunciata, e tutti e quattro DOPO che il book aveva gia' ripreso a eseguire. Il book si era comportato bene (due giri di astensione, 'non eseguo a cieco'); il difetto era negli allarmi. 1. Il blocco piattaforma era un if secco senza memoria, accanto a un rilevatore tarato con cura che allerta una volta per streak. Ora passa da lock_step(), pura e testata: manutenzione entro tolleranza = un solo avviso morbido, che sfora = un solo 🚨 ('ha SFORATO'), blocco inspiegato = 🚨 subito, rientro annunciato una volta. public/status illeggibile NON e' un rientro. 2. Il messaggio diceva 'locked=true' cablato mentre il parser accetta anche 'partial': dichiarava un valore che non aveva letto, e il runbook manda a controllare proprio quel campo. Ora stampa e salva il valore grezzo. 3. Deribit ha cambiato tick e size dei perpetual lineari USDC il 18/08 e la tabella _CONTRACT, cablata a mano, non se n'era accorta. Nessun ordine rifiutato solo perche' i cambi erano riduzioni: e' andata bene per la direzione, non perche' ce ne fossimo accorti. Tabella aggiornata ai valori verificati e aggiunto check_specs(), che gira nel venue_watch orario (fuori dal percorso ordini) e dichiara la direzione: 'granularita'' = conforme, 'rifiuto' = il venue rifiuta. La tabella dichiarata resta l'autorita' per costruire un ordine, il venue e' il controllore. 4. Aggiunta Kraken come terza referenza: Coinbase ha comprato Deribit, e una referenza che e' la casa madre non misura piu' se Deribit scolla dal mondo. THRESHOLD_BPS e PERSIST_HOURS invariati, ma il consenso ora e' su tre serie e quel numero non e' ri-misurato: dichiarato nel codice e nel diario. La direzione dell'errore e' 'allerta di meno', non 'grida al lupo'. Test 23 -> 35 su venue_watch, suite intera 618 verdi. Book, pesi e config invariati; dry-run del book verificato. Diario: docs/diary/2026-08-19-*. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -243,3 +243,114 @@ def test_il_watch_e_cablato_nel_cron_orario():
|
||||
assert sh.index("venue_watch.py") < sh.index("book_execute.py"), (
|
||||
"il watch deve girare PRIMA dell'esecuzione: se Deribit e' in stress l'allarme deve "
|
||||
"partire anche quando book_execute fallisce per la stessa ragione")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Disciplina del blocco piattaforma (2026-08-19). Il 18/08 sono usciti QUATTRO
|
||||
# 🚨 identici con "PRIMO PASSO: prelievo di prova" per una manutenzione
|
||||
# annunciata, e tutti e quattro DOPO che il book aveva gia' ripreso a eseguire.
|
||||
# ===========================================================================
|
||||
from src.live.venue_watch import LockState, is_maintenance, lock_step # noqa: E402
|
||||
|
||||
|
||||
def _replay(sequenza, grace=2):
|
||||
st, out = LockState(), []
|
||||
for locked, maint in sequenza:
|
||||
st, lvl = lock_step(st, locked, maint, grace=grace)
|
||||
out.append(lvl)
|
||||
return out
|
||||
|
||||
|
||||
def test_manutenzione_breve_e_un_avviso_morbido_non_il_runbook_del_prelievo():
|
||||
assert _replay([(True, True), (False, False)]) == ["MAINT", "RIENTRATO"]
|
||||
|
||||
|
||||
def test_non_si_ripete_ogni_ora_mentre_la_condizione_dura():
|
||||
"""Il difetto vero del 18/08: nessuna memoria, quindi un messaggio identico ogni giro."""
|
||||
livelli = _replay([(True, True)] * 5 + [(False, False)])
|
||||
assert livelli.count("MAINT") == 1
|
||||
assert livelli.count("ALERT") == 1
|
||||
assert livelli == ["MAINT", "MUTO", "ALERT", "MUTO", "MUTO", "RIENTRATO"]
|
||||
|
||||
|
||||
def test_la_manutenzione_che_sfora_RIALZA_invece_di_restare_morbida():
|
||||
"""Deribit aveva annunciato 15-30 minuti e la piattaforma e' rimasta bloccata per ore:
|
||||
una manutenzione che dura sei volte l'annuncio torna a essere una notizia."""
|
||||
assert _replay([(True, True)] * 3)[-1] == "ALERT"
|
||||
|
||||
|
||||
def test_un_blocco_senza_manutenzione_dichiarata_e_subito_il_caso_serio():
|
||||
assert _replay([(True, False)])[0] == "ALERT"
|
||||
|
||||
|
||||
def test_status_illeggibile_non_e_un_rientro():
|
||||
"""Dichiarare 'e' rientrato' perche' non si e' riusciti a guardare sarebbe la bugia peggiore."""
|
||||
st = LockState()
|
||||
st, _ = lock_step(st, True, False)
|
||||
prima = st.hours
|
||||
st, lvl = lock_step(st, None, False)
|
||||
assert lvl == "MUTO" and st.hours == prima
|
||||
|
||||
|
||||
def test_dopo_un_rientro_un_nuovo_blocco_riallerta():
|
||||
"""L'ammutolimento vale per QUESTO episodio, non per sempre."""
|
||||
livelli = _replay([(True, False), (False, False), (True, False)])
|
||||
assert livelli == ["ALERT", "RIENTRATO", "ALERT"]
|
||||
|
||||
|
||||
def test_riconosce_la_firma_della_manutenzione_deribit():
|
||||
assert is_maintenance(['OnMaintenance: deribit {"error":{"message":"system_maintenance",'
|
||||
'"code":11051}}'])
|
||||
assert not is_maintenance(["HTTPError: 502 Bad Gateway"])
|
||||
assert not is_maintenance([])
|
||||
|
||||
|
||||
def test_lo_stato_del_lock_sopravvive_al_giro_successivo(tmp_path):
|
||||
"""Senza persistenza la memoria si perde a ogni cron e si ricomincia a gridare."""
|
||||
from src.live.venue_watch import WatchState, load_state, save_state
|
||||
p = tmp_path / "s.json"
|
||||
st = WatchState()
|
||||
st.lock, _ = lock_step(st.lock, True, True)
|
||||
save_state(st, p)
|
||||
assert load_state(p).lock.hours == 1 and load_state(p).lock.alerted_soft
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Specifiche contratto vs venue (2026-08-19)
|
||||
# ===========================================================================
|
||||
from src.live.deribit import compare_specs # noqa: E402
|
||||
|
||||
_VERO = {"ETH_USDC-PERPETUAL": {"tick_size": 0.01, "min_trade_amount": 0.0001,
|
||||
"contract_size": 0.0001}}
|
||||
|
||||
|
||||
def test_dichiarato_piu_grosso_e_solo_granularita():
|
||||
"""La situazione del 18/08: conforme, si perde precisione. Non e' un'emergenza."""
|
||||
d = compare_specs({"ETH_USDC-PERPETUAL": {"tick": 0.05, "min": 0.001, "step": 0.001}}, _VERO)
|
||||
assert d and all(x["rischio"] == "granularita'" for x in d)
|
||||
|
||||
|
||||
def test_dichiarato_piu_fine_significa_ordini_RIFIUTATI():
|
||||
"""L'altra direzione, quella che costa: il venue alza un minimo e noi non lo sappiamo."""
|
||||
d = compare_specs({"ETH_USDC-PERPETUAL": {"tick": 0.001, "min": 1e-5, "step": 1e-5}}, _VERO)
|
||||
assert d and all(x["rischio"] == "rifiuto" for x in d)
|
||||
|
||||
|
||||
def test_uno_strumento_non_letto_non_e_una_divergenza():
|
||||
"""Silenzio e uguaglianza non sono la stessa cosa: cio' che non si e' letto va dichiarato
|
||||
a parte (`non_letti`), non fatto passare per 'combacia'."""
|
||||
assert compare_specs({"X": {"tick": 1.0}}, {}) == []
|
||||
|
||||
|
||||
def test_la_tabella_dichiarata_combacia_col_venue_oggi():
|
||||
"""Verificato a mano il 19/08. Se questo rompe, o Deribit ha cambiato le specifiche o
|
||||
qualcuno ha toccato _CONTRACT: in entrambi i casi va guardato, non silenziato."""
|
||||
from src.live.deribit import _CONTRACT
|
||||
assert compare_specs(_CONTRACT, {
|
||||
"BTC_USDC-PERPETUAL": {"tick_size": 0.1, "min_trade_amount": 0.0001,
|
||||
"contract_size": 0.0001},
|
||||
"ETH_USDC-PERPETUAL": {"tick_size": 0.01, "min_trade_amount": 0.0001,
|
||||
"contract_size": 0.0001},
|
||||
"BTC-PERPETUAL": {"tick_size": 0.5, "min_trade_amount": 10.0, "contract_size": 10.0},
|
||||
"ETH-PERPETUAL": {"tick_size": 0.05, "min_trade_amount": 1.0, "contract_size": 1.0},
|
||||
}) == []
|
||||
|
||||
Reference in New Issue
Block a user