"""tradesdb.arricchisci_da_jsonl (#40, TradingBook#11): le righe `ts_source='cron_log'` dal 24/08 (`CUTOFF_TS_VERO`) prendono l'ora vera, il prezzo pieno e `pos_after` dal jsonl, SOLO quando l'abbinamento (asset, side, qty, finestra di 15 minuti) e' univoco. `fill_id` e' la chiave della riga: l'arricchimento aggiorna le colonne con un UPDATE per `fill_id`, mai la chiave stessa. Nessun test tocca la rete o il libro vero: DB e righe jsonl sono finti, in tmp_path. """ from __future__ import annotations import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from src.live import tradesdb as T # noqa: E402 # ts di riferimento: subito dopo la soglia, cosi' i fill sono sempre candidati. T0 = "2026-08-24T02:00:00+00:00" @pytest.fixture() def db(tmp_path): con = T.connect(tmp_path / "t.db") yield con con.close() def _f(ts, asset="BTC", side="buy", qty=0.005, price=70000.0, fee=0.02): return T.Fill(ts_utc=ts, asset=asset, side=side, qty=qty, price=price, fee=fee) def _riga_jsonl(ts, asset="BTC", side="buy", filled=0.005, price=70123.4, pos_after=None): return dict(ts_utc=ts, asset=asset, side=side, filled=filled, price=price, pos_after=pos_after) def test_abbinamento_unico_aggiorna_e_non_cambia_fill_id(db): fill = _f(T0) T.upsert_fills(db, [fill]) fill_id_prima = fill.fill_id righe = [_riga_jsonl("2026-08-24T02:05:00+00:00", pos_after=555.0)] # +5 min, in finestra aggiornate, ambigue = T.arricchisci_da_jsonl(db, righe) assert (aggiornate, ambigue) == (1, 0) riga = db.execute("SELECT * FROM fills WHERE fill_id=?", (fill_id_prima,)).fetchone() assert riga is not None, "il fill_id originale resta la chiave: l'UPDATE non lo cambia" assert riga["ts_source"] == "jsonl" assert riga["ts_utc"] == "2026-08-24T02:05:00+00:00" assert riga["price"] == 70123.4 assert riga["pos_after"] == 555.0 # nessuna riga in piu' o in meno: l'arricchimento aggiorna, non inserisce. assert db.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"] == 1 def test_pos_after_none_nel_jsonl_non_sovrascrive_quello_gia_nel_db(db): """(#40, punto 1) `pos_after=None` nella riga jsonl non e' un valore: la colonna resta quella che aveva (qui: None dal cron_log, ma la regola e' 'non toccare', non 'azzerare').""" fill = _f(T0) T.upsert_fills(db, [fill]) righe = [_riga_jsonl("2026-08-24T02:05:00+00:00", pos_after=None)] aggiornate, ambigue = T.arricchisci_da_jsonl(db, righe) assert (aggiornate, ambigue) == (1, 0) riga = db.execute("SELECT * FROM fills").fetchone() assert riga["ts_source"] == "jsonl" and riga["price"] == 70123.4 assert riga["pos_after"] is None def test_due_jsonl_compatibili_e_ambiguo_non_si_tocca(db): fill = _f(T0) T.upsert_fills(db, [fill]) righe = [_riga_jsonl("2026-08-24T02:03:00+00:00", price=70100.0), _riga_jsonl("2026-08-24T02:07:00+00:00", price=70200.0)] # entrambe in finestra aggiornate, ambigue = T.arricchisci_da_jsonl(db, righe) assert (aggiornate, ambigue) == (0, 1) riga = db.execute("SELECT * FROM fills").fetchone() assert riga["ts_source"] == "cron_log" # non toccata assert riga["ts_utc"] == T0 assert riga["price"] == 70000.0 def test_secondo_giro_zero_aggiornati_idempotente(db): fill = _f(T0) T.upsert_fills(db, [fill]) righe = [_riga_jsonl("2026-08-24T02:05:00+00:00", pos_after=555.0)] assert T.arricchisci_da_jsonl(db, righe) == (1, 0) # la riga e' gia' ts_source='jsonl': non e' piu' candidata, anche con le stesse righe jsonl. assert T.arricchisci_da_jsonl(db, righe) == (0, 0) assert db.execute("SELECT COUNT(*) c FROM fills WHERE ts_source='jsonl'").fetchone()["c"] == 1 def test_riga_prima_del_cutoff_non_e_candidata(db): ts_prima = "2026-08-10T00:00:00+00:00" # prima di CUTOFF_TS_VERO (24/08) fill = _f(ts_prima) T.upsert_fills(db, [fill]) righe = [_riga_jsonl("2026-08-10T00:05:00+00:00", pos_after=1.0)] # abbinamento perfetto aggiornate, ambigue = T.arricchisci_da_jsonl(db, righe) assert (aggiornate, ambigue) == (0, 0) # non e' ambigua: non e' nemmeno candidata riga = db.execute("SELECT * FROM fills").fetchone() assert riga["ts_source"] == "cron_log" and riga["ts_utc"] == ts_prima and riga["price"] == 70000.0 def test_fuori_dalla_finestra_dei_15_minuti_non_abbina(db): fill = _f(T0) T.upsert_fills(db, [fill]) righe = [_riga_jsonl("2026-08-24T02:16:00+00:00", pos_after=1.0)] # +16 min: fuori finestra assert T.arricchisci_da_jsonl(db, righe) == (0, 1) riga = db.execute("SELECT * FROM fills").fetchone() assert riga["ts_source"] == "cron_log" def test_qty_diversa_non_abbina(db): fill = _f(T0, qty=0.005) T.upsert_fills(db, [fill]) righe = [_riga_jsonl("2026-08-24T02:05:00+00:00", filled=0.006, pos_after=1.0)] assert T.arricchisci_da_jsonl(db, righe) == (0, 1) def test_record_jsonl_usato_una_volta_non_si_riusa(db): """Due fill IDENTICI nello stesso giro (stesso asset/side/qty, ts diversi) non devono finire abbinati alla stessa riga jsonl: la prima riga jsonl la prende un solo fill_id.""" f1 = _f("2026-08-24T02:00:00+00:00") f2 = _f("2026-08-24T02:01:00+00:00") # ts diverso -> fill_id diverso, stesso asset/side/qty T.upsert_fills(db, [f1, f2]) assert db.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"] == 2 righe = [_riga_jsonl("2026-08-24T02:05:00+00:00", pos_after=1.0)] # UNA sola riga jsonl aggiornate, ambigue = T.arricchisci_da_jsonl(db, righe) # un solo candidato jsonl per due righe DB candidate: la prima (ordinata per ts_utc) lo # prende, la seconda resta senza candidati liberi -> ambigua (zero disponibili). assert aggiornate == 1 assert ambigue == 1 presi = db.execute("SELECT COUNT(*) c FROM fills WHERE ts_source='jsonl'").fetchone()["c"] assert presi == 1