Files
NewQuant/grid_search_multi_process2.ipynb

513 lines
829 KiB
Plaintext
Raw Normal View History

2025-07-10 15:07:31 +08:00
{
"cells": [
{
"cell_type": "code",
"id": "782ec73f",
"metadata": {
"ExecuteTime": {
2025-07-15 22:45:51 +08:00
"end_time": "2025-07-15T02:34:56.677192Z",
"start_time": "2025-07-15T02:34:56.154989Z"
2025-07-10 15:07:31 +08:00
}
},
"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",
"from src.strategies.OpenTwoFactorStrategy import SimpleLimitBuyStrategyLong, SimpleLimitBuyStrategyShort, \\\n",
" SimpleLimitBuyStrategy\n",
"\n",
"import builtins\n",
"\n",
"%load_ext autoreload\n",
"%autoreload 2\n",
"\n",
"origin_print = print\n"
2025-07-15 22:45:51 +08:00
],
"outputs": [],
"execution_count": 1
2025-07-10 15:07:31 +08:00
},
{
"cell_type": "code",
"id": "76f9a2e9",
"metadata": {
"ExecuteTime": {
2025-07-15 22:45:51 +08:00
"end_time": "2025-07-15T02:34:56.759912Z",
"start_time": "2025-07-15T02:34:56.736553Z"
2025-07-10 15:07:31 +08:00
}
},
"source": [
"\n",
"# --- 单个回测任务函数 ---\n",
"# 这个函数将在每个独立的进程中运行,因此它必须是自包含的\n",
"def run_single_backtest(\n",
" combination: Tuple[float, float], # 传入当前参数组合\n",
" common_config: Dict[str, Any] # 传入公共配置 (如数据路径, 初始资金等)\n",
") -> Optional[Dict[str, Any]]:\n",
" \"\"\"\n",
" 运行单个参数组合的回测任务。\n",
" 此函数将在一个独立的进程中执行。\n",
" \"\"\"\n",
" p1_value, p2_value = combination\n",
"\n",
" # 从 common_config 中获取必要的配置\n",
" symbol = common_config['symbol']\n",
" data_path = common_config['data_path']\n",
" initial_capital = common_config['initial_capital']\n",
" slippage_rate = common_config['slippage_rate']\n",
" commission_rate = common_config['commission_rate']\n",
" start_time = common_config['start_time']\n",
" end_time = common_config['end_time']\n",
" roll_over_mode = common_config['roll_over_mode']\n",
" # bar_duration_seconds = common_config['bar_duration_seconds'] # 如果DataManager需要可以再传\n",
" param1_name = common_config['param1_name']\n",
" param2_name = common_config['param2_name']\n",
"\n",
" # 每个进程内部独立初始化 DataManager 和 BacktestEngine\n",
" # 确保每个进程有自己的数据副本和模拟状态\n",
" data_manager = DataManager(\n",
" file_path=data_path,\n",
" symbol=symbol,\n",
" # bar_duration_seconds=bar_duration_seconds, # 如果DataManager需要根据数据文件路径推断或者额外参数传入\n",
" # start_date=start_time.date(), # DataManager 现在通过 file_path 和 symbol 处理数据\n",
" # end_date=end_time.date(),\n",
" )\n",
" # data_manager.load_data() # DataManager 内部加载数据\n",
"\n",
" # 策略参数\n",
" strategy_parameters = {\n",
" 'trade_volume': 1,\n",
" param1_name: p1_value,\n",
" param2_name: p2_value,\n",
" 'max_position': 20,\n",
" 'enable_log': False, # 在网格搜索时通常关闭策略内部的详细日志\n",
2025-07-15 22:45:51 +08:00
" 'stop_loss_points': 20,\n",
" 'lag': common_config['lag']\n",
2025-07-10 15:07:31 +08:00
" }\n",
"\n",
" # 打印当前进程正在处理的组合信息\n",
" # 注意:多进程打印会交错显示\n",
" # print(f\"--- 正在运行组合: {strategy_parameters} (PID: {multiprocessing.current_process().pid}) ---\")\n",
"\n",
" try:\n",
" # 初始化回测引擎\n",
" engine = BacktestEngine(\n",
" data_manager=data_manager,\n",
" strategy_class=common_config['strategy'],\n",
" strategy_params=strategy_parameters,\n",
" initial_capital=initial_capital,\n",
" slippage_rate=slippage_rate,\n",
" commission_rate=commission_rate,\n",
" roll_over_mode=True, # 保持换月模式\n",
" start_time=common_config['start_time'],\n",
" end_time=common_config['end_time']\n",
" )\n",
" # 运行回测,传入时间范围\n",
" engine.run_backtest()\n",
"\n",
" # 获取回测结果并分析\n",
" results = engine.get_backtest_results()\n",
" portfolio_snapshots = results[\"portfolio_snapshots\"]\n",
" trade_history = results[\"trade_history\"]\n",
" bars = results[\"all_bars\"]\n",
" initial_capital_result = results[\"initial_capital\"]\n",
"\n",
" if portfolio_snapshots:\n",
" analyzer = ResultAnalyzer(portfolio_snapshots, trade_history, bars, initial_capital_result)\n",
"\n",
" # analyzer.generate_report()\n",
" # analyzer.plot_performance()\n",
" metrics = analyzer.calculate_all_metrics()\n",
"\n",
" # 将当前组合的参数和性能指标存储起来\n",
" result_entry = {**strategy_parameters, **metrics}\n",
" return result_entry\n",
" else:\n",
" print(\n",
" f\" 组合 {strategy_parameters} 没有生成投资组合快照,无法进行结果分析。(PID: {multiprocessing.current_process().pid})\")\n",
" # 返回一个包含参数和默认0值的结果以便追踪失败组合\n",
" return {**strategy_parameters, \"total_return\": 0.0, \"annualized_return\": 0.0, \"sharpe_ratio\": 0.0,\n",
" \"max_drawdown\": 0.0, \"error\": \"No portfolio snapshots\"}\n",
" except Exception as e:\n",
" import traceback\n",
" error_trace = traceback.format_exc()\n",
" print(\n",
" f\" 组合 {strategy_parameters} 运行失败: {e}\\n{error_trace} (PID: {multiprocessing.current_process().pid})\")\n",
" # 返回错误信息,以便后续处理\n",
" return {**strategy_parameters, \"error\": str(e), \"traceback\": error_trace}\n",
"\n"
2025-07-15 22:45:51 +08:00
],
"outputs": [],
"execution_count": 2
2025-07-10 15:07:31 +08:00
},
{
"cell_type": "code",
"id": "c0984689",
"metadata": {
"ExecuteTime": {
2025-07-15 22:45:51 +08:00
"end_time": "2025-07-15T02:34:56.786748Z",
"start_time": "2025-07-15T02:34:56.769648Z"
2025-07-10 15:07:31 +08:00
}
},
"source": [
"\n",
"def slient_print(*args):\n",
" pass\n"
2025-07-15 22:45:51 +08:00
],
"outputs": [],
"execution_count": 3
2025-07-10 15:07:31 +08:00
},
{
"cell_type": "code",
"id": "8b6d9f4cd97a863d",
"metadata": {
"ExecuteTime": {
2025-07-15 22:45:51 +08:00
"end_time": "2025-07-15T02:38:32.614377Z",
"start_time": "2025-07-15T02:34:56.794605Z"
2025-07-10 15:07:31 +08:00
}
},
"source": [
"\n",
"# --- 主执行块 ---\n",
"# 这是多进程代码的入口点,必须在 'if __name__ == \"__main__\":' 保护块中\n",
"# 确保 autoreload 启用 (在Jupyter Notebook中使用纯Python脚本运行时可移除)\n",
"# %load_ext autoreload\n",
"# %autoreload 2\n",
"\n",
"# --- 全局配置 ---\n",
2025-07-15 22:45:51 +08:00
"# data_file_path = \"/mnt/d/PyProject/NewQuant/data/data/KQ_m@CZCE_MA/KQ_m@CZCE_MA_min60.csv\"\n",
"# data_file_path = \"/mnt/d/PyProject/NewQuant/data/data/KQ_m@SHFE_rb/KQ_m@SHFE_rb_min60.csv\"\n",
"data_file_path = \"/mnt/d/PyProject/NewQuant/data/data/KQ_m@DCE_jm/KQ_m@DCE_jm_min60.csv\"\n",
"\n",
2025-07-10 15:07:31 +08:00
"initial_capital = 100000.0\n",
"slippage_rate = 0.0000\n",
"commission_rate = 0.0001\n",
"global_config = {\n",
2025-07-15 22:45:51 +08:00
" 'symbol': 'KQ_m@DCE_jm_min60',\n",
2025-07-10 15:07:31 +08:00
"}\n",
"# 确保每个合约的tick_size在这里定义或获取\n",
"RB_TICK_SIZE = 1.0 # 螺纹钢的最小变动单位\n",
"\n",
"# --- 定义参数网格 ---\n",
"param1_name = \"range_factor\"\n",
"param1_values = generate_parameter_range(start=0, end=3, step=0.1)\n",
"param2_name = \"profit_factor\"\n",
2025-07-15 22:45:51 +08:00
"param2_values = generate_parameter_range(start=0, end=5, step=0.1)\n",
2025-07-10 15:07:31 +08:00
"optimization_metric = 'sharpe_ratio'\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",
" '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",
2025-07-15 22:45:51 +08:00
" 'start_time': datetime(2021, 1, 1), # 回测起始时间\n",
2025-07-10 15:07:31 +08:00
" '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",
2025-07-15 22:45:51 +08:00
" 'strategy': SimpleLimitBuyStrategyShort,\n",
" 'lag': 7,\n",
2025-07-10 15:07:31 +08:00
"}\n",
"\n",
"# 确定要使用的进程数量 (通常是CPU核心数)\n",
"num_processes = int(multiprocessing.cpu_count() * 0.75)\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",
" # 使用 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--- 网格搜索回测完毕 ---\")"
2025-07-15 22:45:51 +08:00
],
2025-07-10 15:07:31 +08:00
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
2025-07-15 22:45:51 +08:00
"总计 1581 种参数组合需要回测。\n",
"--- 启动多进程网格搜索,使用 15 个进程 ---\n",
2025-07-10 15:07:31 +08:00
"\n",
2025-07-15 22:45:51 +08:00
"--- 网格搜索回测完毕 ---\n"
2025-07-10 15:07:31 +08:00
]
}
],
2025-07-15 22:45:51 +08:00
"execution_count": 4
},
{
"cell_type": "code",
"id": "239e9ca0",
"metadata": {
"ExecuteTime": {
"end_time": "2025-07-15T02:39:41.073491Z",
"start_time": "2025-07-15T02:39:40.133994Z"
}
},
2025-07-10 15:07:31 +08:00
"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",
2025-07-15 22:45:51 +08:00
" (results_df['total_trades'] > 200)\n",
" # (results_df['range_factor'] > 1)\n",
" ]\n",
2025-07-10 15:07:31 +08:00
" if len(normal_results) > 0:\n",
" best_result = normal_results.loc[(normal_results[optimization_metric].idxmax())]\n",
2025-07-15 22:45:51 +08:00
" print(f\"\\n--- 最优参数组合 (按{optimization_metric}) ---\")\n",
2025-07-10 15:07:31 +08:00
" 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",
2025-07-15 22:45:51 +08:00
" # print(f\"\\n{optimization_metric} 网格结果 (Pivoted):\")\n",
" # print(pivot_table.to_string())\n",
2025-07-10 15:07:31 +08:00
" 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没有生成任何网格搜索结果无法进行分析。\")"
2025-07-15 22:45:51 +08:00
],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<class 'pandas.core.frame.DataFrame'>\n",
"RangeIndex: 1581 entries, 0 to 1580\n",
"Data columns (total 39 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 trade_volume 1581 non-null int64 \n",
" 1 range_factor 1581 non-null float64\n",
" 2 profit_factor 1581 non-null float64\n",
" 3 max_position 1581 non-null int64 \n",
" 4 enable_log 1581 non-null bool \n",
" 5 stop_loss_points 1581 non-null int64 \n",
" 6 lag 1581 non-null int64 \n",
" 7 初始资金 1581 non-null float64\n",
" 8 最终资金 1581 non-null float64\n",
" 9 总收益率 1581 non-null float64\n",
" 10 年化收益率 1581 non-null float64\n",
" 11 最大回撤 1581 non-null float64\n",
" 12 夏普比率 1581 non-null float64\n",
" 13 卡玛比率 1581 non-null float64\n",
" 14 总交易次数 1581 non-null int64 \n",
" 15 交易成本 1581 non-null float64\n",
" 16 总实现盈亏 1581 non-null float64\n",
" 17 胜率 1581 non-null float64\n",
" 18 盈亏比 1581 non-null float64\n",
" 19 盈利交易次数 1581 non-null int64 \n",
" 20 亏损交易次数 1581 non-null int64 \n",
" 21 平均每次盈利 1581 non-null float64\n",
" 22 平均每次亏损 1581 non-null float64\n",
" 23 initial_capital 1581 non-null float64\n",
" 24 final_capital 1581 non-null float64\n",
" 25 total_return 1581 non-null float64\n",
" 26 annualized_return 1581 non-null float64\n",
" 27 max_drawdown 1581 non-null float64\n",
" 28 sharpe_ratio 1581 non-null float64\n",
" 29 calmar_ratio 1581 non-null float64\n",
" 30 total_trades 1581 non-null int64 \n",
" 31 transaction_costs 1581 non-null float64\n",
" 32 total_realized_pnl 1581 non-null float64\n",
" 33 win_rate 1581 non-null float64\n",
" 34 profit_loss_ratio 1581 non-null float64\n",
" 35 winning_trades_count 1581 non-null int64 \n",
" 36 losing_trades_count 1581 non-null int64 \n",
" 37 avg_profit_per_trade 1581 non-null float64\n",
" 38 avg_loss_per_trade 1581 non-null float64\n",
"dtypes: bool(1), float64(28), int64(10)\n",
"memory usage: 471.0 KB\n",
"None\n",
"\n",
"--- 最优参数组合 (按sharpe_ratio) ---\n",
"trade_volume 1\n",
"range_factor 0.8\n",
"profit_factor 4.7\n",
"max_position 20\n",
"enable_log False\n",
"stop_loss_points 20\n",
"lag 7\n",
"初始资金 100000.0\n",
"最终资金 101755.4085\n",
"总收益率 0.017554\n",
"年化收益率 0.003534\n",
"最大回撤 0.027357\n",
"夏普比率 0.150843\n",
"卡玛比率 0.129186\n",
"总交易次数 814\n",
"交易成本 170.5915\n",
"总实现盈亏 963.0\n",
"胜率 0.275862\n",
"盈亏比 2.869452\n",
"盈利交易次数 112\n",
"亏损交易次数 294\n",
"平均每次盈利 100.928571\n",
"平均每次亏损 -35.173469\n",
"initial_capital 100000.0\n",
"final_capital 101755.4085\n",
"total_return 0.017554\n",
"annualized_return 0.003534\n",
"max_drawdown 0.027357\n",
"sharpe_ratio 0.150843\n",
"calmar_ratio 0.129186\n",
"total_trades 814\n",
"transaction_costs 170.5915\n",
"total_realized_pnl 963.0\n",
"win_rate 0.275862\n",
"profit_loss_ratio 2.869452\n",
"winning_trades_count 112\n",
"losing_trades_count 294\n",
"avg_profit_per_trade 100.928571\n",
"avg_loss_per_trade -35.173469\n",
"Name: 455, dtype: object\n",
"\n",
"--- 动态网格搜索完成 ---\n",
"\n",
"--- 最佳参数组合 ---\n",
" range_factor: 0.8\n",
" profit_factor: 4.7\n",
" sharpe_ratio: 0.1508\n",
"[0, 3.0, 0, 5.0]\n"
]
},
{
"data": {
"text/plain": [
"<Figure size 1000x800 with 2 Axes>"
],
"image/png": "iVBORw0KGgoAAAANSUhEUgAAA7YAAAMWCAYAAADF5hp2AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsnXd8VMX3sJ/tu9mSTe+VGjrSkQ4SilJEugoqiopdLGDDitgrip2vna70ovSi9BZCQgmQhPS2u8n2+/6xySbLhqYg+nvv44eP2Zm558w5987cO3fmzpEIgiAgIiIiIiIiIiIiIiIiIvIfRXqtKyAiIiIiIiIiIiIiIiIi8ncQB7YiIiIiIiIiIiIiIiIi/2nEga2IiIiIiIiIiIiIiIjIfxpxYCsiIiIiIiIiIiIiIiLyn0Yc2IqIiIiIiIiIiIiIiIj8pxEHtiIiIiIiIiIiIiIiIiL/acSBrYiIiIiIiIiIiIiIiMh/GnFgKyIiIiIiIiIiIiIiIvKfRhzYioiIiIiIiIiIiIiIiPynEQe2IiKXyIwZM5BIJBQVFV3rqvx/hej3v8eGDRuQSCRs2LDhWldF5CKsWrWKNm3aoFarkUgklJWVXesq/X/Lzp076dq1K1qtFolEwr59+7x90ZXCbDYzadIkIiMjkUgkPPLII1dMtoiIiMj/j4gDWxGR/wArVqxgxowZ17oaIv9iZs+ezTfffHOtqyHyFykuLmbUqFFoNBo+/vhjvv32W7Ra7RXVsW3bNmbMmPGPD5jdbjdvvfUWjRo1QqPR0KBBA+677z7MZvM/Wo9LxeFwMHLkSEpKSnj33Xf59ttvSUhIqLfsa6+9xpIlS/6Sntdee41vvvmG++67j2+//Zbbbrvtb9T6/Dr+av1ERERE/mtIBEEQrnUlRET+C8yYMYMXX3yRwsJCQkND/1HdDzzwAB9//DH/PzbXa+n3/xItWrQgNDTUb2bW7XZjt9tRKpVIpeK7zH8rq1atYuDAgaxdu5Z+/fpdFR1vvfUWTzzxBCdPniQxMfGq6KiPd999l8cee4xhw4YxcOBATp06xY8//sjvv//+j9bjUklPTyclJYXPP/+cSZMmedOdTidOpxO1Wu1N0+l03HLLLX/ppVLnzp2Ry+Vs2bLlSlS7Xv5O/URERET+a8ivdQVERERErjWCIGC1WtFoNNe6KsCVrY9UKvV5EP+/gtVq/T81WC8oKADAaDRe24pcJpdyrf700080b96cRYsWeZfyvvzyy7jd7n+kjk6nE7fbjVKpvKTy5zsXcrkcufzKPTYVFBTQrFmzKybvn8JisVzx1QQiIiIiV4L/G08EIiL/IGVlZUycOBGj0UhgYCB33HEHlZWVfuW+++472rVrh0ajITg4mDFjxnDmzBmfMps3b2bkyJHEx8ejUqmIi4vj0Ucfpaqqyltm4sSJfPzxxwBIJBLvP4CsrCwkEglvvfUWH3/8McnJyQQEBNC/f3/OnDmDIAi8/PLLxMbGotFoGDp0KCUlJT51+OWXXxg8eDDR0dGoVCoaNGjAyy+/jMvl8inXq1cvWrRowe7du+natSsajYakpCQ+/fRTP9tPnz5Nenr6Jfnzww8/pHnz5gQEBBAUFET79u354Ycf/pLfv/76a/r06UN4eDgqlYpmzZrxySef+MlKTEzkxhtvZPXq1bRv3x6NRsOcOXO8Pn7ggQf4/vvvadKkCWq1mnbt2rFp0yY/OTk5Odx5551ERESgUqlo3rw5X3311SXZfan1uRSbEhMTOXz4MBs3bvReH7169QLO/43t/PnzvddnaGgot956Kzk5ORes565du5BIJMydO9cvb/Xq1UgkEpYtWwaAyWTikUceITExEZVKRXh4ODfccAN79uy5bP/U2PDTTz/x7LPPEhMTQ0BAABUVFZSUlDB16lRatmyJTqfDYDAwcOBA9u/fX6+MefPm8eqrrxIbG4taraZv374cO3bMT2dNe9JoNHTs2JHNmzfTq1cvr19rsNlsvPDCCzRs2NDbhp988klsNtsl29erVy8mTJgAQIcOHZBIJEycOBG4tD6ihvT0dEaNGkVYWBgajYYmTZrwzDPPAJ6VD0888QQASUlJ3uskKysL8Az+Xn75ZRo0aIBKpSIxMZHp06f72XGha/V8SKVS3G63z/epUqn0sgaJEydORKfTceLECVJTU9FqtURHR/PSSy/5rGSp2ye+9957XnvS0tIA+P333+nevTtarRaj0cjQoUM5cuSIj56ePXsCMHLkSJ+2dO43thKJBIvFwty5c73+rDlvF6LmWjx58iTLly/3ORd2u53nn3+edu3aERgYiFarpXv37qxfv95Pjtvt5v3336dly5ao1WrCwsIYMGAAu3btuqT67d27l4EDB2IwGNDpdPTt25cdO3b46Pjmm2+QSCRs3LiR+++/n/DwcGJjYy9qo4iIiMi1QJyxFRG5TEaNGkVSUhIzZ85kz549fPHFF4SHhzNr1ixvmVdffZXnnnuOUaNGMWnSJAoLC/nwww/p0aMHe/fu9c4EzJ8/n8rKSu677z5CQkL4888/+fDDD8nOzmb+/PkATJ48mdzcXNauXcu3335bb52+//577HY7Dz74ICUlJbzxxhuMGjWKPn36sGHDBp566imOHTvGhx9+yNSpU30GX9988w06nY7HHnsMnU7H77//zvPPP09FRQVvvvmmj57S0lIGDRrEqFGjGDt2LPPmzeO+++5DqVRy5513esvdfvvtbNy48aJLpz///HMeeughbrnlFh5++GGsVisHDhzgjz/+YNy4cZft908++YTmzZszZMgQ5HI5S5cu5f7778ftdjNlyhQfeUePHmXs2LFMnjyZu+++myZNmnjzNm7cyM8//8xDDz2ESqVi9uzZDBgwgD///JMWLVoAkJ+fT+fOnb0D4bCwMFauXMldd91FRUXFZW8Ec776XIpN7733Hg8++CA6nc47kImIiDivrm+++YY77riDDh06MHPmTPLz83n//ffZunWrz/V5Lu3btyc5OZl58+Z5B2I1/PzzzwQFBZGamgrAvffey4IFC3jggQdo1qwZxcXFbNmyhSNHjnDdddddlm9qePnll1EqlUydOhWbzYZSqSQtLY0lS5YwcuRIkpKSyM/PZ86cOfTs2ZO0tDSio6N9ZLz++utIpVKmTp1KeXk5b7zxBuPHj+ePP/7wlvnkk0944IEH6N69O48++ihZWVkMGzaMoKAgn4d6t9vNkCFD2LJlC/fccw8pKSkcPHiQd999l4yMjEv+tvGZZ56hSZMmfPbZZ7z00kskJSXRoEED4NL6CIADBw7QvXt3FAoF99xzD4mJiRw/fpylS5fy6quvcvPNN5ORkcGPP/7Iu+++613WHxYWBsCkSZOYO3cut9xyC48//jh//PEHM2fO5MiRIyxevNinvhdqO/Vxxx13MHnyZObMmcPkyZMvySf14XK5GDBgAJ07d+aNN95g1apVvPDCCzidTl566SWfsl9//TVWq5V77rkHlUpFcHAw69atY+DAgSQnJzNjxgyqqqr48MMPuf7669mzZw+JiYlMnjyZmJgYXnvtNR566CE6dOhw3rb07bffMmnSJDp27Mg999wD4D1vFyIlJYVvv/2WRx99lNjYWB5//HHAcy4qKir44osvGDt2LHfffTcmk4kvv/yS1NRU/vzzT9q0aeOVc9ddd/HNN98wcOBAJk2ahNPpZPPmzezYsYP27dtfsH6HDx+me/fuGAwGnnzySRQKBXPmzKFXr15s3LiRTp06+dT5/vvvJywsjOeffx6LxXJpJ0xERETkn0YQERG5JF544QUBEO68806f9OHDhwshISHe31lZWYJMJhNeffVVn3IHDx4U5HK5T3plZaWfnpkzZwoSiUQ4deqUN23KlClCfc315MmTAiCEhYUJZWVl3vRp06YJgNC6dWvB4XB408eOHSsolUrBarVesA6TJ08WAgICfMr17NlTAIS3337bm2az2YQ2bdoI4eHhgt1u9yt7MYYOHSo0b978gmUu1e/nsyU1NVVITk72SUtISBAAYdWqVX7lAQEQdu3a5U07deqUoFa
},
"metadata": {},
"output_type": "display_data"
}
],
"execution_count": 7
2025-07-10 15:07:31 +08:00
}
],
"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
}