""" 回测分析脚本 用法: cd backtest-data && python3 analyze.py 功能: 1. 建立 diff → 合理概率 映射(按 rem 分段) 2. 计算当前概率与合理概率的偏差 3. 找出最优入场偏差阈值 4. 模拟策略1(常规加强)的历史表现 """ import json import glob import sys from collections import defaultdict # ── 1. 加载数据 ────────────────────────────────────────────── ticks = [] for f in sorted(glob.glob("*.jsonl")) + sorted(glob.glob("ticks-*.jsonl")): for line in open(f): try: r = json.loads(line) if r.get("type") == "tick": ticks.append(r) elif "diff" in r and "upPct" in r and "type" not in r: # 兼容旧格式(无 type 字段) ticks.append(r) except: pass if not ticks: print("没有找到 tick 数据") sys.exit(1) windows = sorted(set(t["windowStart"] for t in ticks)) print(f"总 tick 数: {len(ticks)}") print(f"覆盖窗口数: {len(windows)}") print(f"时间范围: {ticks[0]['ts']} ~ {ticks[-1]['ts']}") print() # ── 2. 建立 diff → 合理概率 映射 ───────────────────────────── # 按 (diff 桶, rem 段) 分组 DIFF_BUCKET = 5 # 每5一档 REM_BINS = [(0, 30), (30, 60), (60, 120), (120, 180), (180, 300)] def diff_bucket(d): return round(d / DIFF_BUCKET) * DIFF_BUCKET def rem_bin(r): for lo, hi in REM_BINS: if lo <= r < hi: return f"{lo}-{hi}" return "300+" mapping = defaultdict(list) # (diff_bucket, rem_bin) -> [upPct, ...] for t in ticks: db = diff_bucket(t["diff"]) rb = rem_bin(t["rem"]) mapping[(db, rb)].append(t["upPct"]) print("=" * 70) print("diff → 合理概率 映射(中位数,按 rem 分段)") print("=" * 70) print(f"{'diff':>6} ", end="") for lo, hi in REM_BINS: print(f" {lo}-{hi}s", end="") print(f" {'样本':>6}") print("-" * 70) all_buckets = sorted(set(db for db, _ in mapping)) for db in all_buckets: total = 0 row = f"{db:+6d} " for lo, hi in REM_BINS: rb = f"{lo}-{hi}" vals = mapping.get((db, rb), []) total += len(vals) if vals: vals_sorted = sorted(vals) median = vals_sorted[len(vals_sorted) // 2] row += f" {median:5d}%" else: row += f" —" row += f" {total:6d}" if total >= 5: # 只显示有足够样本的 print(row) # ── 3. 计算偏差分布 ────────────────────────────────────────── print() print("=" * 70) print("偏差分析:实际概率 vs 合理概率(中位数)") print("=" * 70) biases = [] for t in ticks: db = diff_bucket(t["diff"]) rb = rem_bin(t["rem"]) vals = mapping.get((db, rb), []) if len(vals) < 10: continue vals_sorted = sorted(vals) fair_prob = vals_sorted[len(vals_sorted) // 2] bias = fair_prob - t["upPct"] biases.append({ "bias": bias, "diff": t["diff"], "upPct": t["upPct"], "fair": fair_prob, "rem": t["rem"], "windowStart": t["windowStart"], "ts": t["ts"], }) if biases: abs_biases = [abs(b["bias"]) for b in biases] print(f"有效样本数: {len(biases)}") print(f"偏差均值: {sum(b['bias'] for b in biases) / len(biases):.1f}%") print(f"偏差绝对值均值: {sum(abs_biases) / len(abs_biases):.1f}%") print(f"偏差绝对值中位数: {sorted(abs_biases)[len(abs_biases)//2]:.1f}%") print() # 偏差分布 print("偏差分布:") from collections import Counter bias_bins = Counter() for b in biases: bb = round(b["bias"] / 2) * 2 bias_bins[bb] += 1 for bb in sorted(bias_bins): pct = bias_bins[bb] / len(biases) * 100 bar = "█" * int(pct) print(f" {bb:+4d}%: {bias_bins[bb]:5d} ({pct:4.1f}%) {bar}") # ── 4. 入场机会分析 ────────────────────────────────────────── print() print("=" * 70) print("入场机会分析:偏差超过阈值时概率后续走势") print("=" * 70) # 按窗口分组 window_ticks = defaultdict(list) for t in ticks: window_ticks[t["windowStart"]].append(t) for threshold in [5, 8, 10, 12, 15]: entries = [] for b in biases: if abs(b["bias"]) >= threshold and 50 <= b["rem"] <= 200: entries.append(b) if not entries: print(f"\n偏差阈值 {threshold}%: 无入场机会") continue # 看入场后概率是否回归 convergences = [] for entry in entries: ws = entry["windowStart"] wticks = window_ticks.get(ws, []) # 找入场后10秒、30秒的概率变化 entry_ts = entry["ts"] for dt_label, dt_ms in [("10s", 10000), ("30s", 30000)]: future = [t for t in wticks if 0 < t["ts"] - entry_ts <= dt_ms] if future: future_prob = future[-1]["upPct"] prob_change = future_prob - entry["upPct"] # 如果偏差为正(市场低估涨),概率应该涨 expected_dir = 1 if entry["bias"] > 0 else -1 correct = (prob_change * expected_dir) > 0 convergences.append({ "dt": dt_label, "change": prob_change, "correct": correct, "bias_dir": "低估" if entry["bias"] > 0 else "高估", }) if convergences: for dt_label in ["10s", "30s"]: dt_items = [c for c in convergences if c["dt"] == dt_label] if dt_items: correct_count = sum(1 for c in dt_items if c["correct"]) avg_change = sum(abs(c["change"]) for c in dt_items) / len(dt_items) print(f"\n偏差阈值 {threshold}%: {len(entries)} 次入场机会") print(f" {dt_label}后回归率: {correct_count}/{len(dt_items)} ({correct_count/len(dt_items)*100:.0f}%)") print(f" {dt_label}后平均概率变化: {avg_change:.1f}%") # ── 5. 策略1模拟回测 ───────────────────────────────────────── print() print("=" * 70) print("策略1(常规加强)模拟回测") print("=" * 70) ENTRY_DIFF = 35 ENTRY_PROB_CAP = 80 WINDOW_MAX_REM = 210 WINDOW_MIN_REM = 50 TRAILING_STOP_RETRACEMENT = 20 TRAILING_STOP_MIN_DIFF = 5 PROB_PEAK_MIN = 85 PROB_PEAK_RETRACEMENT = 8 FORCE_EXIT_REM = 10 trades = [] for ws in windows: wticks = sorted(window_ticks[ws], key=lambda t: t["ts"]) if len(wticks) < 10: continue last_diff = None holding = False direction = None entry_price = None peak_diff = None peak_prob = None for t in wticks: diff = t["diff"] upPct = t["upPct"] rem = t["rem"] if not holding: # 检查入场 if rem <= WINDOW_MAX_REM and rem > WINDOW_MIN_REM and last_diff is not None: # 买涨穿越 if last_diff <= ENTRY_DIFF and diff > ENTRY_DIFF and upPct < ENTRY_PROB_CAP: holding = True direction = "up" entry_price = upPct / 100 # 简化:用概率作为买入价 peak_diff = diff peak_prob = upPct # 买跌穿越 elif last_diff >= -ENTRY_DIFF and diff < -ENTRY_DIFF and (100 - upPct) < ENTRY_PROB_CAP: holding = True direction = "down" entry_price = (100 - upPct) / 100 peak_diff = -diff peak_prob = 100 - upPct else: my_pct = upPct if direction == "up" else (100 - upPct) fav_diff = diff if direction == "up" else -diff # 更新峰值 if fav_diff > peak_diff: peak_diff = fav_diff if my_pct > peak_prob: peak_prob = my_pct exit_reason = None exit_signal = None # 强制平仓 if rem <= FORCE_EXIT_REM and rem > 0: exit_signal = "tp" if my_pct >= 70 else "sl" exit_reason = f"强制平仓 rem={rem}" # 阶梯止盈 if not exit_reason and rem >= FORCE_EXIT_REM: span = WINDOW_MAX_REM - FORCE_EXIT_REM elapsed = max(0, WINDOW_MAX_REM - rem) tp_thr = 90 + int(elapsed / span * 10) tp_capped = min(tp_thr, 100) if my_pct >= tp_capped: exit_signal = "tp" exit_reason = f"阶梯止盈 {my_pct}%>={tp_capped}%" # 回撤止盈 if not exit_reason and peak_prob >= PROB_PEAK_MIN and my_pct <= peak_prob - PROB_PEAK_RETRACEMENT: exit_signal = "tp" exit_reason = f"回撤止盈 {my_pct}% 峰{peak_prob}%" # 兜底止损 if not exit_reason: if direction == "up" and diff <= TRAILING_STOP_MIN_DIFF: exit_signal = "sl" exit_reason = f"兜底止损 diff={diff:.0f}" elif direction == "down" and diff >= -TRAILING_STOP_MIN_DIFF: exit_signal = "sl" exit_reason = f"兜底止损 diff={diff:.0f}" # 追踪止损 if not exit_reason and peak_diff - fav_diff >= TRAILING_STOP_RETRACEMENT: exit_signal = "sl" exit_reason = f"追踪止损 回撤{peak_diff - fav_diff:.0f}" if exit_reason: exit_price = my_pct / 100 pnl = exit_price - entry_price trades.append({ "window": ws, "direction": direction, "entry_price": entry_price, "exit_price": exit_price, "pnl": pnl, "signal": exit_signal, "reason": exit_reason, }) holding = False direction = None last_diff = diff if trades: wins = [t for t in trades if t["pnl"] > 0] losses = [t for t in trades if t["pnl"] <= 0] total_pnl = sum(t["pnl"] for t in trades) print(f"总交易次数: {len(trades)}") print(f"盈利次数: {len(wins)} ({len(wins)/len(trades)*100:.0f}%)") print(f"亏损次数: {len(losses)} ({len(losses)/len(trades)*100:.0f}%)") print(f"总 PnL: {total_pnl:+.4f}") 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}") print() # 按出场原因统计 reason_stats = defaultdict(lambda: {"count": 0, "pnl": 0}) for t in trades: key = t["reason"].split(" ")[0] + " " + t["reason"].split(" ")[1] if len(t["reason"].split(" ")) > 1 else t["reason"] # 简化为类型 if "阶梯" in t["reason"]: key = "阶梯止盈" elif "回撤止盈" in t["reason"]: key = "回撤止盈" elif "兜底" in t["reason"]: key = "兜底止损" elif "追踪" in t["reason"]: key = "追踪止损" elif "强制" in t["reason"]: key = "强制平仓" reason_stats[key]["count"] += 1 reason_stats[key]["pnl"] += t["pnl"] print("按出场原因统计:") for key in sorted(reason_stats, key=lambda k: -reason_stats[k]["count"]): s = reason_stats[key] print(f" {key}: {s['count']}次, PnL {s['pnl']:+.4f}") print() print("逐笔明细:") for t in trades: dir_zh = "涨" if t["direction"] == "up" else "跌" print(f" 窗口{t['window']} 买{dir_zh} 入{t['entry_price']:.2f}→出{t['exit_price']:.2f} PnL{t['pnl']:+.4f} {t['reason']}") else: print("无交易触发") # ── 6. 策略2(常规)模拟回测 ────────────────────────────────── print() print("=" * 70) print("策略2(常规)模拟回测") print("=" * 70) S2_ENTRY_DIFF = 40 S2_ENTRY_PROB_CAP = 75 S2_WINDOW_MAX_REM = 168 S2_WINDOW_MIN_REM = 48 S2_STOP_LOSS_DIFF = 5 S2_TP_LADDER_FLOOR = 8 trades2 = [] for ws in windows: wticks = sorted(window_ticks[ws], key=lambda t: t["ts"]) if len(wticks) < 10: continue last_diff = None holding = False direction = None entry_price = None for t in wticks: diff = t["diff"] upPct = t["upPct"] rem = t["rem"] if not holding: if rem <= S2_WINDOW_MAX_REM and rem > S2_WINDOW_MIN_REM and last_diff is not None: if last_diff <= S2_ENTRY_DIFF and diff > S2_ENTRY_DIFF and upPct < S2_ENTRY_PROB_CAP: holding = True direction = "up" entry_price = upPct / 100 elif last_diff >= -S2_ENTRY_DIFF and diff < -S2_ENTRY_DIFF and (100 - upPct) < S2_ENTRY_PROB_CAP: holding = True direction = "down" entry_price = (100 - upPct) / 100 else: my_pct = upPct if direction == "up" else (100 - upPct) exit_reason = None exit_signal = None # 阶梯止盈(固定公式:168→8,每16s升1%) if rem >= S2_TP_LADDER_FLOOR: span = S2_WINDOW_MAX_REM - S2_TP_LADDER_FLOOR elapsed = max(0, S2_WINDOW_MAX_REM - max(rem, S2_TP_LADDER_FLOOR)) tp_thr = 90 + int(elapsed / span * 10) tp_capped = min(tp_thr, 100) if my_pct >= tp_capped: exit_signal = "tp" exit_reason = f"阶梯止盈 {my_pct}%>={tp_capped}%" # 止损 if not exit_reason: if direction == "up" and diff <= S2_STOP_LOSS_DIFF: exit_signal = "sl" exit_reason = f"止损 diff={diff:.0f}" elif direction == "down" and diff >= -S2_STOP_LOSS_DIFF: exit_signal = "sl" exit_reason = f"止损 diff={diff:.0f}" if exit_reason: exit_price = my_pct / 100 pnl = exit_price - entry_price trades2.append({ "window": ws, "direction": direction, "entry_price": entry_price, "exit_price": exit_price, "pnl": pnl, "signal": exit_signal, "reason": exit_reason, }) holding = False direction = None last_diff = diff if trades2: wins2 = [t for t in trades2 if t["pnl"] > 0] losses2 = [t for t in trades2 if t["pnl"] <= 0] total_pnl2 = sum(t["pnl"] for t in trades2) print(f"总交易次数: {len(trades2)}") print(f"盈利次数: {len(wins2)} ({len(wins2)/len(trades2)*100:.0f}%)") print(f"亏损次数: {len(losses2)} ({len(losses2)/len(trades2)*100:.0f}%)") print(f"总 PnL: {total_pnl2:+.4f}") if wins2: print(f"平均盈利: +{sum(t['pnl'] for t in wins2)/len(wins2):.4f}") if losses2: print(f"平均亏损: {sum(t['pnl'] for t in losses2)/len(losses2):.4f}") print() reason_stats2 = defaultdict(lambda: {"count": 0, "pnl": 0}) for t in trades2: if "阶梯" in t["reason"]: key = "阶梯止盈" elif "止损" in t["reason"]: key = "止损" else: key = t["reason"] reason_stats2[key]["count"] += 1 reason_stats2[key]["pnl"] += t["pnl"] print("按出场原因统计:") for key in sorted(reason_stats2, key=lambda k: -reason_stats2[k]["count"]): s = reason_stats2[key] print(f" {key}: {s['count']}次, PnL {s['pnl']:+.4f}") print() print("逐笔明细:") for t in trades2: dir_zh = "涨" if t["direction"] == "up" else "跌" print(f" 窗口{t['window']} 买{dir_zh} 入{t['entry_price']:.2f}→出{t['exit_price']:.2f} PnL{t['pnl']:+.4f} {t['reason']}") else: print("无交易触发") # ── 7. 对比总结 ────────────────────────────────────────────── print() print("=" * 70) print("策略对比") print("=" * 70) s1_pnl = sum(t["pnl"] for t in trades) if trades else 0 s2_pnl = sum(t["pnl"] for t in trades2) if trades2 else 0 s1_wins = sum(1 for t in trades if t["pnl"] > 0) if trades else 0 s2_wins = sum(1 for t in trades2 if t["pnl"] > 0) if trades2 else 0 print(f"{'':15} {'常规加强':>10} {'常规':>10}") print(f"{'交易次数':15} {len(trades):>10} {len(trades2):>10}") print(f"{'胜率':15} {(s1_wins/len(trades)*100 if trades else 0):>9.0f}% {(s2_wins/len(trades2)*100 if trades2 else 0):>9.0f}%") print(f"{'总PnL':15} {s1_pnl:>+10.4f} {s2_pnl:>+10.4f}") # ── 8. 波动率分析 ───────────────────────────────────────────── print() print("=" * 70) print("波动率分析(每个窗口内 diff 标准差)") print("=" * 70) import math vol_data = [] for ws in windows: wticks = window_ticks[ws] if len(wticks) < 10: continue diffs = [t["diff"] for t in wticks] mean = sum(diffs) / len(diffs) variance = sum((d - mean) ** 2 for d in diffs) / len(diffs) std = math.sqrt(variance) vol_data.append({"window": ws, "std": std, "count": len(wticks)}) if vol_data: stds = [v["std"] for v in vol_data] print(f"窗口数: {len(vol_data)}") print(f"diff 标准差 范围: {min(stds):.1f} ~ {max(stds):.1f}") print(f"diff 标准差 均值: {sum(stds)/len(stds):.1f}") print(f"diff 标准差 中位数: {sorted(stds)[len(stds)//2]:.1f}") # 高波动 vs 低波动时的概率偏差 median_std = sorted(stds)[len(stds) // 2] high_vol_windows = set(v["window"] for v in vol_data if v["std"] > median_std) low_vol_windows = set(v["window"] for v in vol_data if v["std"] <= median_std) if biases: high_biases = [b for b in biases if b["windowStart"] in high_vol_windows] low_biases = [b for b in biases if b["windowStart"] in low_vol_windows] if high_biases and low_biases: print(f"\n高波动窗口 偏差绝对值均值: {sum(abs(b['bias']) for b in high_biases)/len(high_biases):.1f}%") print(f"低波动窗口 偏差绝对值均值: {sum(abs(b['bias']) for b in low_biases)/len(low_biases):.1f}%") # ── 9. 参数优化(常规加强) ─────────────────────────────────── print() print("=" * 70) print("参数优化:常规加强策略 — 遍历参数组合找最优") print("=" * 70) def run_backtest(params): """用给定参数跑一遍回测,返回交易列表""" ed = params["entry_diff"] epc = params["entry_prob_cap"] wmx = params["window_max_rem"] wmn = params["window_min_rem"] tsr = params["trailing_stop_ret"] tsm = params["trailing_stop_min"] ppm = params["prob_peak_min"] ppr = params["prob_peak_ret"] fer = params["force_exit_rem"] tps = params["tp_start"] tpn = 100 - tps # 从起始值到100%的百分点数,每1%一档 result = [] for ws in windows: wticks = sorted(window_ticks[ws], key=lambda t: t["ts"]) if len(wticks) < 10: continue last_diff = None holding = False direction = None entry_price = None peak_diff = None peak_prob = None for t in wticks: diff = t["diff"] upPct = t["upPct"] rem = t["rem"] if not holding: if rem <= wmx and rem > wmn and last_diff is not None: if last_diff <= ed and diff > ed and upPct < epc: holding = True direction = "up" entry_price = upPct / 100 peak_diff = diff peak_prob = upPct elif last_diff >= -ed and diff < -ed and (100 - upPct) < epc: holding = True direction = "down" entry_price = (100 - upPct) / 100 peak_diff = -diff peak_prob = 100 - upPct else: my_pct = upPct if direction == "up" else (100 - upPct) fav_diff = diff if direction == "up" else -diff if fav_diff > peak_diff: peak_diff = fav_diff if my_pct > peak_prob: peak_prob = my_pct exit_reason = None # 强制平仓 if rem <= fer and rem > 0: exit_reason = "强制平仓" # 阶梯止盈 if not exit_reason and rem >= fer: span = wmx - fer elapsed = max(0, wmx - rem) tp_capped = min(tps + int(elapsed / span * tpn), 100) if my_pct >= tp_capped: exit_reason = "阶梯止盈" # 回撤止盈 if not exit_reason and peak_prob >= ppm and my_pct <= peak_prob - ppr: exit_reason = "回撤止盈" # 兜底止损 if not exit_reason: if direction == "up" and diff <= tsm: exit_reason = "兜底止损" elif direction == "down" and diff >= -tsm: exit_reason = "兜底止损" # 追踪止损 if not exit_reason and peak_diff - fav_diff >= tsr: exit_reason = "追踪止损" if exit_reason: exit_price = my_pct / 100 pnl = exit_price - entry_price result.append(pnl) holding = False direction = None last_diff = diff return result # 参数搜索空间 param_grid = { "entry_diff": [25, 30, 35, 40, 45], "entry_prob_cap": [75, 80, 85], "window_max_rem": [190, 210, 240], "window_min_rem": [30, 50], "trailing_stop_ret":[15, 20, 25], "trailing_stop_min":[5], # 上轮不敏感,固定 "prob_peak_min": [80, 85], "prob_peak_ret": [5, 8, 10], "force_exit_rem": [8, 10], "tp_start": [88, 90, 92, 95], } # 生成所有组合 from itertools import product import multiprocessing as mp mp.set_start_method("fork", force=True) from multiprocessing import Pool, cpu_count keys = list(param_grid.keys()) combos = list(product(*[param_grid[k] for k in keys])) print(f"总参数组合数: {len(combos)}, 使用 {cpu_count()} 核并行计算") print("计算中...") def eval_combo(combo): params = dict(zip(keys, combo)) pnls = run_backtest(params) if not pnls: return None total = sum(pnls) wins = sum(1 for p in pnls if p > 0) return { "params": params, "total_pnl": total, "trades": len(pnls), "win_rate": wins / len(pnls) * 100, } with Pool(cpu_count()) as pool: raw_results = pool.map(eval_combo, combos) top_results = [r for r in raw_results if r is not None] # 按 PnL 排序,显示 Top 15 top_results.sort(key=lambda x: -x["total_pnl"]) best = top_results[0] if top_results else None print(f"\n{'排名':>4} {'交易':>4} {'胜率':>6} {'总PnL':>8} 参数") print("-" * 100) for i, r in enumerate(top_results[:15]): p = r["params"] param_str = f"diff={p['entry_diff']} cap={p['entry_prob_cap']} win={p['window_max_rem']}-{p['window_min_rem']} ts={p['trailing_stop_ret']}/{p['trailing_stop_min']} pp={p['prob_peak_min']}/{p['prob_peak_ret']} fer={p['force_exit_rem']} tp={p['tp_start']}→100%" marker = " ★" if i == 0 else "" print(f"{i+1:>4} {r['trades']:>4} {r['win_rate']:>5.0f}% {r['total_pnl']:>+8.4f} {param_str}{marker}") if best: print(f"\n最优参数:") for k, v in best["params"].items(): label = { "entry_diff": "ENTRY_DIFF(入场差价阈值)", "entry_prob_cap": "ENTRY_PROB_CAP(入场概率上限)", "window_max_rem": "WINDOW_MAX_REMAINING(扫描起始)", "window_min_rem": "WINDOW_MIN_REMAINING(扫描截止)", "trailing_stop_ret": "TRAILING_STOP_RETRACEMENT(追踪止损回撤)", "trailing_stop_min": "TRAILING_STOP_MIN_DIFF(兜底止损)", "prob_peak_min": "PROB_PEAK_MIN_THRESHOLD(回撤止盈门槛)", "prob_peak_ret": "PROB_PEAK_RETRACEMENT(回撤止盈幅度)", "force_exit_rem": "FORCE_EXIT_REM(强制平仓秒数)", "tp_start": "阶梯止盈起始概率(每1%一档升至100%)", }.get(k, k) print(f" {label}: {v}") print(f" 总PnL: {best['total_pnl']:+.4f}, 交易{best['trades']}笔, 胜率{best['win_rate']:.0f}%") print(f" 总PnL: {best_pnl:+.4f}, 交易{len(best_trades)}笔, 胜率{sum(1 for p in best_trades if p>0)/len(best_trades)*100:.0f}%")