fee_watch: realized_fee_bps sempre calcolata, divergenza da tier notificata una volta per episodio (#37 B9)

This commit is contained in:
2026-09-27 15:06:40 +00:00
parent 841d4c78cd
commit 95cfb396e7
2 changed files with 143 additions and 7 deletions
+46 -6
View File
@@ -79,6 +79,7 @@ SOGLIA_OK = 5.0 # <= -> nessuna azione
SOGLIA_AZIONE = 10.0 # > -> rivedere il peso di SKH01
D_SHARPE_PER_BPS = -0.017 # sensibilita' marginale del BOOK, misurata
D_CAGR_PER_BPS = -0.0023
SOGLIA_DIVERGENZA_BPS = 1.0 # #37 B9: |fee realizzata - tier pubblico| oltre questo -> notify
def fetch_fees(instrument: str) -> dict:
@@ -209,14 +210,30 @@ def run() -> dict:
changes = diff_vs(prev, cur)
worst = max(c["taker_bps"] for c in cur.values())
lvl, why = verdict(worst)
# issue #37 (B9). realized_fee_bps() girava SOLO dentro il blocco di stampa: in cron
# (--quiet, il caso normale) non girava mai e una fee realmente pagata diversa dal tier
# pubblico non avvisava nessuno. Qui si calcola SEMPRE ed entra nel record del giro
# (registra_giro, sotto). {} -> None: "non misurata" (nessun trade recente), mai "zero".
real = realized_fee_bps() or None
divergenze = []
if real:
for ins, bps in real.items():
tier = cur.get(ins, {}).get("taker_bps")
if tier is None:
continue
d = bps - tier
if abs(d) > SOGLIA_DIVERGENZA_BPS:
divergenze.append(
f"{ins}: realizzata {bps:.2f} bps vs tier pubblico {tier:.2f} ({d:+.2f})")
return dict(current=cur, changes=changes, worst_taker_bps=worst, level=lvl, reason=why,
first_read=not prev)
first_read=not prev, realized=real, divergenze=divergenze,
divergenza_gia_detta=bool(prev.get("divergenza_detta")))
def main() -> int:
r = run()
quiet = "--quiet" in sys.argv
interessante = bool(r["changes"]) or r["level"] != "OK"
interessante = bool(r["changes"]) or r["level"] != "OK" or bool(r.get("divergenze"))
if not quiet or interessante:
print("=" * 86)
@@ -232,7 +249,7 @@ def main() -> int:
print(" (prima lettura: stato inizializzato, nessun confronto possibile)")
for c in r["changes"]:
print(f" CAMBIO: {c}")
real = realized_fee_bps()
real = r.get("realized") # calcolato una volta in run(), non piu' qui (#37 B9)
if real:
print("\n cross-check sui trade REALI del conto (fonte autorevole):")
for ins, bps in real.items():
@@ -240,7 +257,8 @@ def main() -> int:
nota = ""
if tier is not None:
d = bps - tier
nota = f" (tier taker {tier:.2f} → {d:+.2f})" + (" ⚠️ DIVERGE" if abs(d) > 1.0 else "")
nota = (f" (tier taker {tier:.2f} → {d:+.2f})"
+ (" ⚠️ DIVERGE" if abs(d) > SOGLIA_DIVERGENZA_BPS 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)")
@@ -257,9 +275,31 @@ def main() -> int:
})
if not inviato and (not quiet or interessante):
print(" ⚠️ invio FALLITO — stato NON salvato: il prossimo giro ritenta")
# issue #37 (B9): fee realmente pagata che diverge dal tier pubblico atteso -> notify UNA
# volta per episodio. Marcatore "divergenza_detta" nello stesso state.json (stessa regola di
# B8: si scrive solo se l'invio e' riuscito), azzerato quando la divergenza torna sotto
# soglia. E' un evento indipendente dal cambio di tier: se qui serve un invio e quello di B8
# e' appena fallito, save_state sotto non gira (stessa gate di B8) e il marcatore non si
# scrive — al giro dopo si ridice, ridondante ma mai persa, la stessa scelta di B8.
divergenze = r.get("divergenze") or []
gia_detta = bool(r.get("divergenza_gia_detta"))
marcatore = gia_detta
if divergenze and not gia_detta:
inviata_div = notify("💸 FEE WATCH — fee realizzata diverge dal tier pubblico", {
f"divergenza {i+1}": d for i, d in enumerate(divergenze)
})
if inviata_div:
marcatore = True
elif not quiet or interessante:
print(" ⚠️ invio divergenza FALLITO — marcatore non scritto: il prossimo giro ritenta")
elif not divergenze:
marcatore = False
stato_da_salvare = dict(r["current"])
stato_da_salvare["divergenza_detta"] = marcatore
if not serve_invio or inviato:
save_state(r["current"])
registra_giro(RUNS, level=r["level"])
save_state(stato_da_salvare)
registra_giro(RUNS, level=r["level"], realized=r.get("realized"))
return 2 if r["level"] == "AZIONE" else 0