🚀 AI 一键生成 joinquant 策略代码

JoinQuant 实战:如何使用 batch_submit_orders 实现指数复制策略?

joinquant | 发布时间: 2025/12/15
以下内容由 EasyQuant 生成。

问题描述

我想写一个需要同时买入几十只股票的指数跟踪策略,如何高效下单?

解决方案

在JoinQuant(聚宽)平台中,如果需要同时对几十只甚至上百只股票进行下单(例如指数增强或ETF轮动策略),为了提高执行效率并减少网络交互耗时,推荐使用 batch_submit_orders(篮子下单) 函数。

相比于在 for 循环中逐个调用 orderorder_targetbatch_submit_orders 可以一次性将一批订单发送给交易引擎,效率更高。

核心思路

  1. 获取标的:获取指数成分股(如沪深300)。
  2. 计算目标仓位:根据策略(如等权重或市值加权)计算每只股票的目标持仓股数。
  3. 计算调仓差额:目标股数 - 当前持仓股数 = 需要交易的股数(amount)。
  4. 构建订单列表:将所有需要交易的订单封装成字典列表。
  5. 批量提交:调用 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'])

关键点解析

  1. batch_submit_orders(orders)

    • 这是核心函数。它接收一个列表,列表中每个元素都是一个包含订单信息的字典。
    • 参数要求:字典必须包含 security (代码), amount (数量, 正买负卖), style (市价/限价), side ('long')。
    • 原子性风险:该函数的一个特性是**“若其中任一个委托校验失败,则整个委托将会失败”**。这意味着如果你计算的买入金额超过了现金,或者卖出超过了持仓,所有订单都会被拒绝。
    • 解决方案:代码中预留了 0.99 的资金缓冲(available_value = total_value * 0.99),防止因计算误差或手续费导致资金不足从而触发批量失败。
  2. 先卖后买

    • 代码中使用了 orders_to_submit.sort(key=lambda x: x['amount'])
    • 因为 amount 负数代表卖出,正数代表买入,排序后卖单会在列表前面。
    • 虽然 batch_submit_orders 是批量提交,但在逻辑处理上,确保卖出逻辑在买入之前有助于逻辑清晰(尽管该API是原子校验,资金释放通常是即时的)。
  3. 手动计算股数

    • 普通的 order_target_value 会自动帮你计算需要买多少股。
    • 使用 batch_submit_orders 时,你需要自己完成 (目标资金 / 股价) -> 取整 -> 减去当前持仓 的计算过程。
  4. 备选方案 (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万,导致验资失败,进而导致所有订单全部失败。预留现金是为了覆盖手续费和股价波动。