Files
PythagorasGoal/Old/scripts/analysis/ledger_vs_backtest.py
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

174 lines
7.8 KiB
Python

"""Report ricorrente LEDGER REALE vs BACKTEST — il gate per scalare il capitale.
Per gli sleeve ESEGUITI (6 fade 15m, DIP01, 6 pairs, SH01) il sim del worker ==
backtest canonico PER COSTRUZIONE (validato: validate_worker_pairs, parity test).
Quindi "reale vs backtest" = "reale vs sim" = la **fuga di esecuzione**: slippage
sugli ingressi/uscite + fee reali vs assunte + effetti netting/phantom/sim_fallback.
È il numero che dice se l'edge SOPRAVVIVE ai fill veri — la condizione per passare
da testnet/piccolo a capitale serio.
Cosa misura (finestra mobile, default 7g) leggendo SOLO i trades.jsonl + status.json
(nessuna rete → affidabile in cron):
- PnL realizzato sim vs reale (Σ e per-trade) -> LEAKAGE € e % (la bottom line)
- slippage ingressi (REAL_OPEN.slippage_bps) e uscite (REAL_CLOSE.slippage_bps)
- fee reali vs assunte (0.10% RT)
- trade sim_fallback (reale mai eseguito/fillato) = quota NON coperta dal reale
- ledger per-sleeve: real_capital vs capital (sim)
Verdetto: leakage per-trade piccolo e stabile -> verde (si puo' pensare a scalare).
uv run python scripts/analysis/ledger_vs_backtest.py # stampa
uv run python scripts/analysis/ledger_vs_backtest.py --days 14 # finestra 14g
uv run python scripts/analysis/ledger_vs_backtest.py --telegram # + invio Telegram
"""
from __future__ import annotations
import json
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from statistics import median
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
PAPER = PROJECT_ROOT / "data" / "portfolio_paper"
ASSUMED_FEE_RT = 0.001 # 0.10% RT assunto dal backtest
GREEN_BPS, YELLOW_BPS = 15.0, 40.0 # soglie slippage medio per-lato (verdetto)
def _parse_ts(s: str) -> datetime | None:
try:
t = datetime.fromisoformat(s.replace("Z", "+00:00"))
return t if t.tzinfo else t.replace(tzinfo=timezone.utc)
except Exception:
return None
def collect(days: int, since: datetime | None = None) -> dict:
# since (clean-start) ha priorita' sulla finestra mobile: lo scheduler parte
# dal 2026-06-13 (post-fix TP_PHANTOM/netting/ribilancio) cosi' accumula SOLO
# dati puliti; una finestra mobile pura includerebbe l'incidente testnet pre-fix
# (sim +82 vs reale +5: +4% fantasma che il sim bookava e il reale no).
cut = since or (datetime.now(timezone.utc) - timedelta(days=days))
entries, exits, closes = [], [], []
for f in sorted(PAPER.glob("*/trades.jsonl")):
wid = f.parent.name
for line in f.read_text().splitlines():
try:
e = json.loads(line)
except Exception:
continue
tt = _parse_ts(e.get("ts", ""))
if tt is None or tt < cut:
continue
ev = e.get("event")
if ev == "REAL_OPEN":
entries.append((wid, e))
elif ev in ("REAL_CLOSE", "REAL_CLOSE_PAIR"):
exits.append((wid, e))
elif ev == "CLOSE":
closes.append((wid, e))
return {"entries": entries, "exits": exits, "closes": closes}
def _stats(vals: list[float]) -> dict:
if not vals:
return {"n": 0, "mean": 0.0, "med": 0.0, "p90": 0.0, "max": 0.0}
s = sorted(vals)
return {"n": len(s), "mean": sum(s) / len(s), "med": median(s),
"p90": s[min(len(s) - 1, int(0.9 * len(s)))], "max": s[-1]}
def analyze(days: int, since: datetime | None = None) -> dict:
d = collect(days, since)
# slippage ingressi/uscite (bps); fee reali ingresso
open_slip = [abs(e.get("slippage_bps") or 0.0) for _, e in d["entries"]]
exit_slip = [abs(e.get("slippage_bps") or 0.0) for _, e in d["exits"]
if e.get("real_fill") is not None] # null = uscita da TP resting (no slip)
open_fee = [e.get("fee_usd") or 0.0 for _, e in d["entries"]]
# PnL realizzato sim vs reale dai CLOSE (la verita' contabile guidata da real_truth)
real_closes = [e for _, e in d["closes"] if e.get("pnl_source") == "real"]
fallback = [e for _, e in d["closes"] if e.get("pnl_source") == "sim_fallback"]
sim_sum = sum(e.get("sim_pnl") or 0.0 for e in real_closes)
real_sum = sum(e.get("real_pnl") or 0.0 for e in real_closes)
per_trade_gap = [(e.get("sim_pnl") or 0.0) - (e.get("real_pnl") or 0.0) for e in real_closes]
# ledger per-sleeve: real vs sim (solo worker eseguiti = con REAL_OPEN nella storia)
executed = {wid for wid, _ in d["entries"]}
ledger = []
for wid in sorted(executed):
sp = PAPER / wid / "status.json"
if not sp.exists():
continue
st = json.loads(sp.read_text())
cap, rc = st.get("capital"), st.get("real_capital")
if cap is not None and rc is not None:
ledger.append((wid, cap, rc, rc - cap))
return {"days": days, "since": since,
"open_slip": _stats(open_slip), "exit_slip": _stats(exit_slip),
"open_fee_mean": (sum(open_fee) / len(open_fee)) if open_fee else 0.0,
"n_real": len(real_closes), "n_fallback": len(fallback),
"sim_sum": sim_sum, "real_sum": real_sum,
"leak_total": sim_sum - real_sum,
"leak_per_trade": (sum(per_trade_gap) / len(per_trade_gap)) if per_trade_gap else 0.0,
"ledger": ledger}
def verdict(a: dict) -> tuple[str, str]:
avg_slip = (a["open_slip"]["mean"] + a["exit_slip"]["mean"]) / 2
if a["n_real"] < 10:
return "🟡", f"campione PICCOLO ({a['n_real']} trade reali): non concludere ancora"
if avg_slip <= GREEN_BPS and abs(a["leak_per_trade"]) < 0.30:
return "🟢", "leakage basso e stabile: reale ~ backtest"
if avg_slip <= YELLOW_BPS:
return "🟡", "leakage moderato: tenere d'occhio prima di scalare"
return "🔴", "leakage ALTO: l'edge si erode sull'esecuzione, NON scalare"
def render(a: dict) -> str:
flag, msg = verdict(a)
win = f"da {a['since']:%Y-%m-%d}" if a.get("since") else f"finestra {a['days']}g"
L = [f"LEDGER REALE vs BACKTEST — {win} {flag}",
f" {msg}",
f" trade reali: {a['n_real']} | sim_fallback (reale mai fillato): {a['n_fallback']}",
f" PnL realizzato: sim {a['sim_sum']:+.2f} reale {a['real_sum']:+.2f} "
f"-> LEAKAGE {a['leak_total']:+.2f} ({a['leak_per_trade']:+.3f}/trade)",
f" slippage ingressi (bps): media {a['open_slip']['mean']:.1f} "
f"med {a['open_slip']['med']:.1f} p90 {a['open_slip']['p90']:.1f} max {a['open_slip']['max']:.1f} "
f"(n={a['open_slip']['n']})",
f" slippage uscite (bps): media {a['exit_slip']['mean']:.1f} "
f"med {a['exit_slip']['med']:.1f} max {a['exit_slip']['max']:.1f} (n={a['exit_slip']['n']}; "
f"escluse uscite da TP resting)",
f" fee reale media ingresso: ${a['open_fee_mean']:.4f}"]
if a["ledger"]:
L.append(" ledger per-sleeve (sim -> reale, Δ):")
for wid, cap, rc, dlt in a["ledger"]:
short = wid.replace("_reversion", "").replace("_fade", "").replace("_bollinger", "")[:30]
L.append(f" {short:30} {cap:7.2f} -> {rc:7.2f} {dlt:+.2f}")
return "\n".join(L)
def main() -> int:
days = 7
since = None
if "--days" in sys.argv:
days = int(sys.argv[sys.argv.index("--days") + 1])
if "--since" in sys.argv:
since = _parse_ts(sys.argv[sys.argv.index("--since") + 1] + "T00:00:00+00:00")
a = analyze(days, since)
text = render(a)
print(text)
if "--telegram" in sys.argv:
from src.live.telegram_notifier import send_telegram
from src.version import APP_VERSION
send_telegram(f"📒 <b>LEDGER vs BACKTEST</b> <code>v{APP_VERSION}</code>\n<pre>{text}</pre>")
print("[telegram] report inviato")
flag, _ = verdict(a)
return 0 if flag == "🟢" else 1
if __name__ == "__main__":
sys.exit(main())