Files
Adriano Dal Pastro 14522262e6 chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera
libreria "validata OOS" era artefatto di feed contaminato (print fantasma del
feed Cerbero TESTNET + storico Binance/USDT).

- Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e
  CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia
  (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample
  (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE
  50-82% barre flat; XRP/BNB non certificabili).
- Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni
  portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST
  con segnale residuo, da ri-validare in isolamento.
- Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio,
  runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/
  portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/
  (preservati, non cancellati). Diario consolidato in un unico documento.
- Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal +
  src/backtest/engine + load_data; tool dati certificati (rebuild_history,
  certify_feed, audit_feed, multi_source_check).
- Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico
  (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:20:59 +00:00

235 lines
9.8 KiB
Python

"""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
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2])) # project root -> import src.*
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):
"""Posizioni aperte con i campi per il PnL non realizzato (entry REALE di
esecuzione quando eseguito; il feed di decisione sim e' dislocato)."""
out = []
for f in sorted(glob.glob(str(worker_dir / "*" / "status.json"))):
d = json.load(open(f))
if not d.get("in_position"):
continue
wid = Path(f).parent.name
is_pair = "entry_a" in d
executed = bool(d.get("real_in_position"))
row = {"name": wid, "pair": is_pair, "real": executed,
"dir": d.get("direction", 0), "held": d.get("bars_held"),
"max_bars": d.get("max_bars"), "tp": d.get("tp", 0.0),
"unreal": None}
if is_pair:
a_, b_ = wid.split("__")[1].split("_")
row.update({
"assets": [a_, b_], "leg_a": a_, "leg_b": b_,
"entry_a": d.get("real_entry_a") if executed else d.get("entry_a"),
"entry_b": d.get("real_entry_b") if executed else d.get("entry_b"),
"notional_a": d.get("real_notional_a") or 0.0,
"notional_b": d.get("real_notional_b") or 0.0,
})
else:
asset = wid.split("__")[1]
entry = (d.get("real_entry_price") if executed else None) or d.get("entry_price") or 0.0
row.update({
"assets": [asset], "asset": asset, "entry": entry,
"notional": d.get("real_entry_notional") or round(d.get("capital", 0.0) * 0.5 * 3, 1),
})
out.append(row)
return out
def _marks(assets: set) -> dict:
"""Mark correnti USDC perp (best-effort). {} se Cerbero non risponde."""
if not assets:
return {}
try:
from src.live.cerbero_client import CerberoClient
insts = [f"{a}_USDC-PERPETUAL" for a in assets]
data = CerberoClient().get_ticker_batch(insts)
return {t["instrument_name"].split("_")[0].replace("-PERPETUAL", ""): t.get("mark_price")
for t in data.get("tickers", [])}
except Exception as e:
print(f"[trades_status] mark fetch fallita ({e!r}) -> PnL aperti n/d", file=sys.stderr)
return {}
def _annotate_pnl(positions, marks):
"""PnL non realizzato live, convenzione identica alla dashboard (entry reale vs mark USDC)."""
for a in positions:
if a["pair"]:
ma, mb = marks.get(a["leg_a"]), marks.get(a["leg_b"])
if ma and mb and a.get("entry_a") and a.get("entry_b"):
s = 1 if a["dir"] > 0 else -1 # +1: long A / short B
ga = a["notional_a"] * s * (ma - a["entry_a"]) / a["entry_a"]
gb = a["notional_b"] * (-s) * (mb - a["entry_b"]) / a["entry_b"]
a["unreal"] = ga + gb
a["mark"], a["mark_b"] = ma, mb
else:
mk = marks.get(a["asset"])
if mk and a.get("entry"):
sign = 1 if a["dir"] > 0 else -1
pct = sign * (mk - a["entry"]) / a["entry"]
a["unreal"] = a["notional"] * pct
a["unreal_pct"] = pct * 100
a["mark"] = mk
return positions
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:
assets = set().union(*[set(p["assets"]) for p in op])
marks = _marks(assets)
_annotate_pnl(op, marks)
s_unreal = sum(p["unreal"] for p in op if p["unreal"] is not None)
n_marked = sum(1 for p in op if p["unreal"] is not None)
print(f"\n posizioni aperte ({len(op)}) — PnL non realizzato live (entry reale vs mark USDC):")
for p in sorted(op, key=lambda x: (x["unreal"] is None, x.get("unreal") or 0)):
d = {1: "LONG", -1: "SHORT"}.get(p["dir"], "?")
if p["unreal"] is not None:
pct = f"{p['unreal_pct']:+.2f}%" if not p["pair"] and "unreal_pct" in p else ""
mark = f"mark={p['mark']}" if not p["pair"] else f"mk {p['mark']}/{p.get('mark_b')}"
pnl = f"{p['unreal']:+.2f}"
else:
pct = mark = ""; pnl = "n/d (no mark)"
ent = f"entry={p['entry']}" if not p["pair"] else f"a={p.get('entry_a')} b={p.get('entry_b')}"
print(f" {p['name']:36} {d:5} h={p['held']}/{p['max_bars']} {ent} {mark} "
f"PnL {pnl} {pct}")
print(f" Σ UNREALIZED REALE = {s_unreal:+.2f} ({n_marked}/{len(op)} con mark live) "
f"<- verita' del conto (entry reale vs mark USDC)")
print(f" ledger unrealized = {unreal:+.2f} (feed sim-decisione testnet DISLOCATO); "
f"lo scarto {s_unreal - unreal:+.2f} e' la dislocazione del feed, non soldi.")
# ---- 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()