diff --git a/CLAUDE.md b/CLAUDE.md index 518eeee..a7f7951 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,6 +98,7 @@ uv run python scripts/analysis/regime_fetcher.py # fetch DVOL+funding uv run python scripts/analysis/exit_lab.py # (ri)costruisci cache segnali exit-lab + parity check uv run python -m src.live.dashboard --port 8787 # dashboard web (servizio compose 'dashboard', porta 8787) uv run python scripts/analysis/ledger_vs_backtest.py --since 2026-06-13 # report fuga di esecuzione (reale vs sim) +uv run python scripts/analysis/trades_status.py [--since ISO] # report "stato trades": POOL reale vs PAPER-STATS, realizzato + unrealized ./scripts/deploy.sh [patch|minor|major] # DEPLOY: bump versione + commit + rebuild Docker uv run pytest # test ``` @@ -594,6 +595,14 @@ long A / short B sullo z-score del log-ratio, fee su 2 gambe). - **Fee:** Deribit perp reale = taker ~0.05%/lato (**0.10% round-trip**), maker ~0%. Usare 0.10% RT come baseline (lo 0.20% storico era pessimista 2x). Includere SEMPRE nel backtest: sono vincolo di prim'ordine, molte operazioni = morte per fee. Il worker usa `strategy.fee_rt` (MR01 = 0.001). - **Leva:** testato con 3x. Aumentare a 5x migliora i rendimenti ma raddoppia il drawdown. - **GBM:** GradientBoostingClassifier di scikit-learn. Ensemble di alberi decisionali sequenziali. Walk-forward per evitare leakage temporale. +- **Report "stato trades" — separare SEMPRE pool reale e paper-stats.** La directory è la + fonte di verità: `data/portfolio_paper/` = sleeve nel POOL (capitale reale, muovono + equity/conto); `data/portfolio_paper_stats/` = TR01/ROT02/TSM01/**XS01** in SOLA statistica + (PnL a precisione frazionaria, MAI nel ledger). **NON** fare glob su `portfolio_paper*` + (matcha entrambi → gli utili sim dei paper-stats inquinano il realizzato: errore reale del + 2026-06-17, +8.15 XS01 attribuiti per sbaglio all'equity). Verità aggregata = ledger PORT06 + (`equity`, `total_capital`; unrealized = equity−total_capital). Usare + `scripts/analysis/trades_status.py` (riconcilia: realizzato POOL + Δunrealized ≈ Δequity). - **Cerbero `get_historical` (fix 2026-05-28):** `end_date` come data nuda è inclusivo dell'intera giornata fino all'ultima candela chiusa (es. `end=oggi` arriva fino ad ora, non più a mezzanotte); accettati anche timestamp con orario (`...T14:00:00`, naive=UTC); nessun cap a ~5000 righe (paginazione interna). Il client passa già `end=oggi`, ora corretto. Prima del fix il paper trader restava a zero trade perché il feed era fermo a mezzanotte. - **Dati ETH Deribit 15m:** 14-30%/anno di candele *flat* (O=H=L=C, volume 0, run fino a ~54h) per bassa liquidità del perpetuo. Verificato (2026-05-28): escluderle NON cambia i backtest (Δacc ≤0.5pp) → edge robusto. Resta un caveat operativo (slippage/fill in trading reale, irrilevante per paper). BTC pulito eccetto picco ~8% nel 2024. - **FEED TESTNET INAFFIDABILE — `ETH-PERPETUAL` inverse CONGELATO (2026-06-13).** Il feed di diff --git a/scripts/analysis/trades_status.py b/scripts/analysis/trades_status.py new file mode 100644 index 0000000..a66c48e --- /dev/null +++ b/scripts/analysis/trades_status.py @@ -0,0 +1,156 @@ +"""Report "stato trades" — separa SEMPRE pool reale e paper-stats, e mostra sia +realizzato che unrealized. Verita' aggregata = ledger PORT06; il dettaglio per-trade +viene dai worker dir. + +Fonte unica della separazione (NON la memoria — qui ho gia' sbagliato una volta +includendo XS01 nell'equity): + data/portfolio_paper/ -> POOL REALE (nel ledger, muove l'equity/il conto) + data/portfolio_paper_stats/ -> PAPER-STATS (solo statistica, MAI nel ledger) + +Uso: + uv run python scripts/analysis/trades_status.py # ultime 24h + uv run python scripts/analysis/trades_status.py --since 2026-06-16T14:00 +""" +import argparse +import datetime as dt +import glob +import json +from pathlib import Path + +DATA = Path("data") +POOL_DIR = DATA / "portfolio_paper" +STATS_DIR = DATA / "portfolio_paper_stats" +LEDGER = DATA / "portfolios" / "PORT06" / "status.json" +EQUITY = DATA / "portfolios" / "PORT06" / "equity.jsonl" + + +def _closes(worker_dir: Path, since: dt.datetime): + """CLOSE events dei worker in worker_dir dal momento `since`.""" + out = [] + for f in sorted(glob.glob(str(worker_dir / "*" / "trades.jsonl"))): + name = Path(f).parent.name + for line in open(f): + try: + d = json.loads(line) + except json.JSONDecodeError: + continue + if d.get("event") != "CLOSE": + continue + try: + t = dt.datetime.fromisoformat(d.get("ts", "")) + except ValueError: + continue + if t < since: + continue + # PnL che conta: reale se eseguito (real_truth), altrimenti il sim/pnl + real = d.get("real_pnl") + real = real if real is not None else d.get("pnl", 0.0) + out.append({ + "ts": d.get("ts", "")[:19], "name": name, "reason": d.get("reason"), + "held": d.get("bars_held"), "pnl": d.get("pnl"), + "real": d.get("real_pnl"), "sim": d.get("sim_pnl"), "eff": real or 0.0, + }) + return sorted(out, key=lambda r: r["ts"]) + + +def _open_positions(worker_dir: Path): + out = [] + for f in sorted(glob.glob(str(worker_dir / "*" / "status.json"))): + d = json.load(open(f)) + if d.get("in_position"): + out.append({ + "name": Path(f).parent.name, "dir": d.get("direction"), + "entry": d.get("entry_price"), "held": d.get("bars_held"), + "real": d.get("real_in_position"), + }) + return out + + +def _equity_at(since: dt.datetime): + """Equity piu' vicino (e <=) a `since`, per il Δ della finestra.""" + base = None + if not EQUITY.exists(): + return None + for line in open(EQUITY): + try: + d = json.loads(line) + t = dt.datetime.fromisoformat(d["ts"]) + except (json.JSONDecodeError, KeyError, ValueError): + continue + if t <= since: + base = d["equity"] + else: + break + return base + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--since", default=None, + help="ISO (es. 2026-06-16T14:00). Default: 24h fa.") + args = ap.parse_args() + + now = dt.datetime.now(dt.timezone.utc) + if args.since: + since = dt.datetime.fromisoformat(args.since) + if since.tzinfo is None: + since = since.replace(tzinfo=dt.timezone.utc) + else: + since = now - dt.timedelta(hours=24) + + led = json.load(open(LEDGER)) + equity = led["equity"] + cap = led["total_capital"] + unreal = equity - cap + eq_base = _equity_at(since) + ver = Path("VERSION").read_text().strip() if Path("VERSION").exists() else "?" + + print(f"STATO TRADES — v{ver} — {now:%Y-%m-%d %H:%M} UTC (finestra dal {since:%Y-%m-%d %H:%M})\n") + + # ---- aggregato (ledger = verita') ---- + print("LEDGER PORT06 (verita' aggregata)") + print(f" equity = {equity:.2f}") + print(f" capitale realizz.= {cap:.2f}") + print(f" unrealized aperti= {unreal:+.2f} (equity - capitale; pos. aperte mark-to-market)") + print(f" peak {led.get('peak'):.2f} maxDD {led.get('max_dd')}%") + if eq_base is not None: + print(f" Δ equity finestra= {equity - eq_base:+.2f} ({eq_base:.2f} -> {equity:.2f})") + + # ---- POOL REALE ---- + pool = _closes(POOL_DIR, since) + s_real = sum(r["eff"] for r in pool) + wins = sum(1 for r in pool if r["eff"] > 0) + print(f"\n=== POOL REALE (muove l'equity) — {len(pool)} chiusure, {wins}W/{len(pool)-wins}L ===") + for r in pool: + tag = "LOSS" if r["eff"] < 0 else "win " + print(f" {r['ts']} {tag} {r['name']:36} {str(r['reason']):11} " + f"real={r['real']} sim={r['sim']}") + print(f" Σ REALIZZATO POOL = {s_real:+.2f}") + op = _open_positions(POOL_DIR) + if op: + print(f" posizioni aperte ({len(op)}):") + for p in op: + d = {1: "long", -1: "short"}.get(p["dir"], "?") + print(f" {p['name']:36} {d:5} entry={p['entry']} held={p['held']} " + f"real={'Y' if p['real'] else 'sim'}") + + # ---- PAPER-STATS ---- + stats = _closes(STATS_DIR, since) + s_stats = sum(r["eff"] for r in stats) + print(f"\n=== PAPER-STATS (solo statistica — NON tocca equity/conto) — {len(stats)} chiusure ===") + for r in stats: + print(f" {r['ts']} {r['name']:36} pnl={r['pnl']}") + print(f" Σ PAPER-STATS = {s_stats:+.2f} <- ignorare per l'equity") + + # ---- riconciliazione ---- + print("\nRICONCILIAZIONE") + print(f" realizzato POOL {s_real:+.2f} + Δunrealized aperti ≈ Δ equity finestra") + if eq_base is not None: + residuo = (equity - eq_base) - s_real + print(f" Δequity {equity - eq_base:+.2f} - realizzato {s_real:+.2f} = " + f"{residuo:+.2f} (= Δunrealized + ribilanci/timing)") + print(f" paper-stats {s_stats:+.2f} NON incluso (book in sola simulazione).") + + +if __name__ == "__main__": + main()