""" 布林带均线策略回测 — 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)