148 lines
5.5 KiB
Python
148 lines
5.5 KiB
Python
"""
|
|
BB(10, 2.5) 均值回归策略回测 — 2020-2025 逐年
|
|
完全复现 bb_trade.py 的参数: BB(10,2.5) | 50x | 1%权益/单 | 1000U初始资金
|
|
|
|
测试两种手续费场景:
|
|
A) 0.06% taker (无返佣)
|
|
B) 0.025% maker + 返佣 (模拟浏览器下单)
|
|
"""
|
|
import sys, time
|
|
sys.stdout.reconfigure(line_buffering=True)
|
|
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parents[1]))
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import matplotlib
|
|
matplotlib.use("Agg")
|
|
import matplotlib.pyplot as plt
|
|
from pathlib import Path
|
|
|
|
from strategy.bb_backtest import BBConfig, run_bb_backtest
|
|
from strategy.data_loader import load_klines
|
|
|
|
out_dir = Path(__file__).resolve().parent / "results"
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# ============================================================
|
|
# 加载数据
|
|
# ============================================================
|
|
YEARS = list(range(2020, 2027))
|
|
data = {}
|
|
|
|
print("加载 5 分钟 K 线数据...")
|
|
t0 = time.time()
|
|
for y in YEARS:
|
|
df = load_klines('5m', f'{y}-01-01', f'{y+1}-01-01')
|
|
data[y] = df
|
|
print(f" {y}: {len(df):>7,} 条 ({df.index[0]} ~ {df.index[-1]})")
|
|
print(f"数据加载完成 ({time.time()-t0:.1f}s)\n")
|
|
|
|
# ============================================================
|
|
# 配置 (完全匹配 bb_trade.py)
|
|
# ============================================================
|
|
BASE_KWARGS = dict(
|
|
bb_period=10,
|
|
bb_std=2.5,
|
|
leverage=50,
|
|
initial_capital=200.0,
|
|
margin_pct=0.01, # 1% 权益/单
|
|
max_daily_loss=50.0,
|
|
fee_rate=0.0005, # 万五 (0.05%) 每侧
|
|
rebate_rate=0.0,
|
|
rebate_pct=0.90, # 90% 手续费次日返还
|
|
rebate_hour_utc=0, # UTC 0点 = 北京时间早上8点
|
|
)
|
|
|
|
configs = {
|
|
"A) 原版(不加仓)": BBConfig(**BASE_KWARGS, pyramid_enabled=False),
|
|
"B) 衰减加仓 decay=0.99 max=10": BBConfig(**BASE_KWARGS, pyramid_enabled=True, pyramid_decay=0.99, pyramid_max=10),
|
|
"C) 衰减加仓 decay=0.99 max=3": BBConfig(**BASE_KWARGS, pyramid_enabled=True, pyramid_decay=0.99, pyramid_max=3),
|
|
"D) 递增加仓 +1%/次 max=3": BBConfig(**BASE_KWARGS, pyramid_enabled=True, pyramid_step=0.01, pyramid_max=3),
|
|
"E) 递增加仓 +1%/次 max=10": BBConfig(**BASE_KWARGS, pyramid_enabled=True, pyramid_step=0.01, pyramid_max=10),
|
|
}
|
|
|
|
# ============================================================
|
|
# 运行回测
|
|
# ============================================================
|
|
all_results = {}
|
|
|
|
for label, cfg in configs.items():
|
|
print("=" * 100)
|
|
print(f" {label}")
|
|
print(f" BB({cfg.bb_period}, {cfg.bb_std}) | {cfg.leverage}x | margin_pct={cfg.margin_pct:.0%} | fee={cfg.fee_rate:.4%}")
|
|
print("=" * 100)
|
|
print(f" {'年份':>6s} {'最终权益':>10s} {'收益率':>8s} {'日均PnL':>8s} {'交易次数':>8s} {'胜率':>6s} "
|
|
f"{'最大回撤':>10s} {'总手续费':>10s} {'总返佣':>10s} {'Sharpe':>7s}")
|
|
print("-" * 100)
|
|
|
|
year_results = {}
|
|
for y in YEARS:
|
|
r = run_bb_backtest(data[y], cfg)
|
|
d = r.daily_stats
|
|
pnl = d["pnl"].astype(float)
|
|
eq = d["equity"].astype(float)
|
|
dd = float((eq - eq.cummax()).min())
|
|
final_eq = float(eq.iloc[-1])
|
|
ret_pct = (final_eq - cfg.initial_capital) / cfg.initial_capital * 100
|
|
n_trades = len(r.trades)
|
|
win_rate = sum(1 for t in r.trades if t.net_pnl > 0) / max(n_trades, 1) * 100
|
|
avg_daily = float(pnl.mean())
|
|
sharpe = float(pnl.mean() / pnl.std()) * np.sqrt(365) if pnl.std() > 0 else 0
|
|
|
|
year_results[y] = r
|
|
|
|
print(f" {y:>6d} {final_eq:>10.1f} {ret_pct:>+7.1f}% {avg_daily:>+7.2f}U "
|
|
f"{n_trades:>8d} {win_rate:>5.1f}% {dd:>+10.1f} "
|
|
f"{r.total_fee:>10.1f} {r.total_rebate:>10.1f} {sharpe:>7.2f}")
|
|
|
|
all_results[label] = year_results
|
|
print()
|
|
|
|
# ============================================================
|
|
# 生成图表
|
|
# ============================================================
|
|
fig, axes = plt.subplots(len(configs), 1, figsize=(18, 6 * len(configs)), dpi=120)
|
|
if len(configs) == 1:
|
|
axes = [axes]
|
|
|
|
colors = plt.cm.tab10(np.linspace(0, 1, len(YEARS)))
|
|
|
|
for ax, (label, year_results) in zip(axes, all_results.items()):
|
|
for i, y in enumerate(YEARS):
|
|
r = year_results[y]
|
|
eq = r.equity_curve["equity"].dropna()
|
|
# 归一化到天数 (x轴)
|
|
days = (eq.index - eq.index[0]).total_seconds() / 86400
|
|
ax.plot(days, eq.values, label=f"{y}", color=colors[i], linewidth=0.8)
|
|
|
|
ax.set_title(f"BB(10, 2.5) 50x 1%权益 — {label}", fontsize=13, fontweight="bold")
|
|
ax.set_xlabel("天数")
|
|
ax.set_ylabel("权益 (USDT)")
|
|
ax.axhline(y=1000, color="gray", linestyle="--", alpha=0.5)
|
|
ax.legend(loc="upper left", fontsize=9)
|
|
ax.grid(True, alpha=0.3)
|
|
|
|
plt.tight_layout()
|
|
chart_path = out_dir / "bb_trade_2020_2025_report.png"
|
|
plt.savefig(chart_path, bbox_inches="tight")
|
|
print(f"\n图表已保存: {chart_path}")
|
|
|
|
# ============================================================
|
|
# 汇总表
|
|
# ============================================================
|
|
print("\n" + "=" * 80)
|
|
print(" 汇总: 各场景各年度日均 PnL (U/day)")
|
|
print("=" * 80)
|
|
header = f" {'场景':<35s}" + "".join(f"{y:>10d}" for y in YEARS)
|
|
print(header)
|
|
print("-" * 80)
|
|
for label, year_results in all_results.items():
|
|
vals = []
|
|
for y in YEARS:
|
|
r = year_results[y]
|
|
avg = float(r.daily_stats["pnl"].astype(float).mean())
|
|
vals.append(avg)
|
|
row = f" {label:<35s}" + "".join(f"{v:>+10.2f}" for v in vals)
|
|
print(row)
|
|
print("=" * 80)
|