问题描述
解决方案
在JoinQuant(聚宽)平台中,如果需要同时对几十只甚至上百只股票进行下单(例如指数增强或ETF轮动策略),为了提高执行效率并减少网络交互耗时,推荐使用 batch_submit_orders(篮子下单) 函数。
相比于在 for 循环中逐个调用 order 或 order_target,batch_submit_orders 可以一次性将一批订单发送给交易引擎,效率更高。
核心思路
- 获取标的:获取指数成分股(如沪深300)。
- 计算目标仓位:根据策略(如等权重或市值加权)计算每只股票的目标持仓股数。
- 计算调仓差额:目标股数 - 当前持仓股数 = 需要交易的股数(
amount)。 - 构建订单列表:将所有需要交易的订单封装成字典列表。
- 批量提交:调用
batch_submit_orders。
策略代码实现
以下是一个完整的 沪深300等权重跟踪策略,展示了如何计算仓位差额并使用篮子下单进行高效交易。
# -*- coding: utf-8 -*-
# 导入函数库
from jqdata import *
def initialize(context):
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 设定交易费率
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# 设定要跟踪的指数代码 (沪深300)
g.index_code = '000300.XSHG'
# 设定调仓频率:每月第一个交易日
run_monthly(rebalance, 1, time='09:30')
def rebalance(context):
"""
调仓主函数
"""
log.info("开始进行指数跟踪调仓...")
# 1. 获取指数成分股
target_stocks = get_index_stocks(g.index_code)
if not target_stocks:
log.warn("未获取到成分股数据")
return
# 2. 获取当前账户总资产
total_value = context.portfolio.total_value
# 3. 计算每只股票的目标资金 (这里演示等权重,即 总资产 / 股票数量)
# 注意:实际操作中应预留少量现金以防手续费不足,这里预留 1%
available_value = total_value * 0.99
target_value_per_stock = available_value / len(target_stocks)
# 4. 获取当前所有成分股的价格 (用于计算股数)
# 使用 get_current_data 获取最新价,效率较高
current_data = get_current_data()
# 5. 构建批量订单列表
orders_to_submit = []
# 遍历目标股票池
for stock in target_stocks:
# 如果停牌,跳过
if current_data[stock].paused:
continue
price = current_data[stock].last_price
if price <= 0:
continue
# 计算目标股数 (向下取整到100的倍数)
target_amount = int(target_value_per_stock / price / 100) * 100
# 获取当前持仓股数
current_amount = context.portfolio.positions[stock].total_amount
# 计算需要交易的差额 (正数为买入,负数为卖出)
delta_amount = target_amount - current_amount
# 如果差额不为0,且满足一手(100股),则生成订单
if abs(delta_amount) >= 100:
# 构建订单字典,格式参考API文档
order_dict = {
'security': stock,
'amount': delta_amount, # 正买负卖
'style': MarketOrderStyle(), # 市价单
'side': 'long' # 股票通常是做多方向
}
orders_to_submit.append(order_dict)
# 6. 处理非成分股的清仓 (不在目标池中的持仓股票)
# 注意:batch_submit_orders 也可以处理卖出,这里一并加入
for stock in context.portfolio.positions:
if stock not in target_stocks and context.portfolio.positions[stock].closeable_amount > 0:
# 停牌无法卖出
if current_data[stock].paused:
continue
# 全仓卖出
sell_amount = -context.portfolio.positions[stock].closeable_amount
order_dict = {
'security': stock,
'amount': sell_amount,
'style': MarketOrderStyle(),
'side': 'long'
}
orders_to_submit.append(order_dict)
# 7. 执行批量下单
if len(orders_to_submit) > 0:
log.info(f"准备批量提交 {len(orders_to_submit)} 个订单")
# 技巧:为了防止资金不足导致买入失败,建议先处理卖单,再处理买单
# batch_submit_orders 是按列表顺序执行的
# 将订单按 amount 排序,负数(卖出)在前,正数(买入)在后
orders_to_submit.sort(key=lambda x: x['amount'])
# 调用篮子下单 API
# 注意:batch_submit_orders 会对每一个委托进行验资验券,若其中任一个委托校验失败,则整个委托将会失败
# 因此在实盘中,有时为了稳健,也可以分批次调用,或者使用 try-except 包裹普通 order
# 但在回测和追求速度的场景下,batch_submit_orders 是最快的
try:
batch_submit_orders(orders_to_submit)
log.info("批量订单提交完成")
except Exception as e:
log.error(f"批量下单失败: {e}")
# 如果批量下单失败(例如某一只股票验资失败),可以回退到循环下单作为备选方案
fallback_order(orders_to_submit)
else:
log.info("无需调仓")
def fallback_order(orders_list):
"""
备选下单方案:如果批量下单失败,转为逐个下单
"""
log.info("启动备选逐个下单模式...")
for order_info in orders_list:
order(order_info['security'], order_info['amount'], style=order_info['style'])
关键点解析
-
batch_submit_orders(orders):- 这是核心函数。它接收一个列表,列表中每个元素都是一个包含订单信息的字典。
- 参数要求:字典必须包含
security(代码),amount(数量, 正买负卖),style(市价/限价),side('long')。 - 原子性风险:该函数的一个特性是**“若其中任一个委托校验失败,则整个委托将会失败”**。这意味着如果你计算的买入金额超过了现金,或者卖出超过了持仓,所有订单都会被拒绝。
- 解决方案:代码中预留了
0.99的资金缓冲(available_value = total_value * 0.99),防止因计算误差或手续费导致资金不足从而触发批量失败。
-
先卖后买:
- 代码中使用了
orders_to_submit.sort(key=lambda x: x['amount'])。 - 因为
amount负数代表卖出,正数代表买入,排序后卖单会在列表前面。 - 虽然
batch_submit_orders是批量提交,但在逻辑处理上,确保卖出逻辑在买入之前有助于逻辑清晰(尽管该API是原子校验,资金释放通常是即时的)。
- 代码中使用了
-
手动计算股数:
- 普通的
order_target_value会自动帮你计算需要买多少股。 - 使用
batch_submit_orders时,你需要自己完成(目标资金 / 股价) -> 取整 -> 减去当前持仓的计算过程。
- 普通的
-
备选方案 (
fallback_order):- 由于
batch_submit_orders的严格校验机制,在实盘或复杂回测中,建议加上try...except块。如果批量提交失败,降级为普通的for循环逐个下单,保证策略的健壮性。
- 由于
为什么这样更高效?
- 减少API调用开销:在回测引擎中,每一次
order调用都涉及一次内部的撮合逻辑检查和日志记录。几十只股票循环几十次会产生累积耗时。 - 模拟/实盘网络优化:如果是接入实盘交易接口(如聚宽的实盘工具),
batch_submit_orders通常对应券商接口的批量报单功能,能显著减少网络往返次数(RTT),这对于抢占时间窗口非常重要。
Q&A
Q: batch_submit_orders 支持限价单吗?
A: 支持。在构建字典时,将 'style': MarketOrderStyle() 改为 'style': LimitOrderStyle(price) 即可。
Q: 如果我不想自己计算股数,只想按比例调仓怎么办?
A: 如果不想自己写复杂的计算逻辑,可以使用 order_target_value 配合 for 循环。虽然速度稍慢,但代码容错率更高。只有在对速度有极致要求或股票数量非常大(如全市场选股)时,batch_submit_orders 的优势才非常明显。
Q: 为什么代码里要预留 1% 的现金?
A: 因为 batch_submit_orders 校验非常严格。如果你的总资产是 100万,你试图买入 100万市值的股票,加上手续费(佣金+印花税)肯定会超过 100万,导致验资失败,进而导致所有订单全部失败。预留现金是为了覆盖手续费和股价波动。