3 分钟快速生成代码
输入想法,AI 即刻生成可运行代码
在量化交易中,跟随外部信号(如 Webhook、本地文件、数据库或第三方 API)进行跟单交易是一种常见的策略模式。然而,由于网络延迟、策略轮询频率(如分钟级或 Tick 级)与订单撮合时间差,策略极易在信号未消失或订单未完全成交时重复触发下单。此外,模拟盘或回测中的日志丢失、扫描逻辑不严密也会导致跟单失败。
本文将详细介绍如何在聚宽(JoinQuant)平台中,通过持仓检查、未完成订单同步以及本地文件/全局变量状态记录,构建一个健壮的防重复下单跟单系统。
要彻底避免重复下单,策略在收到外部信号准备下单前,必须通过以下三道关卡进行校验:
g 或 write_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("今日交易结束,已清理信号缓存。")
在聚宽中,使用 get_open_orders() 可以实时获取当天已委托但未完全成交的订单。在下单前,遍历该函数返回的字典,若发现目标标的已存在挂单,应立即拦截新的下单指令。这是解决分钟级轮询策略重复下单的最有效手段。
signal.json 与聚宽策略读取该文件时可能发生 IO 冲突。在 check_external_signals 中必须使用 try...except 捕获异常,确保单次读取失败不影响策略后续运行。order 相关日志可能因级别限制而不输出。可以在 initialize 中通过 log.set_level('order', 'info') 确保所有下单、撤单的系统日志均能正常打印,便于排查跟单问题。聚宽模拟盘每天收盘后会休眠并重启。为了防止重启后全局变量 g.processed_signals 丢失导致历史信号被重复触发:
g 对象会被系统自动序列化保存(大小限制 30M 内)。g 中存储无法被序列化的对象(如数据库连接、query 对象等)。signal_id)建议通过 write_file 写入本地 CSV 或 JSON 文件,重启时在 process_initialize(context) 中重新读取恢复。