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:
+78
-2
@@ -11,6 +11,7 @@ in USD notional, step verificato su Deribit: BTC $10, ETH $1).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
@@ -24,12 +25,87 @@ TIMEOUT = 15
|
||||
# Inverse perp: amount = USD notional, step in USD, settle = base-coin (BTC/ETH).
|
||||
# Linear USDC perp: amount = base-coin (BTC/ETH), step in base-coin, settle = USDC (margine USDC).
|
||||
# NB il conto reale e' USDC -> gli strumenti ESEGUIBILI sono i LINEARI _USDC-PERPETUAL.
|
||||
# ⚠️ TABELLA DICHIARATA, ed e' lei l'autorita' per costruire un ordine — non l'API.
|
||||
# L'esecuzione dev'essere deterministica, in git e leggibile offline: un ordine che cambia forma
|
||||
# perche' una GET ha risposto diverso e' un ordine che non si puo' ricostruire dopo. Il venue fa
|
||||
# il CONTROLLORE, non la fonte: `check_specs()` gira ogni ora e allerta quando questa tabella si
|
||||
# scosta dal vero, e allora la si aggiorna qui a mano, di proposito.
|
||||
#
|
||||
# ⚠️ VERIFICATI contro public/get_instrument il 2026-08-19. Deribit ha cambiato le specifiche dei
|
||||
# perpetual lineari USDC il 18/08 alle 09:00 UTC (annuncio del 14/08) e questa tabella non se n'era
|
||||
# accorta: tick BTC 0.5->0.1, tick ETH 0.05->0.01, min/step ETH 0.001->0.0001. Nessun ordine e'
|
||||
# stato rifiutato perche' i cambi erano RIDUZIONI e un valore piu' grosso resta conforme — cioe'
|
||||
# e' andata bene per la direzione del cambiamento, non perche' ce ne fossimo accorti. Il costo
|
||||
# misurato era di granularita': su ETH l'incremento minimo restava $1.92 invece di $0.19 su un
|
||||
# conto da $597 (0.32% contro 0.03%). Il giorno che Deribit ALZA un minimo la stessa cecita' fa
|
||||
# rifiutare gli ordini: per quello adesso c'e' check_specs().
|
||||
_CONTRACT = {
|
||||
"BTC-PERPETUAL": {"min": 10.0, "step": 10.0, "tick": 0.5, "settle": "BTC"},
|
||||
"ETH-PERPETUAL": {"min": 1.0, "step": 1.0, "tick": 0.05, "settle": "ETH"},
|
||||
"BTC_USDC-PERPETUAL": {"min": 0.0001, "step": 0.0001, "tick": 0.5, "settle": "USDC", "linear": True},
|
||||
"ETH_USDC-PERPETUAL": {"min": 0.001, "step": 0.001, "tick": 0.05, "settle": "USDC", "linear": True},
|
||||
"BTC_USDC-PERPETUAL": {"min": 0.0001, "step": 0.0001, "tick": 0.1, "settle": "USDC", "linear": True},
|
||||
"ETH_USDC-PERPETUAL": {"min": 0.0001, "step": 0.0001, "tick": 0.01, "settle": "USDC", "linear": True},
|
||||
}
|
||||
|
||||
# nome nostro -> campo di public/get_instrument
|
||||
_SPEC_FIELDS = (("tick", "tick_size"), ("min", "min_trade_amount"), ("step", "contract_size"))
|
||||
|
||||
|
||||
def compare_specs(declared: dict, live: dict) -> list[dict]:
|
||||
"""PURA, niente rete. Divergenze fra la tabella dichiarata e le specifiche del venue.
|
||||
|
||||
Ogni voce porta la DIREZIONE, che e' l'informazione che serve per decidere se correre:
|
||||
* `rischio: "granularita'"` — il dichiarato e' piu' GROSSO del vero. L'ordine resta conforme
|
||||
(un multiplo del tick e' un tick valido), si perde solo precisione. Si aggiorna con calma.
|
||||
* `rischio: "rifiuto"` — il dichiarato e' piu' FINE del vero. Il venue RIFIUTA l'ordine.
|
||||
Va corretto prima del prossimo giro.
|
||||
Uno strumento assente dal `live` NON e' una divergenza: puo' essere la rete. Il silenzio non
|
||||
va confuso con l'uguaglianza, e infatti `check_specs()` dichiara a parte cosa non ha letto.
|
||||
"""
|
||||
fuori = []
|
||||
for strumento, atteso in declared.items():
|
||||
vero = live.get(strumento)
|
||||
if not vero:
|
||||
continue
|
||||
for nostro, loro in _SPEC_FIELDS:
|
||||
a, v = atteso.get(nostro), vero.get(loro)
|
||||
if a is None or v is None:
|
||||
continue
|
||||
a, v = float(a), float(v)
|
||||
if abs(a - v) <= 1e-12 * max(1.0, abs(v)):
|
||||
continue
|
||||
fuori.append({"strumento": strumento, "campo": nostro, "dichiarato": a, "venue": v,
|
||||
"rischio": "granularita'" if a > v else "rifiuto"})
|
||||
return fuori
|
||||
|
||||
|
||||
def fetch_live_specs(instruments: list[str] | None = None) -> tuple[dict, list[str]]:
|
||||
"""public/get_instrument per ogni strumento. RETE. Ritorna (specifiche, non_letti).
|
||||
|
||||
⚠️ Non solleva mai: un controllo che non parte non deve poter fermare il cron del book. Cio'
|
||||
che non ha letto lo DICHIARA invece di ometterlo, perche' "non l'ho guardato" e "e' uguale"
|
||||
sono due cose diverse e solo una delle due e' rassicurante.
|
||||
"""
|
||||
import urllib.request
|
||||
fuori, non_letti = {}, []
|
||||
for strumento in (instruments or list(_CONTRACT)):
|
||||
try:
|
||||
url = ("https://www.deribit.com/api/v2/public/get_instrument"
|
||||
f"?instrument_name={strumento}")
|
||||
with urllib.request.urlopen(url, timeout=15) as r:
|
||||
res = json.loads(r.read()).get("result") or {}
|
||||
if res.get("tick_size") is None:
|
||||
non_letti.append(strumento)
|
||||
else:
|
||||
fuori[strumento] = res
|
||||
except Exception as e: # noqa: BLE001
|
||||
non_letti.append(f"{strumento} ({type(e).__name__})")
|
||||
return fuori, non_letti
|
||||
|
||||
|
||||
def check_specs() -> dict:
|
||||
"""Confronta la tabella dichiarata col venue. Sola lettura, mai solleva."""
|
||||
live, non_letti = fetch_live_specs()
|
||||
return {"divergenze": compare_specs(_CONTRACT, live), "non_letti": non_letti}
|
||||
# Il conto reale e' USDC -> mappiamo gli asset sui perp LINEARI USDC (gli unici eseguibili qui).
|
||||
INSTRUMENT = {"BTC": "BTC_USDC-PERPETUAL", "ETH": "ETH_USDC-PERPETUAL"}
|
||||
DISASTER_LABEL = "tp01-disaster" # label dei bracket disaster-SL (per ritrovarli/gestirli/mostrarli)
|
||||
|
||||
Reference in New Issue
Block a user