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:
@@ -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 effettivo−tier 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
|
||||
|
||||
+69
-13
@@ -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:
|
||||
continue
|
||||
tot_fee_usd += fee * price
|
||||
tot_notional += notional
|
||||
except (TypeError, ValueError):
|
||||
bps = fee_bps_di_un_fill(ins, t.get("amount"), t.get("price"), t.get("fee"))
|
||||
if bps is None:
|
||||
continue
|
||||
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
|
||||
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)")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user