fee_watch: sorvegliava i perpetual INVERSE mentre il book trada i LINEARI USDC

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>
This commit is contained in:
Adriano Dal Pastro
2026-08-21 19:19:22 +00:00
parent fac9978d87
commit 8cfe15cbd5
3 changed files with 147 additions and 13 deletions
+24
View File
@@ -1654,6 +1654,30 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
bps**, applica la regola congelata (≤5bps nulla · >10bps rivedere il peso SKH01) e allerta su
**qualsiasi** cambiamento dei tre. `test_baseline_e_quella_dei_backtest` lega la soglia al
default `fee_rt=0.001` di `backtest_signals`: se divergono, il test lo dice.
⚠️ **CORREZIONE 2026-08-21 — sorvegliava lo STRUMENTO SBAGLIATO (test 13 → 17).** `INSTRUMENTS`
era la tupla cablata `("BTC-PERPETUAL","ETH-PERPETUAL")` = i perpetual **INVERSE** (regolati in
BTC/ETH), mentre il book esegue sui **LINEARI USDC** (`BTC_USDC-PERPETUAL`). Due conseguenze:
(a) il tier sorvegliato era di un prodotto **mai tradato** — oggi identico per caso (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' i SOLI lineari (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 ≤5/>10 bps applicata al
numero sbagliato; (b) **il cross-check sui trade reali non poteva misurare nulla per
costruzione** — chiedeva `trade_history` di uno strumento con **0 fill** e stampava «NON
MISURATO» anche nei giorni con 4 esecuzioni (verificato: inverse 0 trade, `_USDC` 3+1). Ora
`INSTRUMENTS` si **DERIVA da `src.live.book.INSTRUMENT`**: la divergenza non e' un rischio da
ricordare, e' impossibile. ⚠️ **E non era un rename:** 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), **~2,6e8 bps invece di 3,50** — e **senza sollevare nulla**. Cablate
`convenzione()` (lineare/inverse/**ignota**: una famiglia non nota non si indovina) e
`fee_bps_di_un_fill()` pure; il cross-check ora gira e da' **3,50 bps effettivi = il tier
esatto**, e il report stampa lo scarto effettivotier con ⚠️ sopra 1 bps.
**REGOLA: un sorvegliante deve DERIVARE il proprio bersaglio dal codice sorvegliato, mai
ridichiararlo** — due liste in due file divergono in silenzio, e il giorno che divergono il
sorvegliante continua a dire OK. **3ª occorrenza in un giorno della stessa forma** (test di
`book_live` senza potenza a libro flat, 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.*
(2) **`src/live/monitor_health.py` + `scripts/live/monitor_health.py`** (test 16): **tre gate
pre-registrati** (STATARB 27/09, XSR01 23/10, DVOLSPREAD 24/10) si decidono su serie forward di
cui **una sola** aveva una guardia d'integrita'. Un monitor fermo produce silenzio, e **il
+68 -12
View File
@@ -48,12 +48,28 @@ import requests
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live.book import INSTRUMENT as BOOK_INSTRUMENT # noqa: E402
from src.live.notifier import notify # noqa: E402
STATE = ROOT / "data" / "fee_watch" / "state.json"
INSTRUMENTS = ("BTC-PERPETUAL", "ETH-PERPETUAL")
# ⚠️ CORREZIONE 2026-08-21. Qui c'era la tupla CABLATA ("BTC-PERPETUAL", "ETH-PERPETUAL") — gli
# INVERSE, regolati in BTC/ETH — mentre il book esegue sui LINEARI USDC (BTC_USDC-PERPETUAL).
# Sono due linee di prodotto distinte e Deribit le cambia in modo indipendente: il 18/08 ha
# toccato tick e size dei SOLI lineari (inverse ancora tick 0.5 / min 10.0, lineari 0.1 / 0.0001).
# Conseguenze del difetto: (a) il tier sorvegliato era di un prodotto che il book non tratta —
# oggi identico per caso, domani no; (b) il cross-check sui trade reali chiedeva la storia di uno
# strumento mai tradato e stampava "NON MISURATO" anche nei giorni con 4 fill.
# La lista si DERIVA dal book: cosi' la divergenza non e' un rischio da ricordare, e' impossibile.
INSTRUMENTS = tuple(BOOK_INSTRUMENT[a] for a in sorted(BOOK_INSTRUMENT))
API = "https://www.deribit.com/api/v2/public/get_instrument"
# Convenzione di unita' dei trade, che NON e' la stessa nelle due famiglie:
# inverse (BTC-PERPETUAL) amount = nozionale in USD, fee in valuta base (BTC)
# lineare (BTC_USDC-PERPETUAL) amount = quantita' in BASE, fee gia' in USDC
# Applicare la convenzione sbagliata non da' un errore: da' un numero. Su un fill vero
# (0.001 BTC @ 74.305,8, fee 0,026 USDC) quella inverse darebbe ~2.6e8 bps invece di 3,50.
SUFFISSO_LINEARE = "_USDC-PERPETUAL"
# --- riferimenti CONGELATI (cambiarli invalida la curva di r0726_fee_sensitivity.py) ---
BASELINE_TAKER_BPS = 5.0 # 0.10% RT: l'assunzione di OGNI backtest del progetto
BASELINE_MAKER_BPS = 0.0
@@ -88,9 +104,47 @@ def verdict(taker_bps: float) -> tuple[str, str]:
f"(4x piu' fee-sensibile di TP01) via weights_tilt_null")
def convenzione(instrument: str) -> str:
"""Famiglia di unita' dell'istrumento: 'lineare' | 'inverse' | 'ignota'. PURA.
'ignota' NON e' un caso da indovinare: un fill misurato con la convenzione sbagliata produce
un numero plausibile-o-assurdo ma sempre SILENZIOSO. Meglio dichiarare di non sapere.
"""
if instrument.endswith(SUFFISSO_LINEARE):
return "lineare"
if instrument.endswith("-PERPETUAL"):
return "inverse"
return "ignota"
def fee_bps_di_un_fill(instrument: str, amount: float, price: float, fee: float) -> float | None:
"""bps di nozionale pagati su UN fill, con la convenzione della sua famiglia. PURA.
Ritorna None se non e' calcolabile (dati mancanti o famiglia ignota): None significa
'non misurata', mai 'zero'.
"""
fam = convenzione(instrument)
try:
amount, price, fee = abs(float(amount)), float(price), abs(float(fee))
except (TypeError, ValueError):
return None
if amount <= 0 or price <= 0:
return None
if fam == "lineare":
notional_usd, fee_usd = amount * price, fee # amount in BASE, fee gia' in USDC
elif fam == "inverse":
notional_usd, fee_usd = amount, fee * price # amount in USD, fee in valuta base
else:
return None
return fee_usd / notional_usd * 1e4 if notional_usd > 0 else None
def realized_fee_bps(limit: int = 20) -> dict:
"""Fee REALMENTE pagata sui trade del conto, in bps di nozionale. Fonte autorevole, ma
disponibile solo se il book ha tradato di recente: {} non e' 'zero', e' 'non misurata'."""
"""Fee REALMENTE pagata sui trade del conto, in bps di nozionale (media pesata sul nozionale).
Fonte autorevole, ma disponibile solo se il book ha tradato di recente: {} non e' 'zero',
e' 'non misurata'.
"""
try:
from src.live.deribit import DeribitRead
d = DeribitRead()
@@ -104,16 +158,13 @@ def realized_fee_bps(limit: int = 20) -> dict:
except Exception:
continue
for t in trades:
try:
notional = abs(float(t.get("amount") or 0.0)) # perp: nozionale in USD
price = float(t.get("price") or 0.0)
fee = abs(float(t.get("fee") or 0.0)) # in valuta di settlement
if notional <= 0 or price <= 0:
bps = fee_bps_di_un_fill(ins, t.get("amount"), t.get("price"), t.get("fee"))
if bps is None:
continue
tot_fee_usd += fee * price
notional = abs(float(t["amount"])) * float(t["price"]) \
if convenzione(ins) == "lineare" else abs(float(t["amount"]))
tot_fee_usd += bps / 1e4 * notional
tot_notional += notional
except (TypeError, ValueError):
continue
if tot_notional > 0:
out[ins] = tot_fee_usd / tot_notional * 1e4
return out
@@ -183,7 +234,12 @@ def main() -> int:
if real:
print("\n cross-check sui trade REALI del conto (fonte autorevole):")
for ins, bps in real.items():
print(f" {ins:<16}{bps:>9.2f} bps/lato effettivi")
tier = r["current"].get(ins, {}).get("taker_bps")
nota = ""
if tier is not None:
d = bps - tier
nota = f" (tier taker {tier:.2f}{d:+.2f})" + (" ⚠️ DIVERGE" if abs(d) > 1.0 else "")
print(f" {ins:<22}{bps:>9.2f} bps/lato effettivi{nota}")
else:
print("\n cross-check sui trade reali: NON MISURATO (nessun trade recente leggibile)")
+54
View File
@@ -108,3 +108,57 @@ def test_sorveglia_il_tier_base_e_lo_dichiara():
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