🚀 AI 一键生成 joinquant 策略代码

JoinQuant 异常处理:如何检测并处理被交易所拒绝的订单(Rejected)?

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

问题描述

订单被交易所拒绝 rejected 后该怎么处理?

解决方案

在 JoinQuant 聚宽平台中,当订单状态变为 rejected(交易所已拒绝),通常意味着订单虽然成功发送到了交易所(或模拟撮合引擎),但由于不符合交易规则(如委托价格超过涨跌停板、标的停牌、没有流动性等)而被拒绝执行。

处理 rejected 订单通常包含三个步骤:原因排查状态检测后续补救(重发或撤销)

以下是详细的处理逻辑和代码示例。

1. 常见拒绝原因

在编写处理逻辑前,需要了解为何会被拒绝,以便在日志中排查:

  • 价格超出涨跌停限制:限价单价格不在当日涨跌停范围内。
  • 标的停牌:股票处于停牌状态。
  • 流动性不足(模拟盘/回测):模拟交易中,如果下单时刻该标的成交量为0,系统会拒绝成交。
  • 合规限制:如科创板/创业板的特殊交易规则限制。

2. 处理策略

由于订单状态的更新是异步的(下单后不会立即变更为 rejected,通常在下一个事件循环或撮合时更新),建议在 handle_data 或定时运行的函数中轮询检查订单状态。

处理流程:

  1. 记录订单 ID:下单时保存 order_id
  2. 检查状态:在每个时间步(bar)检查该订单的 status
  3. 执行逻辑:如果发现 status == OrderStatus.rejected,则打印日志,并根据策略需求决定是否以市价单重试或放弃。

3. 代码实现

以下是一个完整的策略示例,展示了如何捕捉被拒绝的订单并进行重试(例如将限价单改为市价单重试)。

# -*- coding: utf-8 -*-
from jqdata import *

def initialize(context):
    # 设置基准
    set_benchmark('000300.XSHG')
    # 开启动态复权
    set_option('use_real_price', True)
    # 过滤日志
    log.set_level('order', 'error')
    
    # 定义一个全局列表,用于存储当日需要监控的订单ID
    g.monitor_order_ids = []
    
    # 每天开盘时运行
    run_daily(market_open, time='09:30')
    # 每天盘中每分钟检查订单状态
    run_daily(check_order_status, time='every_bar')

def market_open(context):
    # 清空每日监控列表
    g.monitor_order_ids = []
    
    security = '000001.XSHE'
    
    # 示例:故意下一个可能被拒绝的单子(例如跌停价卖出或涨停价买入,或者在某些极端情况下)
    # 这里为了演示,我们下一个限价单
    # 注意:order 函数返回的是 Order 对象,如果创建失败返回 None
    ord = order(security, 100, style=LimitOrderStyle(10.0))
    
    if ord is not None:
        # 将订单ID加入监控列表
        g.monitor_order_ids.append(ord.order_id)
        log.info("下单成功,订单ID: %s,等待状态更新..." % ord.order_id)
    else:
        log.error("下单创建失败(可能是资金不足或标的不存在)")

def check_order_status(context):
    # 遍历监控列表中的订单
    # 使用切片 [:] 遍历,以便在循环中安全移除元素
    for order_id in g.monitor_order_ids[:]:
        # 获取订单对象
        ord = get_orders(order_id=order_id)[order_id]
        
        # 1. 处理被拒绝 (Rejected) 的订单
        if ord.status == OrderStatus.rejected:
            log.warn("订单 %s 被交易所拒绝!标的:%s" % (order_id, ord.security))
            
            # --- 补救措施示例 ---
            # 策略:如果限价单被拒,尝试以市价单重试(慎用,视策略而定)
            log.info("尝试以市价单重试下单...")
            new_ord = order(ord.security, ord.amount, style=MarketOrderStyle())
            
            if new_ord:
                log.info("重试下单成功,新订单ID: %s" % new_ord.order_id)
            
            # 处理完毕,从监控列表中移除
            g.monitor_order_ids.remove(order_id)
            
        # 2. 处理已取消 (Canceled) 的订单
        elif ord.status == OrderStatus.canceled:
            log.info("订单 %s 已撤销。" % order_id)
            g.monitor_order_ids.remove(order_id)
            
        # 3. 处理全部成交 (Held) 的订单
        elif ord.status == OrderStatus.held:
            log.info("订单 %s 全部成交。" % order_id)
            g.monitor_order_ids.remove(order_id)
            
        # 4. 处理新订单 (New) 或 部分成交 (Filled) 或 待成交 (Open)
        # 这些状态需要继续监控,不做移除操作
        elif ord.status in [OrderStatus.new, OrderStatus.open, OrderStatus.filled]:
            pass

4. 关键点说明

  1. OrderStatus 枚举
    必须使用 OrderStatus.rejected 来判断状态。在代码中,OrderStatusjqdata 库的一部分,通常在环境初始化时已自动导入,或者可以通过 from jqdata import * 引入。

  2. 区分 Nonerejected

    • 返回 None:调用 order() 函数时直接返回 None,说明订单创建失败。原因通常是:标的代码错误、资金不足、仓位不足、标的已退市等。这属于程序逻辑或账户层面的错误。
    • 状态 rejectedorder() 返回了对象,但在后续撮合过程中被拒绝。原因通常是:价格违规(涨跌停)、停牌、无对手盘等。这属于市场层面的拒绝。
  3. 避免死循环
    在执行补救措施(如重单)时,务必小心。如果是因为标的停牌导致的 rejected,重试下单依然会被拒绝。建议在重试前检查标的状态(如 get_current_data()[security].paused),或者设置最大重试次数。

  4. 模拟盘与回测的区别

    • 回测:订单状态更新非常快,通常在下一个 bar 就能检测到。
    • 模拟交易:存在真实的网络延迟和撮合延迟,建议不要在下单后立即检查,而是通过 run_dailyhandle_data 轮询。

Q&A

Q1: 订单返回 None 和状态为 rejected 有什么区别?
A1: None 表示订单根本没有生成(如资金不足、代码错误),属于本地校验失败;rejected 表示订单已生成并发送,但被交易所或撮合引擎驳回(如价格超出涨跌停、停牌),属于交易端校验失败。

Q2: 如何判断股票是否停牌以避免下单被拒?
A2: 在下单前使用 current_data = get_current_data(),检查 current_data[security].paused 属性。如果为 True,则不要下单。

Q3: 为什么市价单在模拟盘也会被 rejected?
A3: 在模拟盘中,为了防止以不合理价格成交,如果下单时刻该标的从开盘到现在的累积成交量为0(例如开盘集合竞价期间或极度不活跃的标的),系统会拒绝市价单。建议在 9:30 之后下单,或检查成交量。