8cfe15cbd5
Trovato in un check generale. INSTRUMENTS era la tupla cablata
("BTC-PERPETUAL","ETH-PERPETUAL") — gli inverse, regolati in BTC/ETH — mentre
src.live.book.INSTRUMENT punta a BTC_USDC-PERPETUAL / ETH_USDC-PERPETUAL.
Due conseguenze, e la seconda era gia' visibile ogni giorno nel log:
1. Il tier sorvegliato era di un prodotto che il book non tratta. Oggi coincidono
(3.50/1.50 su entrambe le linee) ma la prova che si muovono in modo indipendente
e' nel progetto: il cambio del 18/08 tocco' tick e size dei SOLI lineari USDC
(inverse ancora tick 0.5 / min 10.0, lineari 0.1 / 0.0001). Un aumento sulla sola
linea lineare sarebbe stato invisibile, e la regola decisa in anticipo
(<=5bps nulla / >10bps rivedere il peso SKH01) applicata al numero sbagliato.
2. Il cross-check sui trade REALI — la fonte autorevole, cioe' quanto abbiamo
davvero pagato — non poteva misurare nulla per costruzione: chiedeva la storia di
uno strumento con zero fill. Stampava "NON MISURATO (nessun trade recente
leggibile)" anche in un giorno con 4 esecuzioni. Verificato sul conto: inverse
0 trade, _USDC-PERPETUAL 3 (BTC) e 1 (ETH).
FIX. INSTRUMENTS si DERIVA da src.live.book.INSTRUMENT: la divergenza non e' piu' un
rischio da ricordare, e' impossibile.
E non era un rename di due stringhe: le due famiglie hanno unita' DIVERSE. Inverse
amount = nozionale USD e fee in valuta base; lineare amount = quantita' base e fee
gia' in USDC. Ripuntare senza correggerle avrebbe dato, su un fill vero (0.001 BTC
@ 74.305,80, fee 0,02600703 USDC), ~2,6e8 bps invece di 3,50 — senza sollevare
nulla. Aggiunte convenzione() (lineare / inverse / IGNOTA: una famiglia non nota non
si indovina, si dichiara) e fee_bps_di_un_fill(), entrambe pure. Il cross-check ora
gira e da' 3,50 bps effettivi = il tier esatto; il report stampa lo scarto
effettivo-tier con ⚠️ oltre 1 bps.
Test 13 -> 17, verificati per MUTAZIONE: rimettendo la tupla cablata fallisce
test_sorveglia_esattamente_gli_strumenti_DEL_BOOK; scambiando le convenzioni
fallisce test_le_due_famiglie_hanno_unita_DIVERSE_e_scambiarle_non_fa_rumore, che
contiene il controllo positivo (la convenzione sbagliata NON solleva niente, mente).
3a occorrenza in un giorno della stessa forma di difetto, dopo i test di book_live
(potenza zero a libro flat) e la taratura di venue_watch (misurata con bitfinex
mentre il live girava senza): un controllo puntato su una configurazione diversa da
quella che gira passa sempre, e non sta controllando niente. REGOLA: un sorvegliante
DERIVA il proprio bersaglio dal codice sorvegliato, mai lo ridichiara.
Book, pesi, config, cron, soglie fee: INVARIATI. Suite 625 verdi, 1 rosso noto
(test_gtaa_band_gate). NB: la prima corsa dopo il fix ha inviato una notifica
Telegram legittima ("strumento nuovo nella sorveglianza"); lo stato e' poi assestato.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
165 lines
7.8 KiB
Python
165 lines
7.8 KiB
Python
"""Test della sorveglianza fee (scripts/live/fee_watch.py).
|
|
|
|
Il nuovo schema fee Deribit entra in vigore il 2026-08-01 SENZA numeri pubblicati da fonte
|
|
primaria. La regola e' stata decisa PRIMA di vedere il numero (r0726_fee_sensitivity.py):
|
|
taker <=5bps -> nulla, >10bps -> rivedere il peso di SKH01. Questi test congelano la regola e
|
|
la sua taratura, cosi' che il numero non possa muovere la soglia una volta arrivato.
|
|
|
|
Il test piu' importante e' `test_baseline_e_quella_dei_backtest`: lega la soglia della
|
|
sorveglianza alla fee usata nei backtest. Se qualcuno cambia una delle due senza l'altra, il
|
|
confronto smette di avere senso e il test lo dice.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
sys.path.insert(0, str(ROOT / "scripts" / "live"))
|
|
|
|
import fee_watch as FW # noqa: E402
|
|
|
|
|
|
# ===========================================================================
|
|
# la regola congelata
|
|
# ===========================================================================
|
|
def test_al_tier_di_oggi_non_si_tocca_nulla():
|
|
assert FW.verdict(5.0)[0] == "OK"
|
|
|
|
|
|
def test_sotto_il_tier_di_oggi_i_backtest_restano_conservativi():
|
|
"""Se la fee SCENDE i numeri pubblicati non vanno rifatti: sono un limite inferiore."""
|
|
assert FW.verdict(2.0)[0] == "OK"
|
|
|
|
|
|
def test_fra_5_e_10_bps_si_riporta_il_costo_ma_non_si_agisce():
|
|
assert FW.verdict(5.1)[0] == "NOTA"
|
|
assert FW.verdict(10.0)[0] == "NOTA"
|
|
|
|
|
|
def test_sopra_10_bps_scatta_la_revisione_del_peso_di_skh01():
|
|
lvl, why = FW.verdict(10.1)
|
|
assert lvl == "AZIONE"
|
|
assert "SKH01" in why
|
|
|
|
|
|
def test_le_soglie_sono_quelle_dichiarate_in_claude_md():
|
|
"""Una regola decisa in anticipo vale solo se non la si puo' spostare dopo aver visto il
|
|
numero: qui le due soglie sono congelate."""
|
|
assert (FW.SOGLIA_OK, FW.SOGLIA_AZIONE) == (5.0, 10.0)
|
|
|
|
|
|
def test_baseline_e_quella_dei_backtest():
|
|
"""5 bps/lato = 0.10% RT = il default di backtest_signals. Le due devono restare agganciate."""
|
|
from src.backtest.harness import backtest_signals
|
|
import inspect
|
|
default_fee_rt = inspect.signature(backtest_signals).parameters["fee_rt"].default
|
|
assert FW.BASELINE_TAKER_BPS * 2 / 1e4 == default_fee_rt
|
|
|
|
|
|
def test_sensibilita_marginale_e_quella_misurata():
|
|
"""-0.017 Sharpe/bps di book (r0726_fee_sensitivity). Se cambia la misura, cambia qui."""
|
|
assert FW.D_SHARPE_PER_BPS == -0.017
|
|
|
|
|
|
# ===========================================================================
|
|
# rilevazione del cambiamento — il motivo per cui e' un cron e non un promemoria
|
|
# ===========================================================================
|
|
def test_prima_lettura_non_e_un_evento():
|
|
"""Senza stato precedente non c'e' un cambiamento: c'e' un'inizializzazione. Allertare
|
|
qui vorrebbe dire allertare a ogni installazione."""
|
|
assert FW.diff_vs({}, {"BTC-PERPETUAL": dict(taker_bps=5.0, maker_bps=0.0, liq_bps=75.0)}) == []
|
|
|
|
|
|
def test_nessun_cambiamento_nessun_rumore():
|
|
cur = {"BTC-PERPETUAL": dict(taker_bps=5.0, maker_bps=0.0, liq_bps=75.0)}
|
|
assert FW.diff_vs(dict(cur), cur) == []
|
|
|
|
|
|
def test_un_taker_che_sale_viene_visto():
|
|
prev = {"BTC-PERPETUAL": dict(taker_bps=5.0, maker_bps=0.0, liq_bps=75.0)}
|
|
cur = {"BTC-PERPETUAL": dict(taker_bps=8.0, maker_bps=0.0, liq_bps=75.0)}
|
|
ch = FW.diff_vs(prev, cur)
|
|
assert len(ch) == 1 and "taker" in ch[0] and "8.00" in ch[0]
|
|
|
|
|
|
def test_anche_maker_e_liquidazione_sono_sorvegliati():
|
|
"""L'annuncio tocca tre cose: taker piu' bassi, rebate maker piu' bassi, liquidation fee 1%.
|
|
Sorvegliarne una sola vorrebbe dire non accorgersi delle altre due."""
|
|
prev = {"ETH-PERPETUAL": dict(taker_bps=5.0, maker_bps=0.0, liq_bps=90.0)}
|
|
cur = {"ETH-PERPETUAL": dict(taker_bps=5.0, maker_bps=1.0, liq_bps=100.0)}
|
|
ch = FW.diff_vs(prev, cur)
|
|
assert len(ch) == 2
|
|
assert any("maker" in c for c in ch) and any("liquidazione" in c for c in ch)
|
|
|
|
|
|
# ===========================================================================
|
|
# assunzione dichiarata
|
|
# ===========================================================================
|
|
def test_sorveglia_il_tier_base_e_lo_dichiara():
|
|
"""A $600 il volume 30g e' trascurabile: il conto sta al tier BASE, che e' cio' che
|
|
l'endpoint pubblico riporta. Se un giorno il conto salisse di tier, questo script
|
|
misurerebbe la cosa sbagliata — l'assunzione sta nel docstring, e questo test la lega."""
|
|
assert "public/get_instrument" in FW.API or "get_instrument" in FW.API
|
|
assert "tier BASE" in FW.__doc__ or "tier base" in FW.__doc__
|
|
|
|
|
|
def test_fee_reali_assenti_non_sono_fee_zero():
|
|
"""{} = 'non misurata'. Un dict vuoto letto come 'zero' direbbe che il book trada gratis."""
|
|
assert FW.realized_fee_bps.__doc__ and "non misurata" in FW.realized_fee_bps.__doc__
|
|
|
|
|
|
# ===========================================================================
|
|
# STRUMENTI E UNITA' — il difetto trovato il 2026-08-21.
|
|
# `INSTRUMENTS` era la tupla cablata ("BTC-PERPETUAL", "ETH-PERPETUAL"), cioe' gli INVERSE,
|
|
# mentre il book esegue sui LINEARI USDC. Il tier sorvegliato era di un prodotto mai tradato
|
|
# e il cross-check sui trade reali non poteva misurare nulla per costruzione.
|
|
# ===========================================================================
|
|
def test_sorveglia_esattamente_gli_strumenti_DEL_BOOK():
|
|
"""La divergenza non dev'essere un rischio da ricordare: dev'essere impossibile.
|
|
|
|
Il 18/08 Deribit ha cambiato tick e size dei SOLI perpetual lineari USDC lasciando intatti
|
|
gli inverse: le due linee di prodotto si muovono in modo indipendente, quindi sorvegliare
|
|
l'una mentre si trada l'altra e' un controllo che non controlla.
|
|
"""
|
|
from src.live.book import INSTRUMENT as BOOK
|
|
assert set(FW.INSTRUMENTS) == set(BOOK.values()), \
|
|
"gli strumenti sorvegliati devono essere quelli su cui il book esegue"
|
|
assert all(i.endswith("_USDC-PERPETUAL") for i in FW.INSTRUMENTS)
|
|
|
|
|
|
def test_la_convenzione_di_unita_e_dichiarata_e_non_indovinata():
|
|
assert FW.convenzione("BTC_USDC-PERPETUAL") == "lineare"
|
|
assert FW.convenzione("BTC-PERPETUAL") == "inverse"
|
|
# una famiglia che non conosciamo NON si indovina: un fill misurato con la convenzione
|
|
# sbagliata non da' un errore, da' un numero.
|
|
assert FW.convenzione("BTC-28MAR27-100000-C") == "ignota"
|
|
assert FW.fee_bps_di_un_fill("BTC-28MAR27-100000-C", 1.0, 100.0, 0.01) is None
|
|
|
|
|
|
def test_le_due_famiglie_hanno_unita_DIVERSE_e_scambiarle_non_fa_rumore():
|
|
"""⚠️ Il punto che rende il difetto pericoloso: applicare la convenzione sbagliata non
|
|
solleva niente. Su un fill VERO del 21/08 (0.001 BTC @ 74.305,80, fee 0,02600703 USDC) la
|
|
formula giusta da' il tier taker esatto; quella inverse da' un numero enorme e muto.
|
|
"""
|
|
amount, price, fee = 0.001, 74305.8, 0.02600703
|
|
giusto = FW.fee_bps_di_un_fill("BTC_USDC-PERPETUAL", amount, price, fee)
|
|
assert giusto is not None and abs(giusto - 3.50) < 0.01, f"atteso ~3.50 bps, ottenuto {giusto}"
|
|
|
|
# la stessa riga letta con la convenzione inverse: nessuna eccezione, solo un numero falso
|
|
sbagliato = FW.fee_bps_di_un_fill("BTC-PERPETUAL", amount, price, fee)
|
|
assert sbagliato is not None and sbagliato > 1e6, \
|
|
"il controllo positivo deve mostrare che la convenzione sbagliata NON fallisce, mente"
|
|
|
|
# e su un fill inverse la convenzione inverse e' quella giusta (amount in USD, fee in BTC)
|
|
inv = FW.fee_bps_di_un_fill("BTC-PERPETUAL", 74.3058, 74305.8, 74.3058 * 3.5e-4 / 74305.8)
|
|
assert inv is not None and abs(inv - 3.50) < 0.01
|
|
|
|
|
|
def test_un_fill_non_misurabile_e_None_non_zero():
|
|
for bad in ((0.0, 100.0, 1.0), (1.0, 0.0, 1.0), (None, 100.0, 1.0), ("x", 100.0, 1.0)):
|
|
assert FW.fee_bps_di_un_fill("BTC_USDC-PERPETUAL", *bad) is None
|
|
# fee nulla e' invece una misura legittima (ordine maker a rebate zero)
|
|
assert FW.fee_bps_di_un_fill("BTC_USDC-PERPETUAL", 1.0, 100.0, 0.0) == 0.0
|