Files
PythagorasGoal/scripts/live/trades_db.py
T
Adriano Dal Pastro de83909db9 un flag sconosciuto non e' l'azione di default: guardia su 18 script, indice USDE orario, pulizia
MISURATO oggi durante la revisione: `trades_db.py --help` non stampava l'uso, cadeva in sync()
e riscriveva meta.ultimo_sync. Nessuno dei 18 script di scripts/live/ usava argparse: un flag
sbagliato era il ramo else. Su journal.py avrebbe scritto pagina e riga di DB, su analista.py
avrebbe speso una chiamata al modello e mandato un Telegram.

- src/live/cli.valida: prima istruzione di ogni __main__, prima di connect()/sync/rete.
  --help -> 0 con l'uso; flag ignoto, valore mancante o posizionale -> 2 con l'elenco dei
  previsti (P4). NIENTE argparse: cambierebbe messaggi, codici d'uscita e --help di script
  che il cron gia' chiama.
- 18 script cablati (i 3 che scrivono + 14 + cc01), flag invariati.
- tests/test_cli_flag.py (30): elenco DERIVATO dalla cartella (P1), valida come prima
  istruzione, uso che documenta i flag, e i flag che il CRON usa davvero restano accettati
  (P15/P16); end-to-end su --help e flag ignoto con trades.db non toccato (M15).
  Verificato a mano: monitor_health --quiet, trades_db --sync --quiet, book_execute dry-run.

Debito §5.15, primo passo: balance_watch (orario) registra `usde_usdc` a ogni campione — None
con la ragione se illeggibile, mai 1,0. Il cablaggio nel bound quando la serie ha storia.

Pulizia dalla revisione: tests/helpers.carica_script al posto della 15a copia del loader
importlib (5 file del libro live); il fill di prova via upsert_fills invece di un INSERT che
lasciava verified NULL; asserzioni non ancorate al padding; movimenti_capitale accetta le
righe gia' lette (una SELECT invece di due ai due lati di una scrittura del cron).

Test 910 verdi (+52). Diario 2026-09-02c.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDJsH3iDSaBns3ccpPBwiu
2026-09-02 15:49:40 +00:00

144 lines
7.2 KiB
Python

"""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 (TWR, non e1/e0)
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:
# Il rendimento NON e' `e1/e0-1` sulla serie grezza: quel numero era per il 96,3% un
# bonifico (debito #14, 2026-09-01). Lo scorporo e' quello del giornale — si CHIAMA,
# non si rifa' (P1): `rendimento_twr` spezza la serie sui movimenti di capitale certi.
from src.live.journal import rendimento_twr
e0, e1 = eq[0]["equity"], eq[-1]["equity"]
picco = max(r["equity"] for r in eq)
r = rendimento_twr(con, eq[-1]["ts_utc"])
print(f" equity : ${e0:,.2f} -> ${e1:,.2f} ({e1 - e0:+,.2f} di equity, "
f"movimenti di capitale INCLUSI) | picco ${picco:,.2f} | {len(eq)} letture")
if r["eventi"]:
print(f" movimenti capitale : {r['certi']:+,.2f} certi | {r['ambigui']:+,.2f} ambiguo/i"
f" (restano nel P&L, dichiarati)")
for ev in r["eventi"]:
mk = ev.get("mercato_max_pct")
dettaglio = (ev.get("mercato_nota") or "mercato non misurabile") if mk is None \
else f"mercato max {100 * mk:.2f}% a leva piena, salto {100 * ev['pct']:+.1f}%"
print(f" {ev['ts_dopo'][:16]} {ev['delta']:+,.2f} [{ev['classe']}: {dettaglio}]")
if r["trading"] is None:
print(f" trading da arming : n/d ({r['motivo']})")
else:
print(f" trading da arming : {r['trading']:+,.2f} (equity al netto dei movimenti "
f"certi; l'ora del movimento esce intera, vedi journal.rendimento_twr)")
if r["twr"] is None:
print(f" TWR : n/d ({r['motivo']})")
else:
segs = " x ".join(f"{100*s['ret']:+.2f}% [{s['da'][:10]} -> {s['a'][:10]}]"
for s in r["segmenti"])
print(f" TWR : {100*r['twr']:+.2f}% = {segs}")
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()
USO = """uso: trades_db.py [--sync] [--report] [--reconcile] [--quiet]
--sync (default) legge log e jsonl e aggiorna data/live/trades.db — idempotente
--report stato del libro di bordo + P&L (TWR, non e1/e0)
--reconcile incrocio delle tre fonti sui fill (log x jsonl x venue)
--quiet con --sync: non stampa il riepilogo"""
if __name__ == "__main__":
from src.live.cli import valida
args = valida("trades_db.py", USO, flag=("--sync", "--report", "--reconcile", "--quiet"))
if "--reconcile" in args:
reconcile()
elif "--report" in args:
report()
else:
sync(verbose="--quiet" not in args)