Files
codex_jxs_code/strategy/run_bb_trade_backtest.py
2026-02-26 16:34:30 +08:00

198 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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