tradesdb: docstring corretto (jsonl ha l'ora vera dal 24/08, non piu' la barra); trades_db sync confronta i conteggi jsonl/DB e notifica una volta per episodio (#37 C11, #35 A9)
This commit is contained in:
@@ -17,6 +17,7 @@ ROOT = Path(__file__).resolve().parents[2]
|
|||||||
sys.path.insert(0, str(ROOT))
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
from src.live import tradesdb as T # noqa: E402
|
from src.live import tradesdb as T # noqa: E402
|
||||||
|
from src.live.notifier import notify # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
def sync(verbose: bool = True) -> dict:
|
def sync(verbose: bool = True) -> dict:
|
||||||
@@ -31,10 +32,35 @@ def sync(verbose: bool = True) -> dict:
|
|||||||
T.set_meta(con, "ultimo_sync", T.ora())
|
T.set_meta(con, "ultimo_sync", T.ora())
|
||||||
tot = con.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"]
|
tot = con.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"]
|
||||||
tot_e = con.execute("SELECT COUNT(*) c FROM equity").fetchone()["c"]
|
tot_e = con.execute("SELECT COUNT(*) c FROM equity").fetchone()["c"]
|
||||||
|
|
||||||
|
# issue #37 (C11). Dal 2026-08-24T01:07Z (T.CUTOFF_TS_VERO) book_executions.jsonl porta l'ora
|
||||||
|
# VERA per-fill (docstring di tradesdb.py), ma `sync()` popola il DB SOLO da cron_book.log —
|
||||||
|
# l'arricchimento dal jsonl e' TradingBook#11, non qui. Un conteggio diverso in quella
|
||||||
|
# finestra segnala che una delle due fonti sta perdendo fill. Notify UNA volta per episodio:
|
||||||
|
# marcatore in `meta` (stessa regola di fee_watch #37 B9— scritto solo se l'invio riesce,
|
||||||
|
# azzerato quando i conteggi tornano uguali).
|
||||||
|
righe_jsonl = T.parse_executions_jsonl(T.EXEC_JSONL.read_text()) if T.EXEC_JSONL.exists() else []
|
||||||
|
n_jsonl_dal = sum(1 for r in righe_jsonl if str(r.get("ts_utc", "")) >= T.CUTOFF_TS_VERO)
|
||||||
|
n_db_dal = con.execute("SELECT COUNT(*) c FROM fills WHERE ts_utc >= ?",
|
||||||
|
(T.CUTOFF_TS_VERO,)).fetchone()["c"]
|
||||||
|
diverge = n_jsonl_dal != n_db_dal
|
||||||
|
gia_detto = T.get_meta(con, "conteggio_diverso_detto") == "1"
|
||||||
|
if diverge:
|
||||||
|
msg = f"⚠️ trades.db: {n_jsonl_dal} fill nel jsonl, {n_db_dal} nel DB dal 24/08"
|
||||||
|
if verbose:
|
||||||
|
print(f" {msg}")
|
||||||
|
if not gia_detto:
|
||||||
|
if notify("⚠️ trades.db — conteggio fill diverso dal jsonl dal 24/08",
|
||||||
|
{"fill nel jsonl": n_jsonl_dal, "fill nel DB": n_db_dal}):
|
||||||
|
T.set_meta(con, "conteggio_diverso_detto", "1")
|
||||||
|
elif gia_detto:
|
||||||
|
T.set_meta(con, "conteggio_diverso_detto", "0")
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f" fills: +{n_f} (totale {tot}) | equity: +{n_e} (totale {tot_e}) | round-trip {n_r}")
|
print(f" fills: +{n_f} (totale {tot}) | equity: +{n_e} (totale {tot_e}) | round-trip {n_r}")
|
||||||
con.close()
|
con.close()
|
||||||
return dict(nuovi_fill=n_f, nuovi_equity=n_e, roundtrip=n_r, totale_fill=tot)
|
return dict(nuovi_fill=n_f, nuovi_equity=n_e, roundtrip=n_r, totale_fill=tot,
|
||||||
|
jsonl_dal_24_08=n_jsonl_dal, db_dal_24_08=n_db_dal)
|
||||||
|
|
||||||
|
|
||||||
def reconcile() -> None:
|
def reconcile() -> None:
|
||||||
@@ -42,8 +68,8 @@ def reconcile() -> None:
|
|||||||
fills = [f for r in runs for f in r.fills]
|
fills = [f for r in runs for f in r.fills]
|
||||||
righe = T.parse_executions_jsonl(T.EXEC_JSONL.read_text()) if T.EXEC_JSONL.exists() else []
|
righe = T.parse_executions_jsonl(T.EXEC_JSONL.read_text()) if T.EXEC_JSONL.exists() else []
|
||||||
rec = T.reconcile(fills, righe)
|
rec = T.reconcile(fills, righe)
|
||||||
print(f"\n cron_book.log : {len(fills)} fill (con ORA VERA)")
|
print(f"\n cron_book.log : {len(fills)} fill (ora di inizio giro)")
|
||||||
print(f" book_executions : {len(righe)} righe (senza ora)")
|
print(f" book_executions : {len(righe)} righe (ora VERA per-fill dal 24/08, #37 C11)")
|
||||||
print(f" concordano : {len(rec['ok'])}")
|
print(f" concordano : {len(rec['ok'])}")
|
||||||
print(f" solo nel log : {len(rec['solo_log'])}")
|
print(f" solo nel log : {len(rec['solo_log'])}")
|
||||||
print(f" solo nel jsonl : {len(rec['solo_jsonl'])}")
|
print(f" solo nel jsonl : {len(rec['solo_jsonl'])}")
|
||||||
|
|||||||
+23
-8
@@ -2,20 +2,31 @@
|
|||||||
|
|
||||||
PERCHE' ESISTE
|
PERCHE' ESISTE
|
||||||
--------------
|
--------------
|
||||||
`data/live/book_executions.jsonl` registra i fill, ma il suo `ts_utc` e' la data della
|
`logs/cron_book.log` e' **gitignored, non nel backup e ruotabile**, ed e' la fonte che `sync()`
|
||||||
**barra di segnale** (`book_execute.py`: `pd.Timestamp(r['last_data'])`), non l'ora del fill:
|
usa per popolare il DB: una rotazione lo cancellerebbe senza che nessuno se ne accorga.
|
||||||
19 righe su 19 a `00:00:00`. L'ora vera sta solo in `logs/cron_book.log` — che e' **gitignored,
|
|
||||||
non nel backup e ruotabile**. Quindi oggi la cronologia reale del libro live vive in un file
|
|
||||||
che una rotazione cancella, e nessuno se ne accorgerebbe.
|
|
||||||
|
|
||||||
Questo modulo MATERIALIZZA quella cronologia in `data/live/trades.db` (sqlite), che sta dentro
|
Questo modulo MATERIALIZZA quella cronologia in `data/live/trades.db` (sqlite), che sta dentro
|
||||||
il perimetro gia' coperto dal backup rotativo della VPS.
|
il perimetro gia' coperto dal backup rotativo della VPS.
|
||||||
|
|
||||||
|
⚠️ CORREZIONE (#37 C11, #35 A9). Fino al 2026-08-23 `data/live/book_executions.jsonl` scriveva in
|
||||||
|
`ts_utc` la data della **barra di segnale** (`pd.Timestamp(r['last_data'])`), non l'ora del fill:
|
||||||
|
19 righe su 19 a `00:00:00`. Dal 2026-08-24 01:07Z `book_execute.py` (`_registra_fill`) scrive
|
||||||
|
l'ora VERA del fill (`datetime.now(timezone.utc)` al momento dell'ordine, non piu' la barra),
|
||||||
|
UNA per fill — con il prezzo a piena precisione (non arrotondato come nella stampa del log) e il
|
||||||
|
campo `pos_after` (oggi sempre `None` qui: la posizione post-rebalance si legge dopo, best-effort,
|
||||||
|
e non viene riscritta nel jsonl — vedi il commento in `_registra_fill`). E' quindi, dal 24/08 in
|
||||||
|
poi, la fonte con l'ora PIU' precisa per-fill — ma resta usata solo da `reconcile()`: arricchire
|
||||||
|
`fills` con ts/prezzo/pos_after dal jsonl e' uno SVILUPPO non fatto qui (TradingBook#11).
|
||||||
|
|
||||||
FONTI, in ordine di autorita' — e nessuna delle tre e' completa da sola
|
FONTI, in ordine di autorita' — e nessuna delle tre e' completa da sola
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
1. `logs/cron_book.log` -> ORA VERA del giro + contesto (equity, tp_frac, skh, target, posizione)
|
1. `logs/cron_book.log` -> l'ora di INIZIO GIRO (una per run, condivisa da tutti i fill di quel
|
||||||
e la riga di fill `-> BUY 0.0008 @ $78,094.2 fee 0.02187 (OK)`.
|
run, non per-fill) + contesto (equity, tp_frac, skh, target,
|
||||||
2. `data/live/book_executions.jsonl` -> gli stessi fill con piu' cifre, ma senza ora.
|
posizione) e la riga di fill
|
||||||
|
`-> BUY 0.0008 @ $78,094.2 fee 0.02187 (OK)`. FONTE usata da `sync()`.
|
||||||
|
2. `data/live/book_executions.jsonl` -> gli stessi fill, con l'ora VERA per-fill dal 2026-08-24
|
||||||
|
01:07Z (prima: la data della barra di segnale, 00:00:00), prezzo a
|
||||||
|
piena precisione e `pos_after`. Entra SOLO in `reconcile()`.
|
||||||
3. venue (`DeribitRead.trade_history`) -> autorevole su `order_id` e timestamp in ms,
|
3. venue (`DeribitRead.trade_history`) -> autorevole su `order_id` e timestamp in ms,
|
||||||
**ma tronca**: al 2026-08-23 ritorna 1 trade su BTC e 0 su ETH.
|
**ma tronca**: al 2026-08-23 ritorna 1 trade su BTC e 0 su ETH.
|
||||||
|
|
||||||
@@ -42,6 +53,10 @@ EXEC_JSONL = PROJECT_ROOT / "data" / "live" / "book_executions.jsonl"
|
|||||||
|
|
||||||
# Data di armamento dell'esecuzione reale (CLAUDE.md, 2026-06-20).
|
# Data di armamento dell'esecuzione reale (CLAUDE.md, 2026-06-20).
|
||||||
ARMING = "2026-06-20"
|
ARMING = "2026-06-20"
|
||||||
|
# Da qui in poi book_executions.jsonl porta l'ora VERA del fill in `ts_utc` (fix #35 A9, docstring
|
||||||
|
# sopra): soglia per il controllo di conteggio jsonl-vs-DB in scripts/live/trades_db.py:sync()
|
||||||
|
# (#37 C11). Prima di questo istante il campo e' la barra di segnale, non comparabile.
|
||||||
|
CUTOFF_TS_VERO = "2026-08-24T01:07:00+00:00"
|
||||||
|
|
||||||
SCHEMA = """
|
SCHEMA = """
|
||||||
CREATE TABLE IF NOT EXISTS fills (
|
CREATE TABLE IF NOT EXISTS fills (
|
||||||
|
|||||||
@@ -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