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:
+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)")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user