# -*- coding: utf-8 -*- """ 回测 JSONL 分析脚本. 用法: python analyze_backtest.py [path/to/file.jsonl] 对于没有 exit 记录的交易, 使用最后一条 snapshot 的 bid 作为 mark-to-market 离场价, reason 标记为 "open_no_exit". """ import json import sys from collections import defaultdict, Counter from datetime import datetime DEFAULT_PATH = r"c:\Users\Administrator\Desktop\polymarket-quick-trade\backtest_0725_0614.jsonl" def load_records(path): records = [] with open(path, "r", encoding="utf-8") as f: for line_no, line in enumerate(f, 1): line = line.strip() if not line: continue try: records.append(json.loads(line)) except json.JSONDecodeError as e: print(f"[WARN] line {line_no} JSON 解析失败: {e}") return records def group_trades(records): """按 round_ts 把 entry / snapshot / exit 聚合成一笔交易.""" trades = defaultdict(lambda: {"entry": None, "snapshots": [], "exit": None}) for r in records: t = r.get("type") key = r.get("round_ts") if t == "entry": # 同一 round_ts 若有多条 entry, 取第一条 if trades[key]["entry"] is None: trades[key]["entry"] = r else: # 后续 entry 视为新一笔 (用 ts 区分) - 简化处理: 覆盖 trades[key]["entry"] = r elif t == "snapshot": trades[key]["snapshots"].append(r) elif t == "exit": # 若已有 exit, 保留第一个; 否则写入 if trades[key]["exit"] is None: trades[key]["exit"] = r return trades def finalize_trade(key, trade): """补全 exit / entry_price / exit_price / pnl / pnl_pct / reason.""" entry = trade["entry"] snaps = sorted(trade["snapshots"], key=lambda x: x.get("ts", 0)) exit_rec = trade["exit"] info = { "round_ts": key, "entry": entry, "snapshots": snaps, "exit": exit_rec, "entry_price": None, "exit_price": None, "shares": None, "pnl": None, "pnl_pct": None, "reason": None, "has_exit": exit_rec is not None, "last_bid": snaps[-1].get("bid") if snaps else None, "last_btc": snaps[-1].get("btc") if snaps else None, } if entry is None: return info # 没有 entry, 无法分析 info["entry_price"] = entry.get("price") info["shares"] = entry.get("shares") if exit_rec is not None: info["exit_price"] = exit_rec.get("exit_price") info["pnl"] = exit_rec.get("pnl") info["pnl_pct"] = exit_rec.get("pnl_pct") info["reason"] = exit_rec.get("reason") else: # 用最后一条 snapshot 的 bid 模拟离场 (mark-to-market) last_bid = info["last_bid"] if last_bid is not None and info["entry_price"] is not None and info["shares"]: # 对于 side=down, bid 是 down 合约的买价; pnl = (bid - entry) * shares # 对于 side=up, 需要相应合约的 bid; 这里 snapshot.bid 直接对应 side 方向的 bid (按数据看是一致的) pnl = (last_bid - info["entry_price"]) * info["shares"] pnl_pct = (last_bid - info["entry_price"]) / info["entry_price"] * 100.0 if info["entry_price"] else None info["exit_price"] = last_bid info["pnl"] = pnl info["pnl_pct"] = pnl_pct info["reason"] = "open_no_exit" return info def fmt_ts(ts): if ts is None: return "-" try: return datetime.fromtimestamp(float(ts)).strftime("%Y-%m-%d %H:%M:%S") except Exception: return str(ts) def score_bucket(score): if score is None: return "unknown" if score <= -5: return "-8~-5" if score <= -3: return "-4~-3" if score <= 2: return "-2~2" if score <= 4: return "3~4" return "5~8" def parse_signal_direction(sig_text): """从信号文本中解析方向. 返回 'UP' / 'DN' / 'NEU' / None.""" if not sig_text: return None s = str(sig_text) if s.startswith("UP"): return "UP" if s.startswith("DN"): return "DN" if s.startswith("无") or s.startswith("NEU") or "无鲸鱼" in s: return "NEU" return None def signal_strength(sig_text): """尝试从 'DN -4 (...)' 中提取数值.""" if not sig_text: return None s = str(sig_text) parts = s.split() if len(parts) >= 2: try: return int(parts[1]) except ValueError: try: return float(parts[1]) except ValueError: return None return None def main(): path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_PATH print(f"=== 分析文件: {path} ===\n") records = load_records(path) type_counts = Counter(r.get("type", "?") for r in records) print(f"[原始记录] 总行数: {len(records)}; 类型分布: {dict(type_counts)}") trades_raw = group_trades(records) trades = [finalize_trade(k, v) for k, v in sorted(trades_raw.items())] n_with_entry = sum(1 for t in trades if t["entry"] is not None) n_with_exit = sum(1 for t in trades if t["has_exit"]) print(f"[交易聚合] 交易笔数(按 round_ts): {len(trades)}; 有 entry: {n_with_entry}; 有 exit: {n_with_exit}") print(f" 无 exit 的交易: {n_with_entry - n_with_exit} (用最后 snapshot bid 模拟离场)\n") # ===== 1. 总体统计 ===== print("=" * 70) print("1. 总体统计") print("=" * 70) closed = [t for t in trades if t["pnl"] is not None] wins = [t for t in closed if t["pnl"] > 0] losses = [t for t in closed if t["pnl"] < 0] ties = [t for t in closed if t["pnl"] == 0] total_pnl = sum(t["pnl"] for t in closed) total_pnl_pct_avg = sum(t["pnl_pct"] for t in closed) / len(closed) if closed else 0 win_rate = len(wins) / len(closed) * 100 if closed else 0 print(f" 可统计交易数 (有 pnl): {len(closed)}") print(f" 胜: {len(wins)} 负: {len(losses)} 平: {len(ties)}") print(f" 总 PnL: {total_pnl:+.4f}") print(f" 平均 PnL%: {total_pnl_pct_avg:+.2f}%") print(f" 胜率: {win_rate:.2f}%") if wins: print(f" 平均盈利: {sum(t['pnl'] for t in wins)/len(wins):+.4f}") if losses: print(f" 平均亏损: {sum(t['pnl'] for t in losses)/len(losses):+.4f}") # ===== 2. 按 side 分组 ===== print("\n" + "=" * 70) print("2. 按 side 分组胜率") print("=" * 70) by_side = defaultdict(list) for t in closed: by_side[t["entry"].get("side", "?")].append(t) for side, ts in by_side.items(): w = sum(1 for t in ts if t["pnl"] > 0) l = sum(1 for t in ts if t["pnl"] < 0) e = sum(1 for t in ts if t["pnl"] == 0) pnl_sum = sum(t["pnl"] for t in ts) wr = w / len(ts) * 100 if ts else 0 print(f" side={side:5s} 笔数={len(ts):3d} 胜={w} 负={l} 平={e} 胜率={wr:5.2f}% 总PnL={pnl_sum:+.4f}") # ===== 3. 按 score 分组 ===== print("\n" + "=" * 70) print("3. 按 score 分组胜率") print("=" * 70) by_score = defaultdict(list) for t in closed: sc = t["entry"].get("score") by_score[score_bucket(sc)].append(t) order = ["-8~-5", "-4~-3", "-2~2", "3~4", "5~8", "unknown"] for bucket in order: ts = by_score.get(bucket, []) if not ts: continue w = sum(1 for t in ts if t["pnl"] > 0) l = sum(1 for t in ts if t["pnl"] < 0) e = sum(1 for t in ts if t["pnl"] == 0) pnl_sum = sum(t["pnl"] for t in ts) avg_pnl_pct = sum(t["pnl_pct"] for t in ts) / len(ts) wr = w / len(ts) * 100 if ts else 0 print(f" score {bucket:8s} 笔数={len(ts):3d} 胜={w} 负={l} 平={e} 胜率={wr:5.2f}% 总PnL={pnl_sum:+.4f} 平均PnL%={avg_pnl_pct:+.2f}%") # ===== 4. 按 reason 分组 ===== print("\n" + "=" * 70) print("4. 按 reason 分组") print("=" * 70) by_reason = defaultdict(list) for t in closed: by_reason[t["reason"] or "unknown"].append(t) for reason, ts in by_reason.items(): pnls = [t["pnl"] for t in ts] avg_pnl = sum(pnls) / len(pnls) if pnls else 0 avg_pnl_pct = sum(t["pnl_pct"] for t in ts) / len(ts) if ts else 0 w = sum(1 for p in pnls if p > 0) print(f" reason={reason:16s} 次数={len(ts):3d} 胜={w} 平均PnL={avg_pnl:+.4f} 平均PnL%={avg_pnl_pct:+.2f}%") # ===== 5. BTC vs strike 偏离 & 动量一致性 ===== print("\n" + "=" * 70) print("5. BTC vs strike 偏离 (入场时方向是否与 BTC 动量一致)") print("=" * 70) print(" 说明: btc_diff = btc - strike. side=up 期望 btc>strike (上涨); side=down 期望 btcstrike) 或 (side=down 且 btc 0 elif side == "down": agree = diff < 0 else: agree = None flag = "一致" if agree else "不一致" if agree: consistent += 1 else: inconsistent += 1 print(f" round_ts={e.get('round_ts')} side={side} btc={btc:.2f} strike={strike:.2f} " f"diff={diff:+.2f} ({diff_pct:+.4f}%) 入场方向与BTC动量: {flag}") total_checked = consistent + inconsistent if total_checked: print(f"\n 汇总: 一致 {consistent} / 不一致 {inconsistent} / 总 {total_checked} " f"一致率={consistent/total_checked*100:.2f}%") # ===== 6. 信号方向 vs 最终结果一致性 ===== print("\n" + "=" * 70) print("6. 每个信号方向 vs 最终 pnl 正负 的一致性") print("=" * 70) print(" 对每个信号: 解析其方向(UP/DN/NEU), 与 entry.side 对比是否同向,") print(" 再看该笔 pnl 正负. '信号对结果有指导性' = 信号方向 == entry.side 且 pnl>0, 或 信号方向 != entry.side 且 pnl<0.\n") signal_names = ["winners", "streak", "whale", "momentum"] for sig in signal_names: agree_side = 0 # 信号方向 == entry.side disagree_side = 0 # 信号方向 != entry.side # 信号方向 == side 且盈利 / 信号方向 != side 且亏损 => 信号"对" sig_correct = 0 sig_wrong = 0 sig_neu = 0 sig_valid = 0 for t in closed: e = t["entry"] sig_text = e.get("signals", {}).get(sig) sig_dir = parse_signal_direction(sig_text) side = e.get("side") side_norm = "UP" if side == "up" else ("DN" if side == "down" else None) if sig_dir is None or sig_dir == "NEU": sig_neu += 1 continue sig_valid += 1 same_dir = (sig_dir == side_norm) if same_dir: agree_side += 1 else: disagree_side += 1 pnl_pos = t["pnl"] > 0 # 信号"对": 同向且盈利 OR 反向且亏损 if (same_dir and pnl_pos) or (not same_dir and not pnl_pos): sig_correct += 1 else: sig_wrong += 1 total = sig_correct + sig_wrong acc = sig_correct / total * 100 if total else 0 print(f" 信号 {sig:10s}: 有效={sig_valid:3d} 中性={sig_neu:3d} | " f"同向={agree_side:3d} 反向={disagree_side:3d} | " f"对结果预测准确={sig_correct}/{total} = {acc:5.2f}%") # ===== 7. 每笔交易简表 ===== print("\n" + "=" * 70) print("7. 每笔交易简表") print("=" * 70) header = f"{'round_ts':<20}{'时间':<20}{'side':<6}{'score':<6}{'entry':<8}{'exit':<8}{'pnl':<10}{'pnl%':<9}{'reason':<16}{'BTC偏离':<14}{'signals简要'}" print(header) print("-" * len(header)) for t in trades: e = t["entry"] if e is None: print(f" round_ts={t['round_ts']} (无 entry)") continue side = e.get("side", "?") score = e.get("score", "?") ep = t["entry_price"] xp = t["exit_price"] pnl = t["pnl"] pnl_pct = t["pnl_pct"] reason = t["reason"] or "-" btc = e.get("btc") strike = e.get("strike") if btc is not None and strike is not None: diff = btc - strike btc_dir = "BTC>strike" if diff > 0 else ("BTC0) or (side=="down" and diff<0) print(f" - BTC={btc:.2f} vs strike={strike:.2f}, 偏离 {diff:+.2f}; side={side}, 方向与 BTC 动量 {'一致' if agree else '不一致'}.") # 对 down 方向, BTC 实际高于 strike => 入场时 BTC 已经反向, 不利 if side == "down" and diff > 0: print(f" - ⚠ side=down 但 BTC>strike ({diff:+.2f}), 即 BTC 已上涨越过 strike, 与下注方向相反, 风险信号.") # signals 摘要 sigs = e.get("signals", {}) print(f" - 信号汇总:") for sn in signal_names: print(f" {sn}: {sigs.get(sn)}") # 时间序列观察 snaps = t["snapshots"] if snaps: bids = [s.get("bid") for s in snaps] print(f" - bid 路径: 起 {bids[0]} -> 最低 {min(bids)} -> 最高 {max(bids)} -> 末 {bids[-1]}") print(f" - bid 在持仓期间整体 {'下行' if bids[-1] < bids[0] else '上行'}, " f"对 side={side} 持仓 {'不利' if (side=='down' and bids[-1]bids[0]) else '有利'}.") print("\n(分析完成)") if __name__ == "__main__": main()