""" 回测 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}")