"""Sincronizza `data/live/trades.db` — il libro di bordo dei trade, allineato col tempo. uv run python scripts/live/trades_db.py --sync # idempotente, da cron uv run python scripts/live/trades_db.py --report # stato + P&L a video uv run python scripts/live/trades_db.py --reconcile # incrocio delle tre fonti SOLA LETTURA sul venue e sui log: non manda ordini, non tocca il libro. Il DB sta in `data/live/`, che il backup rotativo della VPS gia' copre — a differenza di `logs/cron_book.log`, che e' oggi l'unica sede dell'ora vera dei fill. """ from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from src.live import tradesdb as T # noqa: E402 def sync(verbose: bool = True) -> dict: con = T.connect() runs = T.parse_cron_log(T.CRON_LOG.read_text(errors="replace")) if T.CRON_LOG.exists() else [] fills = [f for r in runs for f in r.fills] n_f = T.upsert_fills(con, fills) n_e = T.upsert_equity(con, [(r.ts_utc, r.equity, "cron_book") for r in runs if r.equity is not None]) n_r = T.rebuild_roundtrips(con) if runs: T.set_meta(con, "ultimo_giro", runs[-1].ts_utc) T.set_meta(con, "ultimo_sync", T.ora()) tot = con.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"] tot_e = con.execute("SELECT COUNT(*) c FROM equity").fetchone()["c"] if verbose: print(f" fills: +{n_f} (totale {tot}) | equity: +{n_e} (totale {tot_e}) | round-trip {n_r}") con.close() return dict(nuovi_fill=n_f, nuovi_equity=n_e, roundtrip=n_r, totale_fill=tot) def reconcile() -> None: runs = T.parse_cron_log(T.CRON_LOG.read_text(errors="replace")) 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 [] rec = T.reconcile(fills, righe) print(f"\n cron_book.log : {len(fills)} fill (con ORA VERA)") print(f" book_executions : {len(righe)} righe (senza ora)") print(f" concordano : {len(rec['ok'])}") print(f" solo nel log : {len(rec['solo_log'])}") print(f" solo nel jsonl : {len(rec['solo_jsonl'])}") print(f" prezzi divergenti : {len(rec['prezzi_divergenti'])} (oltre l'arrotondamento di stampa)") for f in rec["solo_log"]: print(f" solo-log {f.ts_utc} {f.asset} {f.side} {f.qty} @ {f.price}") for r in rec["solo_jsonl"]: print(f" solo-jsonl {r['ts_utc'][:10]} {r['asset']} {r['side']} {r['filled']} @ {r['price']}") # il venue: autorevole ma TRONCATO -> si riporta cosa vede, non lo si usa per riparare try: from src.live.deribit import DeribitRead d = DeribitRead() for ins in ("BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"): t = d.trade_history(ins, limit=100) print(f" venue {ins:<20}: {len(t)} trade visibili (l'endpoint TRONCA: non e' un backfill)") except Exception as e: # "NON LETTO" resta (P5: *non vedo* non e' *zero*), ma da solo non diceva DI CHI e' il # guasto: il 2026-08-25 questa riga stampava `HTTPError` mentre Deribit era in # manutenzione annunciata, indistinguibile da un gateway rotto. Ora porta il perche' (P4). from src.live.venue_probe import diagnose d = diagnose([f"{type(e).__name__}: {e}"]) print(f" venue: NON LETTO ({type(e).__name__}) — non e' 'zero trade', e' 'non misurato'") print(f" diagnosi: {d.riga()}") def report() -> None: con = T.connect() n = con.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"] if not n: print(" DB vuoto: lanciare --sync"); return p = con.execute("SELECT MIN(ts_utc) a, MAX(ts_utc) b FROM fills").fetchone() print(f"\n fill registrati : {n} dal {p['a'][:16]} al {p['b'][:16]}") eq = con.execute("SELECT ts_utc, equity FROM equity ORDER BY ts_utc").fetchall() if eq: e0, e1 = eq[0]["equity"], eq[-1]["equity"] picco = max(r["equity"] for r in eq) print(f" equity : ${e0:,.2f} -> ${e1:,.2f} ({e1 - e0:+.2f}, {100*(e1/e0-1):+.2f}%)" f" | picco ${picco:,.2f} | {len(eq)} letture") rt = con.execute("SELECT * FROM roundtrips ORDER BY ts_out").fetchall() lordo = sum(r["pnl_lordo"] for r in rt) fee_rt = sum(r["fee_quota"] for r in rt) fee_tot = con.execute("SELECT COALESCE(SUM(fee),0) s FROM fills").fetchone()["s"] vinc = sum(1 for r in rt if r["pnl_netto"] > 0) print(f" round-trip chiusi : {len(rt)} ({vinc} in utile) " f"lordo {lordo:+.2f} | fee allocate {fee_rt:.2f} | netto {lordo - fee_rt:+.2f}") print(f" fee totali pagate : {fee_tot:.4f}") ap = T.stato_aperto(con) if ap: print(" posizioni aperte :") for a, d in sorted(ap.items()): print(f" {a} {d['qty']:.4f} @ medio ${d['prezzo_medio']:,.2f} (dal {d['dal'][:16]})") else: print(" posizioni aperte : nessuna (flat)") print(f" ultimo sync : {T.get_meta(con, 'ultimo_sync', 'mai')}") con.close() if __name__ == "__main__": args = sys.argv[1:] if "--reconcile" in args: reconcile() elif "--report" in args: report() else: sync(verbose="--quiet" not in args)