diff --git a/scripts/live/fee_watch.py b/scripts/live/fee_watch.py index 19fd5dc..36bb057 100644 --- a/scripts/live/fee_watch.py +++ b/scripts/live/fee_watch.py @@ -245,12 +245,20 @@ def main() -> int: else: print("\n cross-check sui trade reali: NON MISURATO (nessun trade recente leggibile)") - if r["changes"] or r["level"] == "AZIONE": - notify("💸 FEE WATCH — schema fee Deribit", { + # issue #29 (B8): save_state veniva chiamato SEMPRE, anche se l'invio necessario falliva — + # il giro dopo `diff_vs` non avrebbe piu' visto il cambiamento (lo stato salvato era gia' + # quello nuovo) e il cambio andava perso per sempre invece che ritentare al giro dopo. + serve_invio = bool(r["changes"] or r["level"] == "AZIONE") + inviato = True + if serve_invio: + inviato = notify("💸 FEE WATCH — schema fee Deribit", { "verdetto": f"[{r['level']}] {r['reason']}", **{f"cambio {i+1}": c for i, c in enumerate(r["changes"])}, }) - save_state(r["current"]) + if not inviato and (not quiet or interessante): + print(" ⚠️ invio FALLITO — stato NON salvato: il prossimo giro ritenta") + if not serve_invio or inviato: + save_state(r["current"]) registra_giro(RUNS, level=r["level"]) return 2 if r["level"] == "AZIONE" else 0 diff --git a/tests/test_fee_watch.py b/tests/test_fee_watch.py index 481a872..86a4bac 100644 --- a/tests/test_fee_watch.py +++ b/tests/test_fee_watch.py @@ -162,3 +162,54 @@ def test_un_fill_non_misurabile_e_None_non_zero(): 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 + + +# =========================================================================== +# issue #29 (B8) — save_state SOLO se l'invio (quando serviva) e' riuscito +# =========================================================================== +# Fino a qui `save_state` girava SEMPRE dopo l'eventuale notify, anche se l'invio necessario +# falliva: il giro dopo `diff_vs` avrebbe confrontato lo stato nuovo con se stesso e il cambio +# sarebbe sparito senza essere mai stato detto. + +_RIS_AZIONE = dict( + current={"BTC_USDC-PERPETUAL": dict(taker_bps=12.0, maker_bps=0.0, liq_bps=100.0)}, + changes=["BTC_USDC-PERPETUAL taker: 5.00 -> 12.00 bps"], worst_taker_bps=12.0, + level="AZIONE", reason="taker 12.0bps > 10: rivedere il peso di SKH01", first_read=False, +) +_RIS_OK_SENZA_CAMBI = dict( + current={"BTC_USDC-PERPETUAL": dict(taker_bps=3.0, maker_bps=0.0, liq_bps=75.0)}, + changes=[], worst_taker_bps=3.0, level="OK", reason="ok", first_read=False, +) + + +def _prepara(monkeypatch, risultato, esito_invio): + salvato, inviato = [], [] + monkeypatch.setattr(FW, "run", lambda: risultato) + monkeypatch.setattr(FW, "save_state", lambda cur: salvato.append(cur)) + monkeypatch.setattr(FW, "notify", lambda *a, **k: inviato.append((a, k)) or esito_invio) + monkeypatch.setattr(FW, "registra_giro", lambda *a, **k: None) + monkeypatch.setattr(FW, "realized_fee_bps", lambda *a, **k: {}) + return salvato, inviato + + +def test_invio_fallito_non_salva_lo_stato(monkeypatch): + salvato, inviato = _prepara(monkeypatch, _RIS_AZIONE, esito_invio=False) + rc = FW.main() + assert len(inviato) == 1, "un cambio con livello AZIONE deve tentare l'invio" + assert salvato == [], "invio fallito -> non si salva, il giro dopo ritenta lo stesso cambio" + assert rc == 2 + + +def test_invio_riuscito_salva_lo_stato(monkeypatch): + salvato, inviato = _prepara(monkeypatch, _RIS_AZIONE, esito_invio=True) + FW.main() + assert len(inviato) == 1 + assert len(salvato) == 1 and salvato[0] == _RIS_AZIONE["current"] + + +def test_nessun_invio_dovuto_salva_comunque(monkeypatch): + """Senza cambi e a livello OK non c'e' niente da dire: si salva, e non si chiama notify.""" + salvato, inviato = _prepara(monkeypatch, _RIS_OK_SENZA_CAMBI, esito_invio=False) + FW.main() + assert inviato == [], "senza changes e livello OK non deve tentare l'invio" + assert len(salvato) == 1