198 lines
7.9 KiB
Python
198 lines
7.9 KiB
Python
|
|
"""
|
|||
|
|
回测 bb_trade.py 的 D方案策略
|
|||
|
|
BB(10, 2.5) | 5分钟 | ETH | 50x | 递增加仓+1%/次 max=3
|
|||
|
|
200U 本金 | 每次开仓 1% 权益 | 开平仓手续费万五 | 返佣90%次日早8点到账
|
|||
|
|
"""
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# 加载数据 2020-2025
|
|||
|
|
# ============================================================
|
|||
|
|
YEARS = list(range(2020, 2026))
|
|||
|
|
|
|||
|
|
print("加载 5 分钟 K 线数据 (2020-2025)...")
|
|||
|
|
t0 = time.time()
|
|||
|
|
data = {}
|
|||
|
|
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]})")
|
|||
|
|
|
|||
|
|
# 合并全量数据用于连续回测
|
|||
|
|
df_all = pd.concat([data[y] for y in YEARS])
|
|||
|
|
df_all = df_all[~df_all.index.duplicated(keep='first')].sort_index()
|
|||
|
|
print(f" 合计: {len(df_all):>7,} 条 ({df_all.index[0]} ~ {df_all.index[-1]})")
|
|||
|
|
print(f"数据加载完成 ({time.time()-t0:.1f}s)\n")
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# 配置 — 完全匹配 bb_trade.py D方案
|
|||
|
|
# ============================================================
|
|||
|
|
cfg = BBConfig(
|
|||
|
|
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点
|
|||
|
|
pyramid_enabled=True,
|
|||
|
|
pyramid_step=0.01, # 递增加仓 +1%/次
|
|||
|
|
pyramid_max=3, # 最多加仓3次
|
|||
|
|
slippage_pct=0.0, # 回测不加滑点 (实盘浏览器市价单有滑点)
|
|||
|
|
liq_enabled=True,
|
|||
|
|
stop_loss_pct=0.0,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# 1) 逐年回测 (每年独立 200U 起步)
|
|||
|
|
# ============================================================
|
|||
|
|
print("=" * 100)
|
|||
|
|
print(" 【逐年独立回测】每年独立 200U 本金")
|
|||
|
|
print(f" BB({cfg.bb_period}, {cfg.bb_std}) | {cfg.leverage}x | 开仓={cfg.margin_pct:.0%}权益 | "
|
|||
|
|
f"手续费={cfg.fee_rate:.4%}/侧 | 返佣={cfg.rebate_pct:.0%}次日8点")
|
|||
|
|
print("=" * 100)
|
|||
|
|
print(f" {'年份':>6s} {'最终权益':>10s} {'收益率':>8s} {'日均PnL':>8s} {'交易次数':>8s} {'胜率':>6s} "
|
|||
|
|
f"{'最大回撤':>10s} {'回撤%':>8s} {'总手续费':>10s} {'总返佣':>10s} {'净手续费':>10s} {'Sharpe':>7s}")
|
|||
|
|
print("-" * 130)
|
|||
|
|
|
|||
|
|
year_results = {}
|
|||
|
|
for y in YEARS:
|
|||
|
|
r = run_bb_backtest(data[y], cfg)
|
|||
|
|
year_results[y] = r
|
|||
|
|
|
|||
|
|
d = r.daily_stats
|
|||
|
|
pnl = d["pnl"].astype(float)
|
|||
|
|
eq = d["equity"].astype(float)
|
|||
|
|
peak = eq.cummax()
|
|||
|
|
dd = float((eq - peak).min())
|
|||
|
|
dd_pct = dd / float(peak[eq - peak == dd].iloc[0]) * 100 if dd < 0 else 0
|
|||
|
|
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
|
|||
|
|
net_fee = r.total_fee - r.total_rebate
|
|||
|
|
|
|||
|
|
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} {dd_pct:>+7.1f}% "
|
|||
|
|
f"{r.total_fee:>10.1f} {r.total_rebate:>10.1f} {net_fee:>10.1f} {sharpe:>7.2f}")
|
|||
|
|
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# 2) 连续回测 (2020-2025 一次性跑,资金连续滚动)
|
|||
|
|
# ============================================================
|
|||
|
|
print("=" * 100)
|
|||
|
|
print(" 【连续回测】2020-2025 资金滚动,200U 起步")
|
|||
|
|
print("=" * 100)
|
|||
|
|
|
|||
|
|
r_all = run_bb_backtest(df_all, cfg)
|
|||
|
|
|
|||
|
|
d_all = r_all.daily_stats
|
|||
|
|
pnl_all = d_all["pnl"].astype(float)
|
|||
|
|
eq_all = d_all["equity"].astype(float)
|
|||
|
|
peak_all = eq_all.cummax()
|
|||
|
|
dd_all = float((eq_all - peak_all).min())
|
|||
|
|
dd_pct_all = dd_all / float(peak_all[eq_all - peak_all == dd_all].iloc[0]) * 100 if dd_all < 0 else 0
|
|||
|
|
final_eq_all = float(eq_all.iloc[-1])
|
|||
|
|
ret_pct_all = (final_eq_all - cfg.initial_capital) / cfg.initial_capital * 100
|
|||
|
|
n_trades_all = len(r_all.trades)
|
|||
|
|
win_rate_all = sum(1 for t in r_all.trades if t.net_pnl > 0) / max(n_trades_all, 1) * 100
|
|||
|
|
avg_daily_all = float(pnl_all.mean())
|
|||
|
|
sharpe_all = float(pnl_all.mean() / pnl_all.std()) * np.sqrt(365) if pnl_all.std() > 0 else 0
|
|||
|
|
net_fee_all = r_all.total_fee - r_all.total_rebate
|
|||
|
|
|
|||
|
|
print(f" 初始资金: {cfg.initial_capital:.0f} U")
|
|||
|
|
print(f" 最终权益: {final_eq_all:.1f} U")
|
|||
|
|
print(f" 总收益率: {ret_pct_all:+.1f}%")
|
|||
|
|
print(f" 日均 PnL: {avg_daily_all:+.2f} U")
|
|||
|
|
print(f" 交易次数: {n_trades_all}")
|
|||
|
|
print(f" 胜率: {win_rate_all:.1f}%")
|
|||
|
|
print(f" 最大回撤: {dd_all:+.1f} U ({dd_pct_all:+.1f}%)")
|
|||
|
|
print(f" 总手续费: {r_all.total_fee:.1f} U")
|
|||
|
|
print(f" 总返佣: {r_all.total_rebate:.1f} U")
|
|||
|
|
print(f" 净手续费: {net_fee_all:.1f} U")
|
|||
|
|
print(f" Sharpe: {sharpe_all:.2f}")
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
# 按年统计连续回测中的表现
|
|||
|
|
print(" 连续回测逐年切片:")
|
|||
|
|
print(f" {'年份':>6s} {'年初权益':>10s} {'年末权益':>10s} {'年收益':>10s} {'年收益率':>8s}")
|
|||
|
|
print("-" * 60)
|
|||
|
|
for y in YEARS:
|
|||
|
|
mask = (eq_all.index >= f'{y}-01-01') & (eq_all.index < f'{y+1}-01-01')
|
|||
|
|
if mask.sum() == 0:
|
|||
|
|
continue
|
|||
|
|
eq_year = eq_all[mask]
|
|||
|
|
start_eq = float(eq_year.iloc[0])
|
|||
|
|
end_eq = float(eq_year.iloc[-1])
|
|||
|
|
yr_ret = end_eq - start_eq
|
|||
|
|
yr_pct = yr_ret / start_eq * 100
|
|||
|
|
print(f" {y:>6d} {start_eq:>10.1f} {end_eq:>10.1f} {yr_ret:>+10.1f} {yr_pct:>+7.1f}%")
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# 图表
|
|||
|
|
# ============================================================
|
|||
|
|
fig, axes = plt.subplots(3, 1, figsize=(18, 18), dpi=120)
|
|||
|
|
|
|||
|
|
# 图1: 逐年独立回测权益曲线
|
|||
|
|
ax1 = axes[0]
|
|||
|
|
colors = plt.cm.tab10(np.linspace(0, 1, len(YEARS)))
|
|||
|
|
for i, y in enumerate(YEARS):
|
|||
|
|
r = year_results[y]
|
|||
|
|
eq = r.equity_curve["equity"].dropna()
|
|||
|
|
days = (eq.index - eq.index[0]).total_seconds() / 86400
|
|||
|
|
ax1.plot(days, eq.values, label=f"{y}", color=colors[i], linewidth=0.8)
|
|||
|
|
ax1.set_title(f"BB(10,2.5) D方案 逐年独立回测 (200U起步)", fontsize=13, fontweight="bold")
|
|||
|
|
ax1.set_xlabel("天数")
|
|||
|
|
ax1.set_ylabel("权益 (USDT)")
|
|||
|
|
ax1.axhline(y=200, color="gray", linestyle="--", alpha=0.5, label="本金200U")
|
|||
|
|
ax1.legend(loc="upper left", fontsize=9)
|
|||
|
|
ax1.grid(True, alpha=0.3)
|
|||
|
|
|
|||
|
|
# 图2: 连续回测权益曲线
|
|||
|
|
ax2 = axes[1]
|
|||
|
|
eq_curve = r_all.equity_curve["equity"].dropna()
|
|||
|
|
ax2.plot(eq_curve.index, eq_curve.values, color="steelblue", linewidth=0.6)
|
|||
|
|
ax2.set_title(f"BB(10,2.5) D方案 连续回测 2020-2025 (200U→{final_eq_all:.0f}U)", fontsize=13, fontweight="bold")
|
|||
|
|
ax2.set_xlabel("日期")
|
|||
|
|
ax2.set_ylabel("权益 (USDT)")
|
|||
|
|
ax2.axhline(y=200, color="gray", linestyle="--", alpha=0.5)
|
|||
|
|
ax2.grid(True, alpha=0.3)
|
|||
|
|
|
|||
|
|
# 图3: 连续回测日PnL
|
|||
|
|
ax3 = axes[2]
|
|||
|
|
daily_pnl = r_all.daily_stats["pnl"].astype(float)
|
|||
|
|
colors_pnl = ['green' if x >= 0 else 'red' for x in daily_pnl.values]
|
|||
|
|
ax3.bar(daily_pnl.index, daily_pnl.values, color=colors_pnl, width=1, alpha=0.7)
|
|||
|
|
ax3.set_title("日 PnL 分布", fontsize=13, fontweight="bold")
|
|||
|
|
ax3.set_xlabel("日期")
|
|||
|
|
ax3.set_ylabel("PnL (USDT)")
|
|||
|
|
ax3.axhline(y=0, color="black", linewidth=0.5)
|
|||
|
|
ax3.grid(True, alpha=0.3)
|
|||
|
|
|
|||
|
|
plt.tight_layout()
|
|||
|
|
chart_path = out_dir / "bb_trade_d_plan_2020_2025.png"
|
|||
|
|
plt.savefig(chart_path, bbox_inches="tight")
|
|||
|
|
print(f"图表已保存: {chart_path}")
|