Files
my_polymarket_m5_dasboad/analyze_backtest.py
T

406 lines
16 KiB
Python
Raw Normal View History

2026-07-26 01:51:18 +08:00
# -*- 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 期望 btc<strike (下跌).")
print(" 方向一致 = (side=up 且 btc>strike) 或 (side=down 且 btc<strike)\n")
consistent = 0
inconsistent = 0
for t in trades:
e = t["entry"]
if e is None:
continue
btc = e.get("btc")
strike = e.get("strike")
side = e.get("side")
if btc is None or strike is None:
continue
diff = btc - strike
diff_pct = diff / strike * 100 if strike else 0
if side == "up":
agree = diff > 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 ("BTC<strike" if diff < 0 else "BTC=strike")
btc_str = f"{btc_dir}({diff:+.1f})"
else:
btc_str = "-"
# signals 简要
sigs = e.get("signals", {})
sig_parts = []
for sn in signal_names:
sv = sigs.get(sn)
d = parse_signal_direction(sv)
v = signal_strength(sv)
if d and d != "NEU":
sig_parts.append(f"{sn[:3]}:{d}({v:+d})" if v is not None else f"{sn[:3]}:{d}")
elif d == "NEU":
sig_parts.append(f"{sn[:3]}:NEU")
sig_brief = " ".join(sig_parts)
ep_s = f"{ep:.2f}" if ep is not None else "-"
xp_s = f"{xp:.2f}" if xp is not None else "-"
pnl_s = f"{pnl:+.4f}" if pnl is not None else "-"
pnlpct_s = f"{pnl_pct:+.2f}%" if pnl_pct is not None else "-"
ts_s = fmt_ts(e.get("round_ts"))
print(f"{str(e.get('round_ts')):<20}{ts_s:<20}{side:<6}{str(score):<6}{ep_s:<8}{xp_s:<8}{pnl_s:<10}{pnlpct_s:<9}{reason:<16}{btc_str:<14}{sig_brief}")
# ===== 洞察 =====
print("\n" + "=" * 70)
print("洞察 (Insights)")
print("=" * 70)
if not closed:
print(" - 没有已平仓交易 (exit 记录), 所有 pnl 均为基于最后 snapshot bid 的估算.")
if len(trades) == 1:
t = trades[0]
e = t["entry"]
print(f" - 本文件仅含 1 笔交易 (round_ts={e.get('round_ts')}, side={e.get('side')}, score={e.get('score')}).")
print(f" - 入场价 {t['entry_price']}, 最后 bid {t['last_bid']}, 估算 pnl {t['pnl']:+.4f} ({t['pnl_pct']:+.2f}%).")
btc = e.get("btc"); strike = e.get("strike"); side = e.get("side")
diff = btc - strike
agree = (side=="up" and diff>0) 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]) or (side=='up' and bids[-1]>bids[0]) else '有利'}.")
print("\n(分析完成)")
if __name__ == "__main__":
main()