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

聚宽平台如何实现外部信号跟单策略?(附防重复下单与日志修复方案)

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

问题描述

聚宽跟单策略开发:如何记录下单结果并避免重复下单?
如何在聚宽中实现跟随外部信号的交易策略,修复扫描和日志问题,并通过订单记录、持仓检查与状态同步避免重复下单?

解决方案

在量化交易中,跟随外部信号(如 Webhook、本地文件、数据库或第三方 API)进行跟单交易是一种常见的策略模式。然而,由于网络延迟、策略轮询频率(如分钟级或 Tick 级)与订单撮合时间差,策略极易在信号未消失或订单未完全成交时重复触发下单。此外,模拟盘或回测中的日志丢失、扫描逻辑不严密也会导致跟单失败。

本文将详细介绍如何在聚宽(JoinQuant)平台中,通过持仓检查未完成订单同步以及本地文件/全局变量状态记录,构建一个健壮的防重复下单跟单系统。


一、 防重复下单的核心逻辑

要彻底避免重复下单,策略在收到外部信号准备下单前,必须通过以下三道关卡进行校验:

  1. 持仓检查(Positions Check):检查当前账户是否已经持有该标的,或持仓量是否已达到目标值。
  2. 未完成订单检查(Open Orders Check):检查当前是否有该标的的挂单(未成交或部分成交的订单)正在撮合。如果已有挂单,则不重复下单。
  3. 本地/全局状态同步(State Synchronization):使用全局变量 gwrite_file 记录已发送的下单信号,防止在同一 Bar 内多次触发。

二、 聚宽跟单策略防重源码实现

以下是一个完整的跟单策略模板。策略通过 read_file 模拟读取外部信号文件(实际开发中可替换为 API 请求或数据库查询),并严格执行防重校验。

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

def initialize(context):
    # 开启真实价格模式
    set_option('use_real_price', True)
    set_benchmark('000300.XSHG')
    
    # 设置交易税费
    set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
    
    # 全局变量:记录今日已处理的信号ID,防止重复执行同一信号
    g.processed_signals = set()
    
    # 定时运行:每分钟扫描一次外部信号
    run_daily(check_external_signals, time='every_bar')

def check_external_signals(context):
    """扫描外部信号并执行跟单"""
    try:
        # 模拟从研究根目录读取外部信号文件 signal.json
        # 信号格式示例: {"signal_id": "10023", "security": "000001.XSHE", "action": "BUY", "amount": 1000}
        signal_content = read_file("signal.json")
        if not signal_content:
            return
        
        signal_data = json.loads(signal_content)
    except Exception as e:
        # 修复扫描异常,防止因文件读取冲突导致策略中断
        log.error("读取外部信号文件失败: %s" % str(e))
        return

    signal_id = signal_data.get("signal_id")
    security = signal_data.get("security")
    action = signal_data.get("action")
    amount = signal_data.get("amount", 0)

    # 1. 校验信号是否已被处理过
    if signal_id in g.processed_signals:
        return

    # 2. 检查该标的当前是否有未完成的订单(防重核心)
    open_orders = get_open_orders()
    has_active_order = False
    for order_id, order_obj in open_orders.items():
        if order_obj.security == security:
            has_active_order = True
            log.warn("标的 %s 当前已有挂单正在撮合,跳过本次下单信号。" % security)
            break
    
    if has_active_order:
        return

    # 3. 结合持仓检查与下单执行
    current_position = context.portfolio.positions[security].total_amount

    if action == "BUY":
        # 如果已经持有该股票,且持仓量大于等于信号量,则不重复买入
        if current_position >= amount:
            log.info("标的 %s 当前持仓 %s 已满足信号要求,无需买入。" % (security, current_position))
            g.processed_signals.add(signal_id) # 标记为已处理
            return
        
        # 执行买入
        buy_amount = amount - current_position
        order_obj = order(security, buy_amount)
        if order_obj:
            log.info("成功发送买入委托,单号: %s, 数量: %s" % (order_obj.order_id, buy_amount))
            g.processed_signals.add(signal_id)
            # 将下单结果写入本地文件,供外部系统同步状态
            record_order_result(signal_id, "SUBMITTED", order_obj.order_id)

    elif action == "SELL":
        # 如果当前无持仓,则无需卖出
        if current_position == 0:
            log.info("标的 %s 当前无持仓,无需卖出。" % security)
            g.processed_signals.add(signal_id)
            return
        
        # 执行卖出(平仓)
        order_obj = order_target(security, 0)
        if order_obj:
            log.info("成功发送卖出委托,单号: %s" % order_obj.order_id)
            g.processed_signals.add(signal_id)
            record_order_result(signal_id, "SUBMITTED", order_obj.order_id)

def record_order_result(signal_id, status, order_id):
    """记录下单结果到研究环境,便于外部系统跟单状态同步"""
    result = {
        "signal_id": signal_id,
        "status": status,
        "order_id": order_id
    }
    write_file("order_result.json", json.dumps(result), append=True)

def after_trading_end(context):
    """收盘后清理今日信号缓存"""
    g.processed_signals.clear()
    log.info("今日交易结束,已清理信号缓存。")

三、 关键技术点与常见问题修复

1. 如何精准获取未完成订单?

在聚宽中,使用 get_open_orders() 可以实时获取当天已委托但未完全成交的订单。在下单前,遍历该函数返回的字典,若发现目标标的已存在挂单,应立即拦截新的下单指令。这是解决分钟级轮询策略重复下单的最有效手段。

2. 修复扫描与日志丢失问题

  • 文件读写冲突:外部系统写入 signal.json 与聚宽策略读取该文件时可能发生 IO 冲突。在 check_external_signals 中必须使用 try...except 捕获异常,确保单次读取失败不影响策略后续运行。
  • 日志级别设置:默认情况下,聚宽的 order 相关日志可能因级别限制而不输出。可以在 initialize 中通过 log.set_level('order', 'info') 确保所有下单、撤单的系统日志均能正常打印,便于排查跟单问题。

3. 模拟盘重启后的状态保持

聚宽模拟盘每天收盘后会休眠并重启。为了防止重启后全局变量 g.processed_signals 丢失导致历史信号被重复触发:

  • 策略中使用的 g 对象会被系统自动序列化保存(大小限制 30M 内)。
  • 避免在 g 中存储无法被序列化的对象(如数据库连接、query 对象等)。
  • 重要的跟单状态(如已成交的 signal_id)建议通过 write_file 写入本地 CSV 或 JSON 文件,重启时在 process_initialize(context) 中重新读取恢复。