🚀 AI 一键生成 joinquant 策略代码
立即体验
👉 AI 生成代码 👈

JoinQuant聚宽平台策略自动调参指南:利用API实现批量回测

joinquant | 发布时间: 2026/6/5
以下内容由 EasyQuant 生成。

问题描述

有什么好的调参方法吗?

比如,想知道策略到底用近5日涨幅还是近8日涨幅,用突破近10日均线还是20日均线,买入前3只股票还是前5只股票,策略效果更好。这个时候就要去对近n日涨幅,近m日均线,前a只股票等多个参数进行调试并回测,看哪一个效果最佳。比如上述3个参数,就有8种排列组合。现在回测需要人手工点击做8次,记录每次的效果最后比较。有没有什么方式,可以一次点击做回测,最后8种组合的回测结果都出来了?

解决方案

在JoinQuant(聚宽)平台上,你完全不需要手工点击8次来进行调参。你可以利用聚宽的研究环境(Research),结合 create_backtestget_backtest 这两个强大的API,编写一段Python脚本来实现全自动的网格搜索(Grid Search)批量回测。\n\n### 核心思路\n1. 定义参数网格:利用Python的 itertools.product 生成所有可能的参数组合(例如你的8种组合)。\n2. 批量提交回测:在研究环境中,通过一个循环调用 create_backtest 函数。你可以利用 extras 参数将不同的参数组合动态传递给策略代码中的全局变量 g。\n3. 自动获取结果:使用 get_backtest 函数轮询回测状态,当回测完成后,自动提取年化收益、夏普比率、最大回撤等核心指标。\n4. 结果汇总对比:将所有结果汇总到一个 Pandas DataFrame 中,按收益或夏普比率排序,最佳参数一目了然。\n\n### 具体实现步骤与代码示例\n\n请在聚宽的研究环境中新建一个 Notebook,并运行以下代码:\n\npython\nimport time\nimport pandas as pd\nimport itertools\nfrom jqdata import *\n\n# 1. 定义你的策略代码(注意:策略代码作为字符串传入)\n# 在策略中,我们将使用 g.n_days, g.m_ma, g.a_stocks 来接收外部传入的参数\nstrategy_code = \"\"\"\nfrom jqdata import *\n\ndef initialize(context):\n set_benchmark('000300.XSHG')\n set_option('use_real_price', True)\n # 注意:通过 extras 传入的参数会在 initialize 执行后覆盖 g 变量\n # 这里可以给个默认值,防止单独运行策略时报错\n if not hasattr(g, 'n_days'): g.n_days = 5\n if not hasattr(g, 'm_ma'): g.m_ma = 10\n if not hasattr(g, 'a_stocks'): g.a_stocks = 3\n \n run_daily(market_open, time='09:30')\n\ndef market_open(context):\n # 这里写你的核心逻辑,使用 g.n_days, g.m_ma, g.a_stocks 进行选股和交易\n # log.info(f\"当前参数: n={g.n_days}, m={g.m_ma}, a={g.a_stocks}\")\n pass\n\"\"\"\n\n# 2. 定义需要遍历的参数字典\nparam_grid = {\n 'n_days': [5, 8], # 近n日涨幅\n 'm_ma': [10, 20], # 突破m日均线\n 'a_stocks': [3, 5] # 买入前a只股票\n}\n\n# 生成所有参数组合 (2 x 2 x 2 = 8种组合)\nkeys = param_grid.keys()\ncombinations = [dict(zip(keys, values)) for values in itertools.product(*param_grid.values())]\n\n# 3. 批量提交回测\nbacktest_ids = []\nprint(f\"共生成 {len(combinations)} 种参数组合,开始提交回测...\")\n\nfor params in combinations:\n # extras 参数会将字典中的键值对赋值给策略中的 g 变量 (例如 g.n_days)\n bt_params = {\n \"algorithm_id\": \"\", # 如果使用字符串代码,这里留空\n \"start_date\": \"2022-01-01\",\n \"end_date\": \"2023-01-01\",\n \"frequency\": \"day\",\n \"initial_cash\": 100000,\n \"extras\": params, # 核心:将参数组合传入策略\n \"name\": f\"调参测试_n{params['n_days']}_m{params['m_ma']}_a{params['a_stocks']}\"\n }\n \n # 提交回测\n bt_id = create_backtest(code=strategy_code, **bt_params)\n backtest_ids.append((bt_id, params))\n print(f\"已提交回测: {bt_params['name']}, ID: {bt_id}\")\n\n# 4. 轮询等待回测完成并收集结果\nresults = []\nprint(\"\n等待回测完成,正在收集数据...\")\n\nfor bt_id, params in backtest_ids:\n gt = get_backtest(bt_id)\n \n # 循环检查状态直到完成\n while True:\n status = gt.get_status()\n if status == 'done':\n break\n elif status in ['failed', 'canceled', 'deleted']:\n print(f\"回测 {bt_id} 失败或被取消。\")\n break\n time.sleep(5) # 每5秒查询一次\n \n if status == 'done':\n # 获取风险指标(年化收益、夏普等)\n risk = gt.get_risk()\n if risk:\n res_dict = params.copy()\n res_dict['年化收益'] = risk.get('annual_return', 0)\n res_dict['夏普比率'] = risk.get('sharpe', 0)\n res_dict['最大回撤'] = risk.get('max_drawdown', 0)\n results.append(res_dict)\n\n# 5. 结果展示与对比\nif results:\n df_results = pd.DataFrame(results)\n # 按年化收益降序排列\n df_results = df_results.sort_values(by='年化收益', ascending=False).reset_index(drop=True)\n print(\"\n================ 调参结果汇总 ================\")\n print(df_results)\nelse:\n print(\"未能获取到有效回测结果。\")\n\n\n### 注意事项\n1. extras 参数的覆盖机制create_backtest 传入的 extras 参数,是在策略的 initialize 函数执行之后才赋值给 g 变量的。因此,在 initialize 中直接打印 g.n_days 可能还是默认值,但在 market_open 等盘中函数里,它已经是你传入的调参值了。\n2. 并发与积分限制:聚宽对同时运行的回测数量有一定限制。如果你的参数组合非常多(比如几百种),建议在循环提交时加入适当的 time.sleep(),或者分批次提交。此外,大量回测可能会消耗平台的运行积分(如果超过免费时长,可通过 use_credit=True 允许消耗积分)。\n3. 过拟合风险:虽然机器调参非常方便,但请警惕“过度优化(Overfitting)”。在历史数据上表现最好的参数,不一定在未来实盘中表现最好。建议结合业务逻辑和参数平原(Parameter Plateau)来选择稳健的参数组合。