Files
codex_jxs_code/strategy/run_bb_trade_backtest.py

198 lines
7.9 KiB
Python
Raw Normal View History

2026-02-26 16:34:30 +08:00
"""
回测 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}")