merge lavoro/goldrake-u11 (secondo giro 27/09)

This commit is contained in:
2026-09-27 15:40:43 +00:00
7 changed files with 400 additions and 24 deletions
+97 -1
View File
@@ -204,7 +204,10 @@ 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"]
# (#37 B9) lo stato salvato porta anche il marcatore "divergenza_detta" (qui False: la
# fixture non ha divergenze), sibling delle fee per-strumento, non piu' solo `current`.
assert len(salvato) == 1
assert salvato[0] == {**_RIS_AZIONE["current"], "divergenza_detta": False}
def test_nessun_invio_dovuto_salva_comunque(monkeypatch):
@@ -213,3 +216,96 @@ def test_nessun_invio_dovuto_salva_comunque(monkeypatch):
FW.main()
assert inviato == [], "senza changes e livello OK non deve tentare l'invio"
assert len(salvato) == 1
# ===========================================================================
# issue #37 (B9) — realized_fee_bps() SEMPRE calcolato (non solo nel blocco di stampa) e
# divergenza dal tier pubblico notificata UNA volta per episodio.
# ===========================================================================
def test_run_calcola_sempre_la_fee_realizzata(monkeypatch):
"""Prima del #37 la fee realizzata girava solo dentro `if not quiet or interessante`: in
cron (--quiet) non girava mai. Ora e' `run()` a calcolarla, sempre."""
monkeypatch.setattr(FW, "fetch_fees",
lambda ins: dict(instrument=ins, taker_bps=5.0, maker_bps=0.0, liq_bps=75.0))
monkeypatch.setattr(FW, "load_state", lambda: {})
ins = FW.INSTRUMENTS[0]
monkeypatch.setattr(FW, "realized_fee_bps", lambda limit=20: {ins: 7.5})
r = FW.run()
assert r["realized"] == {ins: 7.5}
assert len(r["divergenze"]) == 1 and ins in r["divergenze"][0]
assert r["divergenza_gia_detta"] is False
def test_run_nessun_trade_e_None_non_zero(monkeypatch):
monkeypatch.setattr(FW, "fetch_fees",
lambda ins: dict(instrument=ins, taker_bps=5.0, maker_bps=0.0, liq_bps=75.0))
monkeypatch.setattr(FW, "load_state", lambda: {})
monkeypatch.setattr(FW, "realized_fee_bps", lambda limit=20: {})
r = FW.run()
assert r["realized"] is None
assert r["divergenze"] == []
def test_run_sotto_soglia_non_e_divergenza(monkeypatch):
monkeypatch.setattr(FW, "fetch_fees",
lambda ins: dict(instrument=ins, taker_bps=5.0, maker_bps=0.0, liq_bps=75.0))
monkeypatch.setattr(FW, "load_state", lambda: {})
ins = FW.INSTRUMENTS[0]
monkeypatch.setattr(FW, "realized_fee_bps", lambda limit=20: {ins: 5.3}) # +0.3 < 1.0
r = FW.run()
assert r["divergenze"] == []
_RIS_DIVERGE = dict(
current={"BTC_USDC-PERPETUAL": dict(taker_bps=5.0, maker_bps=0.0, liq_bps=75.0)},
changes=[], worst_taker_bps=5.0, level="OK", reason="ok", first_read=False,
realized={"BTC_USDC-PERPETUAL": 7.5},
divergenze=["BTC_USDC-PERPETUAL: realizzata 7.50 bps vs tier pubblico 5.00 (+2.50)"],
divergenza_gia_detta=False,
)
_RIS_DIVERGE_RIPETUTA = dict(_RIS_DIVERGE, divergenza_gia_detta=True)
_RIS_DIVERGE_RIENTRATA = dict(
current={"BTC_USDC-PERPETUAL": dict(taker_bps=5.0, maker_bps=0.0, liq_bps=75.0)},
changes=[], worst_taker_bps=5.0, level="OK", reason="ok", first_read=False,
realized={"BTC_USDC-PERPETUAL": 5.3}, divergenze=[], divergenza_gia_detta=True,
)
def _prepara_b9(monkeypatch, risultato, esito_invio):
"""Come `_prepara`, ma senza `changes`/livello AZIONE: isola il notify di B9 da quello di
B8 (qui non richiesto, dato che tutte le fixture hanno level='OK' e changes=[])."""
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)
return salvato, inviato
def test_divergenza_prima_volta_notifica_e_marca(monkeypatch):
salvato, inviato = _prepara_b9(monkeypatch, _RIS_DIVERGE, esito_invio=True)
FW.main()
assert len(inviato) == 1
assert salvato[-1]["divergenza_detta"] is True
def test_divergenza_stesso_episodio_non_ripete(monkeypatch):
salvato, inviato = _prepara_b9(monkeypatch, _RIS_DIVERGE_RIPETUTA, esito_invio=True)
FW.main()
assert inviato == [], "gia' detta in questo episodio: nessun secondo invio"
assert salvato[-1]["divergenza_detta"] is True
def test_divergenza_notify_fallita_non_marca(monkeypatch):
salvato, inviato = _prepara_b9(monkeypatch, _RIS_DIVERGE, esito_invio=False)
FW.main()
assert len(inviato) == 1
assert salvato[-1]["divergenza_detta"] is False, \
"invio fallito -> marcatore non scritto, il giro dopo ritenta"
def test_divergenza_rientrata_azzera_il_marcatore(monkeypatch):
salvato, inviato = _prepara_b9(monkeypatch, _RIS_DIVERGE_RIENTRATA, esito_invio=False)
FW.main()
assert inviato == [], "sotto soglia: nessun invio da tentare"
assert salvato[-1]["divergenza_detta"] is False
+52 -1
View File
@@ -68,14 +68,65 @@ def test_i_sorveglianti_vengono_prima_delle_voci_grandi(tmp_path):
def test_il_troncamento_e_dichiarato(tmp_path, monkeypatch):
"""CLAUDE.md e' la prima voce e ha un tetto SUO (MAX_CHARS_CLAUDE_MD, #37 C12): si monkeypatcha
quello, non il MAX_CHARS_VOCE generico, che non lo tocca piu'."""
r = _radice(tmp_path)
(r / "CLAUDE.md").write_text("x" * 1000)
monkeypatch.setattr(R, "MAX_CHARS_VOCE", 100)
monkeypatch.setattr(R, "MAX_CHARS_CLAUDE_MD", 100)
n, t, tr = R.materiale(r, ADESSO)[0]
assert tr and "[… troncato a 100 caratteri su 1,000]" in t
assert "[TRONCATO]" in R.costruisci_prompt([(n, t, tr)], ADESSO)
def test_claude_md_ha_un_tetto_suo_distinto_dal_generico(tmp_path, monkeypatch):
"""#37 C12: CLAUDE.md sfiora MAX_CHARS_VOCE con un margine minimo — deve avere un tetto suo
piu' largo, cosi' un piccolo aggiornamento non lo tronca."""
r = _radice(tmp_path)
n_chars = R.MAX_CHARS_VOCE + 1_000 # sopra il generico, ma sotto MAX_CHARS_CLAUDE_MD
assert n_chars < R.MAX_CHARS_CLAUDE_MD
(r / "CLAUDE.md").write_text("x" * n_chars)
n, t, tr = R.materiale(r, ADESSO)[0]
assert n.startswith("CLAUDE.md")
assert not tr, "sotto il tetto SUO (150k): non deve troncare, anche se supera i 90k generici"
def test_voce_vicina_al_tetto_e_avvisata_nel_prompt_e_nel_rapporto(tmp_path, monkeypatch):
"""#37 C12: al 95% del tetto (non ancora troncata) l'avviso entra nel testo, e da li' arriva
sia al prompt del revisore sia alla tabella del rapporto — come [TRONCATO] oggi."""
r = _radice(tmp_path)
monkeypatch.setattr(R, "MAX_CHARS_CLAUDE_MD", 10_000)
(r / "CLAUDE.md").write_text("x" * 9_600) # 96% di 10.000, margine largo per la nota
n, t, tr = R.materiale(r, ADESSO)[0]
assert not tr, "non e' troncata: e' vicina"
assert R._MARCA_VICINO_TETTO in t
assert "[VICINO AL TETTO]" in R.costruisci_prompt([(n, t, tr)], ADESSO)
rap = R.rapporto("revisione", None, "ok", [], "claude-fable-5-1", ADESSO, [(n, t, tr)])
assert "| quasi |" in rap
def test_voce_lontana_dal_tetto_non_e_avvisata(tmp_path):
voci = R.materiale(_radice(tmp_path), ADESSO)
claude_md = [t for n, t, tr in voci if n.startswith("CLAUDE.md")][0]
assert R._MARCA_VICINO_TETTO not in claude_md
def test_il_totale_resta_sotto_il_tetto_anche_se_la_somma_dei_tetti_lo_supera(tmp_path, monkeypatch):
"""#37 C12: CLAUDE.md (150k) + il resto delle voci (90k l'una) puo' superare facilmente i 400k
di MAX_CHARS_TOTALE in somma — il tetto totale e' applicato A PARTE, a valle, e resta il vero
vincolo: non deve MAI sfondare, qualunque sia il tetto di ogni singola voce."""
r = _radice(tmp_path)
(r / "CLAUDE.md").write_text("x" * R.MAX_CHARS_CLAUDE_MD) # al SUO tetto: 150k
j = r / "docs" / "journal"
for i in range(1, 10): # sovrascrive i giornali della finestra
g = (ADESSO - timedelta(days=i)).date().isoformat()
(j / f"{g}.md").write_text("y" * R.MAX_CHARS_VOCE) # ognuno al SUO tetto: 90k
voci = R.materiale(r, ADESSO)
tot = sum(len(t) for _, t, _ in voci)
somma_tetti = sum(R._cap_voce(n) for n, _, _ in voci)
assert somma_tetti > R.MAX_CHARS_TOTALE, "il caso e' quello che sfonderebbe se il totale non fosse a parte"
assert tot <= R.MAX_CHARS_TOTALE
def test_il_prompt_porta_le_regole_e_la_data(tmp_path):
p = R.costruisci_prompt(R.materiale(_radice(tmp_path), ADESSO), ADESSO)
assert "DATA DELLA REVISIONE: 2026-09-14 06:15Z" in p
+120
View File
@@ -0,0 +1,120 @@
"""scripts/live/trades_db.py:sync() — controllo di conteggio jsonl-vs-DB (#37 C11, #35 A9).
Dal 2026-08-24T01:07Z (`tradesdb.CUTOFF_TS_VERO`) `book_executions.jsonl` porta l'ora VERA del
fill: un conteggio diverso da quello del DB in quella finestra segnala che una delle due fonti
sta perdendo fill. `sync()` non ripara (come `reconcile()`): riporta e notifica UNA volta per
episodio, con lo stesso marcatore-solo-su-invio-riuscito di fee_watch (#37 B9).
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "live"))
from src.live import tradesdb as T # noqa: E402
import trades_db as TD # noqa: E402
@pytest.fixture()
def redirige(monkeypatch, tmp_path):
"""DB, cron_book.log e jsonl finti in tmp_path: non si tocca mai il libro vero."""
reale = T.connect
monkeypatch.setattr(T, "connect", lambda *a, **k: reale(tmp_path / "t.db"))
monkeypatch.setattr(T, "CRON_LOG", tmp_path / "cron_book.log") # assente -> nessun fill dal log
monkeypatch.setattr(T, "EXEC_JSONL", tmp_path / "book_executions.jsonl")
return tmp_path
def _semina_db(tmp_path, n, ts=None):
ts = ts or T.CUTOFF_TS_VERO
con = T.connect(tmp_path / "t.db")
fills = [T.Fill(ts_utc=ts, asset="BTC", side="buy", qty=0.001 * (i + 1), price=70000.0, fee=0.01)
for i in range(n)]
T.upsert_fills(con, fills)
con.close()
def _scrivi_jsonl(tmp_path, n, ts=None):
ts = ts or T.CUTOFF_TS_VERO
righe = [dict(ts_utc=ts, asset="BTC", side="buy", filled=0.001 * (i + 1), price=70000.0, fee=0.01)
for i in range(n)]
(tmp_path / "book_executions.jsonl").write_text(
"\n".join(json.dumps(r) for r in righe) + ("\n" if righe else ""))
def test_conteggi_uguali_non_notifica(monkeypatch, redirige):
tmp_path = redirige
_semina_db(tmp_path, 2)
_scrivi_jsonl(tmp_path, 2)
inviato = []
monkeypatch.setattr(TD, "notify", lambda *a, **k: inviato.append((a, k)) or True)
r = TD.sync(verbose=False)
assert inviato == []
assert r["jsonl_dal_24_08"] == r["db_dal_24_08"] == 2
def test_conteggi_diversi_notifica_una_volta_per_episodio(monkeypatch, redirige):
tmp_path = redirige
_semina_db(tmp_path, 2)
_scrivi_jsonl(tmp_path, 3)
inviato = []
monkeypatch.setattr(TD, "notify", lambda *a, **k: inviato.append((a, k)) or True)
r = TD.sync(verbose=False)
assert len(inviato) == 1, "conteggi diversi: un invio"
assert r["jsonl_dal_24_08"] == 3 and r["db_dal_24_08"] == 2
# secondo giro, stessa divergenza -> nessun invio aggiuntivo (marcatore gia' scritto)
r2 = TD.sync(verbose=False)
assert len(inviato) == 1, "stesso episodio: nessun secondo invio"
assert r2["jsonl_dal_24_08"] == 3 and r2["db_dal_24_08"] == 2
def test_invio_fallito_non_marca_ritenta_al_giro_dopo(monkeypatch, redirige):
tmp_path = redirige
_semina_db(tmp_path, 2)
_scrivi_jsonl(tmp_path, 3)
inviato = []
monkeypatch.setattr(TD, "notify", lambda *a, **k: inviato.append((a, k)) or False)
TD.sync(verbose=False)
TD.sync(verbose=False)
assert len(inviato) == 2, "invio fallito -> marcatore non scritto, si ritenta ogni giro"
def test_conteggio_rientrato_azzera_il_marcatore(monkeypatch, redirige):
tmp_path = redirige
_semina_db(tmp_path, 2)
_scrivi_jsonl(tmp_path, 3)
monkeypatch.setattr(TD, "notify", lambda *a, **k: True)
TD.sync(verbose=False) # divergenza detta
# il jsonl si allinea (es. arrivato il fill mancante) -> episodio chiuso
_scrivi_jsonl(tmp_path, 2)
inviato = []
monkeypatch.setattr(TD, "notify", lambda *a, **k: inviato.append((a, k)) or True)
TD.sync(verbose=False)
assert inviato == [], "conteggi tornati uguali: nessun invio da fare"
# una NUOVA divergenza deve poter notificare di nuovo (il marcatore era stato azzerato)
_scrivi_jsonl(tmp_path, 4)
TD.sync(verbose=False)
assert len(inviato) == 1
def test_fill_prima_del_24_08_non_entra_nel_conteggio(monkeypatch, redirige):
"""Prima di CUTOFF_TS_VERO il ts_utc del jsonl e' la barra di segnale, non l'ora vera: non
e' comparabile col DB, e non deve contare come divergenza."""
tmp_path = redirige
_semina_db(tmp_path, 0)
_scrivi_jsonl(tmp_path, 2, ts="2026-08-10T00:00:00+00:00") # prima della soglia
inviato = []
monkeypatch.setattr(TD, "notify", lambda *a, **k: inviato.append((a, k)) or True)
r = TD.sync(verbose=False)
assert inviato == []
assert r["jsonl_dal_24_08"] == 0 and r["db_dal_24_08"] == 0