| |
|
|
| import argparse |
| import sys |
| import pandas as pd |
| import numpy as np |
| import os |
|
|
| |
| sys.path.append(os.path.dirname(os.path.abspath(__file__))) |
|
|
| from app.data_manager import fetch_and_prepare_data |
| from app.patterns import analyze_patterns |
| from app.simulator import run_backtest_sim |
|
|
| def walk_forward_optimization(ticker, interval="1d", period="2y", train_size=120, test_size=40): |
| """ |
| 运行滚动优化回测 (Walk-forward rolling optimization) |
| - 使用 train_size 根 K线作为训练集寻找最佳参数组合 |
| - 在随后的 test_size 根 K线中作为样本外测试执行该参数组合 |
| - 滚动前进,直到覆盖所有行情数据 |
| """ |
| print("=" * 80) |
| print(f" QUANT.AI - WALK-FORWARD OPTIMIZER (滚动参数优化引擎)") |
| print(f" 标的代码: {ticker} | 周期: {interval} | 区间: {period} | 训练集大小: {train_size} | 测试集大小: {test_size}") |
| print("=" * 80) |
| |
| |
| try: |
| df_raw = fetch_and_prepare_data(ticker, period=period, interval=interval) |
| df = analyze_patterns(df_raw) |
| except Exception as e: |
| raise ValueError(f"数据加载或指标计算失败: {str(e)}") |
| |
| total_len = len(df) |
| if total_len < (train_size + test_size): |
| raise ValueError(f"错误:历史数据共 {total_len} 根 Bar,不足以支持当前 Train({train_size}) + Test({test_size}) 的滚动窗口分配!") |
| |
| print(f"行情数据准备就绪:共包含 {total_len} 根 K 线。") |
| print("正在生成参数搜索网格...") |
| |
| |
| param_grid = [] |
| for mode in ["dynamic", "consensus"]: |
| for atr_mult in [1.5, 2.0, 2.5]: |
| for rsi_th in [60.0, 65.0, 70.0]: |
| param_grid.append({ |
| "strategy_mode": mode, |
| "trailing_stop_atr_mult": atr_mult, |
| "rsi_threshold_buy": rsi_th, |
| "stop_loss_pct": 0.015, |
| "profit_target_pct": 0.030 |
| }) |
| |
| risk_params = { |
| "slippage_rate": 0.0003, |
| "commission_per_share": 0.005, |
| "min_commission_per_order": 1.0, |
| "position_sizing_mode": "atr", |
| "risk_per_trade_pct": 0.01, |
| "max_position_size_pct": 0.50 |
| } |
| |
| |
| start_idx = 0 |
| oos_results = [] |
| |
| is_intraday = interval in ["1m", "5m", "15m", "30m", "1h"] |
| |
| print("\n[开始滚动优化]") |
| window_count = 1 |
| |
| while start_idx + train_size + test_size <= total_len: |
| train_df = df.iloc[start_idx : start_idx + train_size] |
| test_df = df.iloc[start_idx + train_size : start_idx + train_size + test_size] |
| |
| train_start_date = train_df.index[0].strftime("%Y-%m-%d") |
| train_end_date = train_df.index[-1].strftime("%Y-%m-%d") |
| test_start_date = test_df.index[0].strftime("%Y-%m-%d") |
| test_end_date = test_df.index[-1].strftime("%Y-%m-%d") |
| |
| |
| best_score = -999999.0 |
| best_params = None |
| |
| for params in param_grid: |
| res = run_backtest_sim(train_df, ticker, params, risk_params, is_intraday=is_intraday) |
| |
| score = res["net_pnl"] - (res["max_drawdown"] * 30000.0 * 2.0) |
| if score > best_score: |
| best_score = score |
| best_params = params |
| |
| |
| test_res = run_backtest_sim(test_df, ticker, best_params, risk_params, is_intraday=is_intraday) |
| |
| print(f" 窗口 #{window_count} | 训练集: {train_start_date} ~ {train_end_date} | 测试集: {test_start_date} ~ {test_end_date}") |
| print(f" -> 最佳参数: Mode={best_params['strategy_mode']}, ATR_Mult={best_params['trailing_stop_atr_mult']}, RSI={best_params['rsi_threshold_buy']}") |
| print(f" -> 样本外表现: 盈亏 ${test_res['net_pnl']:+,.2f} | 最大回撤 {test_res['max_drawdown']*100:.2f}% | 交易对数 {test_res['round_trips']} | 胜率 {test_res['win_rate']}%") |
| |
| oos_results.append({ |
| "window": window_count, |
| "train_period": f"{train_start_date} ~ {train_end_date}", |
| "test_period": f"{test_start_date} ~ {test_end_date}", |
| "best_params": best_params, |
| "net_pnl": test_res["net_pnl"], |
| "max_drawdown": test_res["max_drawdown"], |
| "round_trips": test_res["round_trips"], |
| "win_rate": test_res["win_rate"], |
| "commission": test_res["commission"] |
| }) |
| |
| |
| start_idx += test_size |
| window_count += 1 |
|
|
| |
| default_params = { |
| "strategy_mode": "dynamic", |
| "trailing_stop_atr_mult": 2.0, |
| "rsi_threshold_buy": 65.0, |
| "stop_loss_pct": 0.015, |
| "profit_target_pct": 0.030 |
| } |
| static_res = run_backtest_sim(df, ticker, default_params, risk_params, is_intraday=is_intraday) |
|
|
| |
| total_wf_pnl = sum(r["net_pnl"] for r in oos_results) |
| total_wf_commission = sum(r["commission"] for r in oos_results) |
| avg_wf_drawdown = float(np.mean([r["max_drawdown"] for r in oos_results])) if oos_results else 0.0 |
| total_wf_trades = sum(r["round_trips"] for r in oos_results) |
| |
| print("\n" + "=" * 80) |
| print(" WALK-FORWARD PERFORMANCE VS STATIC CONTROL") |
| print("=" * 80) |
| print(f" 【滚动参数优化 (Walk-Forward) 样本外表现】:") |
| print(f" - 累计样本外净利润: $ {total_wf_pnl:+,.2f}") |
| print(f" - 总交易笔数(对): {total_wf_trades} 笔") |
| print(f" - 佣金支出总计: $ {total_wf_commission:,.2f}") |
| print(f" - 平均滚动最大回撤: {avg_wf_drawdown*100:.2f}%") |
| print("-" * 80) |
| print(f" 【静态默认参数 (Static Default) 全样本对照组表现】:") |
| print(f" - 累计净利润: $ {static_res['net_pnl']:+,.2f} ({static_res['pnl_pct']:+.2f}%)") |
| print(f" - 总交易笔数(对): {static_res['round_trips']} 笔") |
| print(f" - 佣金支出总计: $ {static_res['commission']:,.2f}") |
| print(f" - 全历史最大回撤: {static_res['max_drawdown']*100:.2f}%") |
| print("=" * 80) |
|
|
| return { |
| "ticker": ticker, |
| "interval": interval, |
| "period": period or "1y", |
| "train_size": train_size, |
| "test_size": test_size, |
| "oos_results": oos_results, |
| "static_control": { |
| "net_pnl": static_res["net_pnl"], |
| "pnl_pct": static_res["pnl_pct"], |
| "round_trips": static_res["round_trips"], |
| "commission": static_res["commission"], |
| "max_drawdown": static_res["max_drawdown"] |
| }, |
| "summary": { |
| "total_wf_pnl": total_wf_pnl, |
| "total_wf_commission": total_wf_commission, |
| "avg_wf_drawdown": avg_wf_drawdown, |
| "total_wf_trades": total_wf_trades |
| } |
| } |
|
|
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser(description="Walk-Forward Rolling Parameter Optimizer") |
| parser.add_argument("--ticker", type=str, default="TSLA", help="测试股票代码 (默认: TSLA)") |
| parser.add_argument("--interval", type=str, default="1d", help="K线周期 (默认: 1d)") |
| parser.add_argument("--period", type=str, default="1y", help="总历史时间段 (默认: 1y)") |
| parser.add_argument("--train", type=int, default=120, help="训练集 K线根数 (默认: 120)") |
| parser.add_argument("--test", type=int, default=40, help="测试集 K线根数 (默认: 40)") |
| |
| args = parser.parse_args() |
| try: |
| walk_forward_optimization(args.ticker, args.interval, args.period, args.train, args.test) |
| except Exception as e: |
| print(e) |
| sys.exit(1) |
|
|