feat(analysis): trades_status.py — report stato trades con POOL reale vs PAPER-STATS separati
La dir e' la fonte di verita': portfolio_paper/ = pool (ledger/equity), portfolio_paper_stats/ = TR01/ROT02/TSM01/XS01 solo statistica. Niente piu' glob su portfolio_paper* (matchava entrambi -> +8.15 XS01 sim attribuiti per sbaglio all'equity il 2026-06-17). Mostra realizzato POOL + unrealized aperti e riconcilia con Δequity del ledger. Caveat + comando in CLAUDE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user