Files
codex_jxs_code/run_bb_midline_2020_2025.py
2026-02-26 19:05:17 +08:00

357 lines
16 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.

"""
布林带均线策略回测 — 2020-2025优化版
策略:
- 阳线 + 碰到均线 → 开多1m 过滤:先涨碰到)
- 持多: 碰上轨止盈(无下轨止损)
- 阴线 + 碰到均线 → 平多开空1m先跌碰到
- 持空: 碰下轨止盈(无上轨止损)
配置: 200U | 1%权益/单 | 万五手续费 | 90%返佣次日8点 | 100x杠杆 | 全仓
参数扫描:
- 快速: --sweep
- 全量: --full 试遍 period(0.5~1000 step0.5) × std(0.5~1000 step0.5)
"""
import sys
import time
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor, as_completed
sys.path.insert(0, str(Path(__file__).resolve().parents[0]))
sys.stdout.reconfigure(line_buffering=True)
import numpy as np
import pandas as pd
from strategy.bb_midline_backtest import BBMidlineConfig, run_bb_midline_backtest
from strategy.data_loader import load_klines
def run_single(df: pd.DataFrame, df_1m: pd.DataFrame | None, cfg: BBMidlineConfig) -> dict:
r = run_bb_midline_backtest(df, cfg, df_1m=df_1m)
eq = r.equity_curve["equity"].dropna()
if len(eq) == 0:
return {"final_eq": 0, "ret_pct": -100, "n_trades": 0, "win_rate": 0, "sharpe": -999, "dd": -200}
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
pnl = r.daily_stats["pnl"].astype(float)
sharpe = float(pnl.mean() / pnl.std()) * np.sqrt(365) if pnl.std() > 0 else 0
dd = float((eq.astype(float) - eq.astype(float).cummax()).min())
return {
"final_eq": final_eq,
"ret_pct": ret_pct,
"n_trades": n_trades,
"win_rate": win_rate,
"sharpe": sharpe,
"dd": dd,
"result": r,
}
def build_param_grid(
period_range: tuple[float, float] = (1, 200),
std_range: tuple[float, float] = (0.5, 10),
period_step: float = 1.0,
std_step: float = 0.5,
) -> list[tuple[int, float]]:
"""生成 (period, std) 参数网格period 取整(rolling 需整数)"""
out = []
p = period_range[0]
while p <= period_range[1]:
s = std_range[0]
while s <= std_range[1]:
out.append((max(1, int(round(p))), round(s, 2)))
s += std_step
p += period_step
return out
def build_full_param_grid(
period_step: float = 0.5,
std_step: float = 0.5,
period_max: float = 1000.0,
std_max: float = 1000.0,
) -> list[tuple[int, float]]:
"""全量网格: (0.5,0.5)(0.5,1)...(0.5,std_max), (1,0.5)(1,1)...(1,std_max), ..."""
out = []
p = 0.5
while p <= period_max:
s = 0.5
while s <= std_max:
out.append((max(1, int(round(p))), round(s, 2)))
s += std_step
p += period_step
return out
def _run_one(args: tuple) -> dict:
"""供多进程调用:((p, s), df_path, use_1m, step_min) -> res"""
(p, s), df_path, use_1m, step_min = args
df = pd.read_pickle(df_path)
df_1m = None
cfg = BBMidlineConfig(
bb_period=p, bb_std=s,
initial_capital=200.0, margin_pct=0.01, leverage=100.0,
cross_margin=True, fee_rate=0.0005, rebate_pct=0.90,
rebate_hour_utc=0, fill_at_close=True,
use_1m_touch_filter=False, kline_step_min=step_min,
)
r = run_bb_midline_backtest(df, cfg, df_1m=None)
eq = r.equity_curve["equity"].dropna()
if len(eq) == 0:
return {"period": p, "std": s, "final_eq": 0, "ret_pct": -100, "n_trades": 0, "win_rate": 0, "sharpe": -999, "dd": -200}
final_eq = float(eq.iloc[-1])
ret_pct = (final_eq - 200) / 200 * 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
pnl = r.daily_stats["pnl"].astype(float)
sharpe = float(pnl.mean() / pnl.std()) * np.sqrt(365) if pnl.std() > 0 else 0
dd = float((eq.astype(float) - eq.astype(float).cummax()).min())
return {"period": p, "std": s, "final_eq": final_eq, "ret_pct": ret_pct, "n_trades": n_trades,
"win_rate": win_rate, "sharpe": sharpe, "dd": dd}
def main(sweep_params: bool = False, full_sweep: bool = False, steps: str | None = None,
use_1m: bool = True, kline_period: str = "5m", workers: int = 1,
max_period: float = 1000.0, max_std: float = 1000.0):
out_dir = Path(__file__).resolve().parent / "strategy" / "results"
out_dir.mkdir(parents=True, exist_ok=True)
step_min = int(kline_period.replace("m", ""))
print(f"加载 K 线数据 (2020-01-01 ~ 2026-01-01) 周期={kline_period}...")
t0 = time.time()
df = load_klines(kline_period, "2020-01-01", "2026-01-01")
df_1m = load_klines("1m", "2020-01-01", "2026-01-01") if use_1m else None
print(f" {kline_period}: {len(df):,}" + (f", 1m: {len(df_1m):,}" if df_1m is not None else "") + f" ({time.time()-t0:.1f}s)\n")
if full_sweep:
# 全量网格: period 0.5~period_max step0.5, std 0.5~std_max step0.5,多进程
grid = build_full_param_grid(period_step=0.5, std_step=0.5, period_max=max_period, std_max=max_std)
print(f" 全量参数扫描: {len(grid):,} 组 (period 0.5~{max_period} step0.5 × std 0.5~{max_std} step0.5)")
use_parallel = workers > 1
if workers <= 0:
workers = max(1, (__import__("os").cpu_count() or 4) - 1)
use_parallel = workers > 1
print(f" 并行进程数: {workers}" + (" (多进程)" if use_parallel else " (顺序)"))
import tempfile
with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as f:
df.to_pickle(f.name)
df_path = f.name
try:
tasks = [((p, s), df_path, False, step_min) for p, s in grid]
all_results = []
best = None
best_score = -999
t0 = time.time()
if use_parallel:
try:
with ProcessPoolExecutor(max_workers=workers) as ex:
fut = {ex.submit(_run_one, t): t for t in tasks}
for f in as_completed(fut):
try:
res = f.result()
all_results.append(res)
score = res["ret_pct"] - 0.001 * max(0, res["n_trades"] - 3000)
if res["final_eq"] > 0 and score > best_score:
best_score = score
best = (res["period"], res["std"], res)
except Exception as e:
print(f" [WARN] 任务失败: {e}")
done = len(all_results)
if done % 5000 == 0 or done == len(tasks):
print(f" 进度: {done}/{len(tasks)} ({time.time()-t0:.0f}s)")
except (PermissionError, OSError) as e:
print(f" 多进程不可用 ({e}),改用顺序执行...")
use_parallel = False
if not use_parallel:
for i, t in enumerate(tasks):
try:
res = _run_one(t)
all_results.append(res)
score = res["ret_pct"] - 0.001 * max(0, res["n_trades"] - 3000)
if res["final_eq"] > 0 and score > best_score:
best_score = score
best = (res["period"], res["std"], res)
except Exception as e:
print(f" [WARN] 任务失败: {e}")
if (i + 1) % 5000 == 0 or i + 1 == len(tasks):
print(f" 进度: {i+1}/{len(tasks)} ({time.time()-t0:.0f}s)")
print(f" 全量扫描完成 ({time.time()-t0:.0f}s)")
finally:
Path(df_path).unlink(missing_ok=True)
if best:
p, s, res = best
print(f"\n 最优: BB({p},{s}) -> 权益={res['final_eq']:.1f} 收益={res['ret_pct']:+.1f}% 交易={res['n_trades']}")
sweep_df = pd.DataFrame(all_results)
sweep_path = out_dir / f"bb_midline_{kline_period}_full_sweep.csv"
sweep_df.to_csv(sweep_path, index=False)
print(f" 扫描结果: {sweep_path}")
cfg = BBMidlineConfig(
bb_period=best[0] if best else 20, bb_std=best[1] if best else 2.0,
initial_capital=200.0, margin_pct=0.01, leverage=100.0,
cross_margin=True, fee_rate=0.0005, rebate_pct=0.90,
rebate_hour_utc=0, fill_at_close=True, use_1m_touch_filter=False,
kline_step_min=step_min,
)
r = run_bb_midline_backtest(df, cfg, df_1m=None)
elif sweep_params:
# 步长组合: (period_step, std_step) 如 (0.5,0.5), (0.5,1), (1,0.5)
step_pairs = [(10, 0.5), (20, 1)] if steps is None else []
if steps:
for part in steps.split(","):
a, b = map(float, part.strip().split("_"))
step_pairs.append((a, b))
if not step_pairs:
step_pairs = [(5, 0.5), (10, 1)]
all_results = []
best = None
best_score = -999
for period_step, std_step in step_pairs:
grid = build_param_grid(
period_range=(15, 120),
std_range=(1.5, 4.0),
period_step=max(5, int(period_step)),
std_step=std_step,
)
print(f" 步长 (period>={period_step}, std+{std_step}): {len(grid)} 组...")
for p, s in grid:
cfg = BBMidlineConfig(
bb_period=p, bb_std=s,
initial_capital=200.0, margin_pct=0.01, leverage=100.0,
cross_margin=True, fee_rate=0.0005, rebate_pct=0.90,
rebate_hour_utc=0, fill_at_close=True,
use_1m_touch_filter=use_1m, kline_step_min=step_min,
)
res = run_single(df, df_1m, cfg)
res["period"] = p
res["std"] = s
res["period_step"] = period_step
res["std_step"] = std_step
all_results.append(res)
score = res["ret_pct"] - 0.001 * max(0, res["n_trades"] - 3000)
if res["final_eq"] > 0 and score > best_score:
best_score = score
best = (p, s, res)
if best:
p, s, res = best
print(f"\n 最优: BB({p},{s}) -> 权益={res['final_eq']:.1f} 收益={res['ret_pct']:+.1f}% 交易={res['n_trades']}")
sweep_rows = [
{"period": r["period"], "std": r["std"], "period_step": r.get("period_step"), "std_step": r.get("std_step"),
"final_eq": r["final_eq"], "ret_pct": r["ret_pct"], "n_trades": r["n_trades"],
"win_rate": r["win_rate"], "sharpe": r["sharpe"], "dd": r["dd"]}
for r in all_results
]
sweep_df = pd.DataFrame(sweep_rows)
sweep_path = out_dir / f"bb_midline_{kline_period}_param_sweep.csv"
sweep_df.to_csv(sweep_path, index=False)
print(f" 扫描结果: {sweep_path}")
cfg = BBMidlineConfig(
bb_period=best[0] if best else 20, bb_std=best[1] if best else 2.0,
initial_capital=200.0, margin_pct=0.01, leverage=100.0,
cross_margin=True, fee_rate=0.0005, rebate_pct=0.90,
rebate_hour_utc=0, fill_at_close=True, use_1m_touch_filter=use_1m,
kline_step_min=step_min,
)
r = best[2]["result"] if best and best[2].get("result") else run_bb_midline_backtest(df, cfg, df_1m if use_1m else None)
else:
cfg = BBMidlineConfig(
bb_period=20, bb_std=2.0,
initial_capital=200.0, margin_pct=0.01, leverage=100.0,
cross_margin=True, fee_rate=0.0005, rebate_pct=0.90,
rebate_hour_utc=0, fill_at_close=True,
use_1m_touch_filter=use_1m, kline_step_min=step_min,
)
r = run_bb_midline_backtest(df, cfg, df_1m if use_1m else None)
print("=" * 90)
print(f" 布林带均线策略回测({kline_period}周期" + (" | 1m触及方向过滤" if use_1m else "") + "")
print(f" BB({cfg.bb_period},{cfg.bb_std}) | 200U | 1%权益/单 | 万五 | 90%返佣次日8点 | 100x全仓")
print("=" * 90)
eq = r.equity_curve["equity"].dropna()
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
pnl = r.daily_stats["pnl"].astype(float)
avg_daily = float(pnl.mean())
sharpe = float(pnl.mean() / pnl.std()) * np.sqrt(365) if pnl.std() > 0 else 0
dd = float((eq.astype(float) - eq.astype(float).cummax()).min())
print(f"\n 最终权益: {final_eq:.1f} U")
print(f" 总收益率: {ret_pct:+.1f}%")
print(f" 交易次数: {n_trades}")
print(f" 胜率: {win_rate:.1f}%")
print(f" 日均PnL: {avg_daily:+.2f} U")
print(f" 最大回撤: {dd:.1f} U")
print(f" Sharpe: {sharpe:.2f}")
print(f" 总手续费: {r.total_fee:.2f} U")
print(f" 总返佣: {r.total_rebate:.2f} U")
years = list(range(2020, 2026))
eq_ts = eq.copy()
eq_ts.index = pd.to_datetime(eq_ts.index)
prev_ye = cfg.initial_capital
print("\n 逐年权益 (年末):")
for y in years:
subset = eq_ts[eq_ts.index.year == y]
if len(subset) > 0:
ye = float(subset.iloc[-1])
ret = (ye - prev_ye) / prev_ye * 100 if prev_ye > 0 else 0
print(f" {y}: {ye:.1f} U (当年收益 {ret:+.1f}%)")
prev_ye = ye
rows = []
for i, t in enumerate(r.trades, 1):
rows.append({
"序号": i,
"方向": "做多" if t.side == "long" else "做空",
"开仓时间": t.entry_time,
"平仓时间": t.exit_time,
"开仓价": round(t.entry_price, 2),
"平仓价": round(t.exit_price, 2),
"保证金": round(t.margin, 2),
"杠杆": t.leverage,
"数量": round(t.qty, 4),
"毛盈亏": round(t.gross_pnl, 2),
"手续费": round(t.fee, 2),
"净盈亏": round(t.net_pnl, 2),
"平仓原因": t.exit_reason,
})
pd.DataFrame(rows).to_csv(out_dir / f"bb_midline_{kline_period}_2020_2025_trade_detail.csv", index=False, encoding="utf-8-sig")
r.daily_stats.to_csv(out_dir / f"bb_midline_{kline_period}_2020_2025_daily.csv", encoding="utf-8-sig")
print(f"\n 交易明细: {out_dir / f'bb_midline_{kline_period}_2020_2025_trade_detail.csv'}")
if __name__ == "__main__":
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--sweep", action="store_true", help="快速参数扫描")
ap.add_argument("--full", action="store_true", dest="full_sweep", help="全量: period(0.5~1000 step0.5)×std(0.5~1000 step0.5)")
ap.add_argument("--steps", type=str, help="步长组合,如 1_0.5,2_1,5_0.5")
ap.add_argument("--no-1m", dest="no_1m", action="store_true", help="禁用 1m 触及方向过滤(更快)")
ap.add_argument("-p", "--period", choices=["5m", "15m", "30m"], default="5m", help="K线周期")
ap.add_argument("-j", "--workers", type=int, default=0, help="全量扫描并行进程数(0=auto)")
ap.add_argument("--max-period", type=float, default=1000, help="全量扫描 period 上限")
ap.add_argument("--max-std", type=float, default=1000, help="全量扫描 std 上限")
args = ap.parse_args()
main(sweep_params=args.sweep, full_sweep=args.full_sweep, steps=args.steps,
use_1m=not args.no_1m, kline_period=args.period, workers=args.workers,
max_period=args.max_period, max_std=args.max_std)