Files
NewQuant/futures_trading_strategies/ru/Spectral/search_SpectralTrendStrategy.ipynb

463 lines
109 KiB
Plaintext
Raw Normal View History

2025-11-29 16:35:02 +08:00
{
"cells": [
{
"cell_type": "code",
"id": "522f09ca7b3fe929",
"metadata": {
"ExecuteTime": {
"end_time": "2025-11-26T19:15:23.597932Z",
"start_time": "2025-11-26T19:15:23.579880Z"
}
},
"source": [
"import sys\n",
"\n",
"if '/mnt/d/PyProject/NewQuant/' not in sys.path:\n",
" sys.path.append('/mnt/d/PyProject/NewQuant/')\n",
"\n",
"%load_ext autoreload\n",
"%autoreload 2\n",
"\n"
],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"The autoreload extension is already loaded. To reload it, use:\n",
" %reload_ext autoreload\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"[autoreload of src.indicators.indicators failed: Traceback (most recent call last):\n",
" File \"D:\\Python\\conda\\envs\\quant\\Lib\\site-packages\\IPython\\extensions\\autoreload.py\", line 322, in check\n",
" elif self.deduper_reloader.maybe_reload_module(m):\n",
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
" File \"D:\\Python\\conda\\envs\\quant\\Lib\\site-packages\\IPython\\extensions\\deduperreload\\deduperreload.py\", line 545, in maybe_reload_module\n",
" new_source_code = f.read()\n",
" ^^^^^^^^\n",
"UnicodeDecodeError: 'gbk' codec can't decode byte 0xaf in position 775: illegal multibyte sequence\n",
"]\n"
]
}
],
"execution_count": 21
},
{
"cell_type": "code",
"id": "4f7e4b438cea750e",
"metadata": {
"ExecuteTime": {
"end_time": "2025-11-26T19:15:23.614735Z",
"start_time": "2025-11-26T19:15:23.600936Z"
}
},
"source": [
"import pandas as pd\n",
"from datetime import datetime\n",
"import itertools\n",
"from typing import Dict, Any, List, Tuple, Optional\n",
"import multiprocessing # 导入 multiprocessing 模块\n",
"import math # 保留 math 导入,因为您的策略内部可能需要用到数学函数\n",
"\n",
"# 导入所有必要的模块\n",
"# 请确保这些导入路径与您的项目结构相符\n",
"from src.analysis.grid_search_analyzer import GridSearchAnalyzer\n",
"from src.analysis.result_analyzer import ResultAnalyzer\n",
"from src.common_utils import generate_parameter_range\n",
"from src.data_manager import DataManager\n",
"from src.backtest_engine import BacktestEngine\n",
"# 导入策略类\n",
"\n",
"import builtins\n",
"\n",
"\n",
"origin_print = print\n",
"\n"
],
"outputs": [],
"execution_count": 22
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-11-26T19:15:23.631058Z",
"start_time": "2025-11-26T19:15:23.614735Z"
}
},
"cell_type": "code",
"source": [
"\n",
"def slient_print(*args):\n",
" pass\n"
],
"id": "f903fd2761d446cd",
"outputs": [],
"execution_count": 23
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-11-26T19:17:55.871507Z",
"start_time": "2025-11-26T19:15:23.631058Z"
}
},
"cell_type": "code",
"source": [
"from src.strategies.Spectral.utils import run_single_backtest\n",
"# --- 主执行块 ---\n",
"# 这是多进程代码的入口点,必须在 'if __name__ == \"__main__\":' 保护块中\n",
"# 确保 autoreload 启用 (在Jupyter Notebook中使用纯Python脚本运行时可移除)\n",
"# %load_ext autoreload\n",
"# %autoreload 2\n",
"\n",
"from src.strategies.Spectral.SpectralTrendStrategy2 import SpectralTrendStrategy\n",
"\n",
"# --- 全局配置 ---\n",
"data_file_path = \"D:/PyProject/NewQuant/data/data/KQ_m@SHFE_rb/KQ_m@SHFE_rb_min15.csv\"\n",
"\n",
"\n",
"initial_capital = 100000.0\n",
"slippage_rate = 0.0000\n",
"commission_rate = 0.0000\n",
"global_config = {\n",
" 'symbol': 'rb',\n",
"}\n",
"# 确保每个合约的tick_size在这里定义或获取\n",
"RB_TICK_SIZE = 1.0 # 螺纹钢的最小变动单位\n",
"\n",
"# --- 定义参数网格 ---\n",
"param1_name = \"spectral_window_days\"\n",
"param1_values = generate_parameter_range(start=2, end=12, step=1)\n",
"# param1_name = \"exit_threshold\"\n",
"# param1_values = generate_parameter_range(start=0.1, end=0.9, step=0.1)\n",
"param2_name = \"trend_strength_threshold\"\n",
"param2_values = generate_parameter_range(start=0.1, end=0.9, step=0.1)\n",
"# param2_name = \"dominance_multiplier\"\n",
"# param2_values = generate_parameter_range(start=0, end=5, step=0.5)\n",
"optimization_metric = 'total_return'\n",
"\n",
"# 生成所有参数组合\n",
"param_combinations = list(itertools.product(param1_values, param2_values))\n",
"total_combinations = len(param_combinations)\n",
"print(f\"总计 {total_combinations} 种参数组合需要回测。\")\n",
"\n",
"all_results: List[Dict[str, Any]] = []\n",
"grid_results: List[Dict[str, Any]] = []\n",
"\n",
"# 准备传递给每个子进程的公共配置字典\n",
"common_config_for_processes = {\n",
" 'main_symbol': global_config['symbol'],\n",
" 'symbol': global_config['symbol'],\n",
" 'data_path': data_file_path,\n",
" 'initial_capital': initial_capital,\n",
" 'slippage_rate': slippage_rate,\n",
" 'commission_rate': commission_rate,\n",
" 'start_time': datetime(2022, 1, 1), # 回测起始时间\n",
" 'end_time': datetime(2024, 6, 1), # 回测结束时间\n",
" 'roll_over_mode': True, # 保持换月模式\n",
" 'param1_name': param1_name,\n",
" 'param2_name': param2_name,\n",
" 'optimization_metric': optimization_metric,\n",
" 'strategy': SpectralTrendStrategy,\n",
" 'order_direction': ['BUY', 'SELL'],\n",
" # 'hawkes_entry_percent': 0.9,\n",
" 'stop_loss_tick': 5\n",
"}\n",
"\n",
"# 确定要使用的进程数量 (通常是CPU核心数)\n",
"num_processes = int(multiprocessing.cpu_count() * 0.6)\n",
"if num_processes < 1:\n",
" num_processes = 1\n",
"\n",
"print(f\"--- 启动多进程网格搜索,使用 {num_processes} 个进程 ---\")\n",
"\n",
"builtins.print = slient_print\n",
"\n",
"# 创建一个进程池\n",
"with multiprocessing.Pool(processes=num_processes) as pool:\n",
" # 准备 run_single_backtest 函数的参数列表\n",
" # starmap 需要一个可迭代对象,其中每个元素是传递给目标函数的参数元组\n",
" args_for_starmap = [\n",
" (combo, common_config_for_processes) for combo in param_combinations\n",
" ]\n",
"\n",
" run_single_backtest(*args_for_starmap[0])\n",
"\n",
" # 使用 starmap() 来并行执行 run_single_backtest 函数\n",
" # starmap 是阻塞的,会等待所有任务完成并返回结果列表\n",
" for i, result_entry in enumerate(pool.starmap(run_single_backtest, args_for_starmap)):\n",
" if result_entry: # 确保结果不为空\n",
" all_results.append(result_entry)\n",
" # 仅将成功的(无错误的)结果添加到用于网格分析的列表中\n",
" if 'error' not in result_entry:\n",
" grid_results.append(\n",
" {\n",
" param1_name: result_entry.get(param1_name),\n",
" param2_name: result_entry.get(param2_name),\n",
" optimization_metric: result_entry.get(optimization_metric, 0.0),\n",
" }\n",
" )\n",
" else:\n",
" # 对于失败的组合,将其优化指标设置为一个特殊值,便于识别\n",
" grid_results.append(\n",
" {\n",
" param1_name: result_entry.get(param1_name),\n",
" param2_name: result_entry.get(param2_name),\n",
" optimization_metric: float('-inf'), # 用负无穷表示失败\n",
" 'error_message': result_entry['error']\n",
" }\n",
" )\n",
"\n",
"builtins.print = origin_print\n",
"print(\"\\n--- 网格搜索回测完毕 ---\")"
],
"id": "aed5938660e42b20",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"总计 99 种参数组合需要回测。\n",
"--- 启动多进程网格搜索,使用 12 个进程 ---\n",
"\n",
"--- 网格搜索回测完毕 ---\n"
]
}
],
"execution_count": 24
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-11-26T19:17:56.065022Z",
"start_time": "2025-11-26T19:17:55.882808Z"
}
},
"cell_type": "code",
"source": [
"\n",
"# --- 5. 后处理和最佳结果选择 ---\n",
"if all_results:\n",
" results_df = pd.DataFrame(all_results)\n",
" # print(\"\\n--- 所有回测结果汇总 ---\")\n",
" # # 确保打印时浮点数格式化\n",
" # pd.set_option('display.float_format', lambda x: '%.4f' % x)\n",
" # print(results_df.to_string())\n",
"\n",
" # 找到最佳组合 (排除有错误的)\n",
" # 过滤掉包含 'error' 键的行,或者 'error' 键的值不为空的行\n",
" # 同时确保优化指标是数值,并且不为无穷大\n",
" print(results_df.info())\n",
" successful_results_df = results_df[(pd.to_numeric(results_df[optimization_metric], errors='coerce').notna()) &\n",
" (pd.to_numeric(results_df[optimization_metric], errors='coerce') != float(\n",
" '-inf'))\n",
" ].copy() # 使用 .copy() 避免 SettingWithCopyWarning\n",
"\n",
" if not successful_results_df.empty and optimization_metric in successful_results_df.columns:\n",
" # 确保优化指标列是数值类型\n",
" successful_results_df[optimization_metric] = pd.to_numeric(successful_results_df[optimization_metric],\n",
" errors='coerce')\n",
"\n",
" if not successful_results_df.empty and optimization_metric in successful_results_df.columns:\n",
" # 过滤掉NaN值如果所有夏普比率都是NaN则可能没有有效结果\n",
" normal_results = successful_results_df[\n",
" (results_df['total_trades'] > 100)\n",
" # &\n",
" # (results_df['profit_factor'] < 3.)\n",
" ]\n",
" if len(normal_results) > 0:\n",
" best_result = normal_results.loc[(normal_results[optimization_metric].idxmax())]\n",
" print(f\"\\n--- 最优参数组合 (按{optimization_metric}) ---\")\n",
" print(best_result)\n",
" else:\n",
" print('ERROR!!!!!!!!!!!!!!!!!!!!')\n",
"\n",
" # 找到最大值的索引\n",
" # best_result = successful_results_df.loc[successful_results_df[optimization_metric].idxmax()]\n",
" # print(f\"\\n--- 最优参数组合 (按 {optimization_metric}) ---\")\n",
" # print(best_result)\n",
"\n",
" # 导出到CSV\n",
" output_filename = f\"grid_search_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv\"\n",
" # results_df.to_csv(output_filename, index=False, encoding='utf-8')\n",
" # print(f\"\\n所有结果已导出到: {output_filename}\")\n",
"\n",
" # 打印枢轴表\n",
" grid_df = pd.DataFrame(grid_results)\n",
" # 确保优化指标列是数值类型,非数值的(如 -inf在pandas中可能被正确处理\n",
" grid_df[optimization_metric] = pd.to_numeric(grid_df[optimization_metric], errors='coerce')\n",
"\n",
" pivot_table = grid_df.pivot_table(\n",
" index=param1_name, columns=param2_name, values=optimization_metric\n",
" )\n",
" # print(f\"\\n{optimization_metric} 网格结果 (Pivoted):\")\n",
" # print(pivot_table.to_string())\n",
" else:\n",
" print(f\"\\n没有成功的组合结果可供分析或优化指标 '{optimization_metric}' 不在结果中,或所有组合均失败。\")\n",
"else:\n",
" print(\"没有可用的回测结果。\")\n",
"print(\"\\n--- 动态网格搜索完成 ---\")\n",
"\n",
"# --- 6. 可视化 (依赖 GridSearchAnalyzer) ---\n",
"if grid_results:\n",
" grid_analyzer = GridSearchAnalyzer(grid_results, optimization_metric)\n",
" grid_analyzer.find_best_parameters() # 这会找到并打印最佳参数\n",
" grid_analyzer.plot_heatmap() # 这会绘制热力图\n",
"else:\n",
" print(\"\\n没有生成任何网格搜索结果无法进行分析。\")"
],
"id": "a1c18a2776fcaba2",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<class 'pandas.core.frame.DataFrame'>\n",
"RangeIndex: 99 entries, 0 to 98\n",
"Data columns (total 42 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 main_symbol 99 non-null object \n",
" 1 trade_volume 99 non-null int64 \n",
" 2 spectral_window_days 99 non-null int64 \n",
" 3 trend_strength_threshold 99 non-null float64\n",
" 4 order_direction 99 non-null object \n",
" 5 enable_log 99 non-null bool \n",
" 6 low_freq_days 99 non-null int64 \n",
" 7 high_freq_days 99 non-null int64 \n",
" 8 exit_threshold 99 non-null float64\n",
" 9 初始资金 99 non-null float64\n",
" 10 最终资金 99 non-null float64\n",
" 11 总收益率 99 non-null float64\n",
" 12 年化收益率 99 non-null float64\n",
" 13 最大回撤 99 non-null float64\n",
" 14 夏普比率 99 non-null float64\n",
" 15 卡玛比率 99 non-null float64\n",
" 16 总交易次数 99 non-null int64 \n",
" 17 交易成本 99 non-null float64\n",
" 18 总实现盈亏 99 non-null float64\n",
" 19 胜率 99 non-null float64\n",
" 20 盈亏比 99 non-null float64\n",
" 21 盈利交易次数 99 non-null int64 \n",
" 22 亏损交易次数 99 non-null int64 \n",
" 23 平均每次盈利 99 non-null float64\n",
" 24 平均每次亏损 99 non-null float64\n",
" 25 initial_capital 99 non-null float64\n",
" 26 final_capital 99 non-null float64\n",
" 27 total_return 99 non-null float64\n",
" 28 annualized_return 99 non-null float64\n",
" 29 max_drawdown 99 non-null float64\n",
" 30 sharpe_ratio 99 non-null float64\n",
" 31 calmar_ratio 99 non-null float64\n",
" 32 sortino_ratio 99 non-null float64\n",
" 33 total_trades 99 non-null int64 \n",
" 34 transaction_costs 99 non-null float64\n",
" 35 total_realized_pnl 99 non-null float64\n",
" 36 win_rate 99 non-null float64\n",
" 37 profit_loss_ratio 99 non-null float64\n",
" 38 winning_trades_count 99 non-null int64 \n",
" 39 losing_trades_count 99 non-null int64 \n",
" 40 avg_profit_per_trade 99 non-null float64\n",
" 41 avg_loss_per_trade 99 non-null float64\n",
"dtypes: bool(1), float64(29), int64(10), object(2)\n",
"memory usage: 31.9+ KB\n",
"None\n",
"\n",
"--- 最优参数组合 (按total_return) ---\n",
"main_symbol rb\n",
"trade_volume 1\n",
"spectral_window_days 9\n",
"trend_strength_threshold 0.8\n",
"order_direction [BUY, SELL]\n",
"enable_log False\n",
"low_freq_days 9\n",
"high_freq_days 4\n",
"exit_threshold 0.5\n",
"初始资金 100000.0\n",
"最终资金 100917.0\n",
"总收益率 0.00917\n",
"年化收益率 0.002623\n",
"最大回撤 0.005553\n",
"夏普比率 0.138434\n",
"卡玛比率 0.472418\n",
"总交易次数 104\n",
"交易成本 0.0\n",
"总实现盈亏 916.0\n",
"胜率 0.576923\n",
"盈亏比 1.095259\n",
"盈利交易次数 30\n",
"亏损交易次数 22\n",
"平均每次盈利 92.4\n",
"平均每次亏损 -84.363636\n",
"initial_capital 100000.0\n",
"final_capital 100917.0\n",
"total_return 0.00917\n",
"annualized_return 0.002623\n",
"max_drawdown 0.005553\n",
"sharpe_ratio 0.138434\n",
"calmar_ratio 0.472418\n",
"sortino_ratio 0.142283\n",
"total_trades 104\n",
"transaction_costs 0.0\n",
"total_realized_pnl 916.0\n",
"win_rate 0.576923\n",
"profit_loss_ratio 1.095259\n",
"winning_trades_count 30\n",
"losing_trades_count 22\n",
"avg_profit_per_trade 92.4\n",
"avg_loss_per_trade -84.363636\n",
"Name: 70, dtype: object\n",
"\n",
"--- 动态网格搜索完成 ---\n",
"\n",
"--- 最佳参数组合 ---\n",
" spectral_window_days: 9\n",
" trend_strength_threshold: 0.8\n",
" total_return: 0.0092\n",
"[2, 12, 0.1, 0.9]\n"
]
},
{
"data": {
"text/plain": [
"<Figure size 1000x800 with 2 Axes>"
],
"image/png": "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
},
"metadata": {},
"output_type": "display_data",
"jetTransient": {
"display_id": null
}
}
],
"execution_count": 25
}
],
"metadata": {
"kernelspec": {
"display_name": "quant",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}