merge lavoro/goldrake-u11 (secondo giro 27/09)
This commit is contained in:
+97
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user