tradesdb: fill cron_log arricchiti con ora, prezzo e pos_after dal jsonl (#40)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 17:51:15 +00:00
parent ae887a3d98
commit bebfa63213
3 changed files with 252 additions and 12 deletions
+147
View File
@@ -0,0 +1,147 @@
"""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