Files
2026-07-26 01:51:18 +08:00

146 lines
6.0 KiB
Python

"""Analyze backtest_*.jsonl — print key stats and per-trade attribution."""
import json
import sys
import os
from collections import defaultdict
# Fix Windows console encoding
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
def load_jsonl(path):
entries, exits, snapshots = [], [], []
with open(path, encoding="utf-8") as f:
for line in f:
try:
e = json.loads(line)
except Exception:
continue
t = e.get("type")
if t == "entry":
entries.append(e)
elif t == "exit":
exits.append(e)
elif t == "snapshot":
snapshots.append(e)
return entries, exits, snapshots
def stats(entries, exits):
"""Print overall + segment stats"""
print(f"\n{'='*70}")
print(f"OVERALL: {len(entries)} entries, {len(exits)} exits")
print(f"{'='*70}")
wins = [x for x in exits if x["reason"] == "take_profit"]
losses = [x for x in exits if x["reason"] == "stop_loss"]
other = [x for x in exits if x["reason"] not in ("take_profit", "stop_loss")]
total_pnl = sum(x["pnl"] for x in exits)
win_pnl = sum(x["pnl"] for x in wins)
loss_pnl = sum(x["pnl"] for x in losses)
win_pct = len(wins) / len(exits) * 100 if exits else 0
print(f"Wins: {len(wins)} Losses: {len(losses)} Other: {len(other)}")
print(f"Win rate: {win_pct:.1f}%")
print(f"Total PnL: ${total_pnl:+.2f}")
print(f"Avg win: ${win_pnl/len(wins):+.3f}" if wins else "")
print(f"Avg loss: ${loss_pnl/len(losses):+.3f}" if losses else "")
print(f"Avg hold (win): {sum(x['hold_seconds'] for x in wins)/len(wins):.1f}s" if wins else "")
print(f"Avg hold (loss): {sum(x['hold_seconds'] for x in losses)/len(losses):.1f}s" if losses else "")
# Pair entries with exits by round_ts
by_round = defaultdict(dict)
for e in entries:
by_round[e["round_ts"]]["entry"] = e
for x in exits:
# round_ts lives in state (from capture_market_state at exit)
rt = x.get("round_ts") or x.get("state", {}).get("round_ts")
if rt:
by_round[rt]["exit"] = x
pairs = [v for v in by_round.values() if "entry" in v and "exit" in v]
print(f"\n--- PER TRADE ({len(pairs)} round trips) ---")
print(f"{'#':>3} {'side':>5} {'conc':>5} {'streak':>9} {'winn':>6} {'price':>5} {'reason':>10} {'pnl':>7} {'hold':>5}")
for i, p in enumerate(pairs, 1):
e = p["entry"]
x = p["exit"]
s = e["state"]
streak = f"{e['streak_up']}/{e['streak_dn']}"
print(f"{i:3d} {e['side'].upper():>5} "
f"{e['streak_concentration']:+.2f} "
f"{streak:>9} "
f"{s['winners_up_n']}/{s['winners_dn_n']:<4} "
f"{e['price']:.2f} "
f"{x['reason']:>10} "
f"${x['pnl']:+.2f} "
f"{x['hold_seconds']:>4.0f}s")
# Segment analysis by streak concentration
print(f"\n--- BY STREAK CONCENTRATION ---")
buckets = [(-1.0, -0.7), (-0.7, -0.4), (-0.4, 0.4), (0.4, 0.7), (0.7, 1.01)]
for lo, hi in buckets:
seg = [p for p in pairs
if lo <= p["entry"]["streak_concentration"] < hi]
if not seg:
continue
seg_wins = sum(1 for p in seg if p["exit"]["reason"] == "take_profit")
seg_pnl = sum(p["exit"]["pnl"] for p in seg)
avg_pnl = seg_pnl / len(seg)
print(f" [{lo:+.1f}, {hi:+.1f}) n={len(seg):>3} wins={seg_wins:>3} ({seg_wins/len(seg)*100:.0f}%) PnL=${seg_pnl:+.2f} avg=${avg_pnl:+.3f}")
# Segment by entry price (token price tier)
print(f"\n--- BY ENTRY PRICE ---")
p_buckets = [(0, 0.4), (0.4, 0.55), (0.55, 0.7), (0.7, 0.85), (0.85, 1.01)]
for lo, hi in p_buckets:
seg = [p for p in pairs if lo <= p["entry"]["price"] < hi]
if not seg:
continue
seg_wins = sum(1 for p in seg if p["exit"]["reason"] == "take_profit")
seg_pnl = sum(p["exit"]["pnl"] for p in seg)
print(f" [{lo:.2f}, {hi:.2f}) n={len(seg):>3} wins={seg_wins:>3} ({seg_wins/len(seg)*100:.0f}%) PnL=${seg_pnl:+.2f}")
# Segment by hold time
print(f"\n--- BY HOLD SECONDS ---")
h_buckets = [(0, 30), (30, 60), (60, 120), (120, 300)]
for lo, hi in h_buckets:
seg = [p for p in pairs if lo <= p["exit"]["hold_seconds"] < hi]
if not seg:
continue
seg_wins = sum(1 for p in seg if p["exit"]["reason"] == "take_profit")
seg_pnl = sum(p["exit"]["pnl"] for p in seg)
print(f" [{lo:>3}s, {hi:>3}s) n={len(seg):>3} wins={seg_wins:>3} ({seg_wins/len(seg)*100:.0f}%) PnL=${seg_pnl:+.2f}")
# Per-feature breakdown: book imbalance, BTC velocity at entry
print(f"\n--- BY BOOK IMBALANCE (positive = UP buy pressure) ---")
b_buckets = [(-9999, -500), (-500, -100), (-100, 100), (100, 500), (500, 9999)]
for lo, hi in b_buckets:
seg = [p for p in pairs if lo <= p["entry"]["state"]["book_imbalance"] < hi]
if not seg:
continue
seg_wins = sum(1 for p in seg if p["exit"]["reason"] == "take_profit")
seg_pnl = sum(p["exit"]["pnl"] for p in seg)
print(f" [{lo:>5}, {hi:>5}) n={len(seg):>3} wins={seg_wins:>3} ({seg_wins/len(seg)*100:.0f}%) PnL=${seg_pnl:+.2f}")
# BTC deviation at entry
print(f"\n--- BY BTC DEV AT ENTRY ($) ---")
d_buckets = [(-999, -10), (-10, -3), (-3, 3), (3, 10), (10, 999)]
for lo, hi in d_buckets:
seg = [p for p in pairs if lo <= p["entry"]["state"]["btc_dev"] < hi]
if not seg:
continue
seg_wins = sum(1 for p in seg if p["exit"]["reason"] == "take_profit")
seg_pnl = sum(p["exit"]["pnl"] for p in seg)
print(f" [{lo:>4}, {hi:>4}) n={len(seg):>3} wins={seg_wins:>3} ({seg_wins/len(seg)*100:.0f}%) PnL=${seg_pnl:+.2f}")
if __name__ == "__main__":
path = sys.argv[1] if len(sys.argv) > 1 else "backtest_0725_2344.jsonl"
if not os.path.exists(path):
print(f"File not found: {path}")
sys.exit(1)
entries, exits, snapshots = load_jsonl(path)
stats(entries, exits)