Files
btc5m-web/backtest-data/analyze.py
T
2026-05-31 13:49:36 +08:00

702 lines
25 KiB
Python

"""
回测分析脚本
用法: 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}%")