资讯动态

用Redis搭桥:让聚宽策略在QMT上自动下单

发布时间:2026/9/18 9:45:00 来源:尧图企业网站定制
聚宽上的策略跑得再好回测曲线再漂亮只要没到实盘那一刻你手里的代码就还只是纸上谈兵。而一旦决定实盘摆在面前的第一道坎往往不是行情、不是策略而是策略逻辑写在哪、交易指令从哪下这个问题。我就是在这个环节绕了不少弯路——聚宽的策略代码没法直接在QMT里原样运行QMT自带的QMT终端虽然能手动下单但总不能每天盯盘手点吧。折腾了一圈之后我最终用一条Redis信号链路把两边打通聚宽负责产信号QMT通过xtquant负责真正下单中间用Redis做异步消息通道。这篇文章就完整记录我这套对接方案的思路和代码给同样卡在策略与交易端分离这件事上的朋友一个可直接抄作业的参考。1. 为什么聚宽策略落地QMT需要一条信号桥1.1 我在对接前踩过的弯路最早我拿到QMT的时候第一反应是把聚宽的策略代码直接搬过来跑。结果打开QMT内置的Python环境一看就傻眼了——聚宽的研究环境里能用的一堆数据接口、回测框架在QMT里根本没做兼容你在聚宽研究里写的那套基于get_price、order_target_percent的代码换到QMT的API体系里几乎等于重写。更尴尬的是聚宽的因子计算、股票池筛选、信号生成逻辑往往依赖平台的数据库和调度机制这部分你很难完整迁移到QMT本地的Python环境里。我当时的第二版方案是QMT端直接定时跑聚宽的模拟交易接口也就是让聚宽云端的模拟交易帮我把策略逻辑跑起来然后我再靠人工去QMT里照着信号下单。这个方案能跑但有个致命问题量化交易讲究的是执行速度和非情绪化人肉下单不仅慢还容易漏单、错单尤其信号在盘中频繁出现的时候根本盯不过来。而且聚宽的模拟盘信号和QMT实盘之间没有天然联动一旦出现成交差异、价格滑点整个账都对不上。1.2 信号桥接方案与手工/直移方案的对比后来我搭了这套聚宽产信号 - Redis传递 - xtquant执行的桥接架构本质上是把策略系统和交易系统彻底解耦。对比三种常见方案区别非常明显方案策略逻辑执行方式延迟改造成本适合阶段直接在QMT内重写策略迁移到QMT本地QMT原生执行最低极高几乎重写有专职团队长期维护聚宽模拟人工下单保留在聚宽云端人工在QMT操作高按分钟计低信号频率低、资金量小的场景Redis信号桥接保留在聚宽/本地xtquant自动下单秒级甚至毫秒级中只需写信号读写层独立策略、个人/小团队实盘我把这个对比列出来是希望大家明白中间这个信号桥不是多余的动作而是两个异构系统之间最自然的连接方式。聚宽擅长的是研究、回测和策略逻辑的快速迭代QMT擅长的是行情接入和柜台交易通道两者之间通过一个独立的消息通道协作哪一端的代码都不会被对方的框架绑架。2. 选Redis做信号中枢不只是图它快2.1 各传输方案选型对比要做信号传递可选的技术方案其实不少MySQL、文件目录、Kafka、Redis。我都评估过一遍。数据库轮询用MySQL这种关系型数据库存信号稳定性没问题但为了一个下单信号搞一个库表、建索引、写轮询多少有点重。而且高频信号场景下数据库的写入和查询延迟都在毫秒以上事务开销也不划算。文件传输最早我觉得JSON文件最省事聚宽写一个文件QMT定时去读。实际一跑就发现问题——QMT和聚宽两个进程同时读写同一个文件冲突概率很高而且没有谁消费了这条信号的确认机制容易重复执行。Kafka/RabbitMQ功能强大、吞吐量高但对于个人量化交易来说部署和维护成本偏高光是一个Kafka就要起Zookeeper属于杀鸡用牛刀。Redis内存数据库读写都是亚毫秒级延迟自带List这种天然适合做任务队列的数据结构还有BRPOP这样的阻塞读取命令正好匹配信号产生者-信号消费者模型。更重要的是Redis安装配置足够简单我在Windows和Linux上都部署过完全没有负担。2.2 整体链路设计聚宽产信号到xtquant下单我最终确定的链路是单向的、异步的聚宽策略逻辑产出信号 - 信号JSON序列化 -LPUSH写入Redis List - QMT端BRPOP阻塞读取 - 解析JSON - xtquant调用order_stock下单 - 写入已处理记录防止重复执行。选择Redis List而不是Redis String作为信号队列是因为List原生支持FIFO先进先出语义LPUSH从左边推入BRPOP从右边弹出天然就是生产者-消费者模式。而且BRPOP是阻塞读QMT端没有新信号的时候会挂在那里等待不会空转消耗CPU一旦信号进来能秒级响应。2.3 信号字段与幂等设计为真实盘留好安全阀信号格式是整条链路的协议两边都靠这个JSON里的字段进行协作。我设计的信号结构长这样{ strategy_id: jq_demo_01, signal_id: 20250101103000_600519_BUY, code: 600519.SH, action: BUY, price: 1588.0, volume: 100, signal_time: 2025-01-01 10:30:00, remark: daily_signal }字段不多但每个都有讲究strategy_id标识信号来源方便同一个QMT端同时接多套策略各自独立的主题队列互不干扰。signal_id唯一信号ID这是幂等处理的核心。我把它设计成时间戳代码方向保证同一时刻同一只股票的同一操作只有一个ID。code标的代码。这里有个容易忽略的坑聚宽里的代码格式是600519.XSHGQMT的xtquant用的是600519.SH如果你不转换直接下单QMT不认账户里的股票。所以我的做法是在聚宽端写好映射或者QMT端统一做一次代码格式清洗二者必须留一个。action买卖方向我用BUY和SELL两个值通信然后在QMT端映射成xtquant的xtconstant.STOCK_BUY、xtconstant.STOCK_SELL。price委托价格。传0的话表示按对手价/最新价下单传具体数值则用限价单。volume委托数量注意A股是100股起步、按100股递增。实盘里最怕的就是信号重复执行。比如QMT端读取Redis后刚好进程重启或者网络超时导致聚宽端也异常重发同一个信号很可能会下单两次。我的解决方案是结合Redis的SETNX命令做幂等锁dedupe_key ftrading:signal:done:{signal_id} if redis_client.setnx(dedupe_key, 1): redis_client.expire(dedupe_key, 3600) # 执行真实下单 execute_signal(...)SETNX的含义是只有key不存在时才设置成功所以只有第一次读到这个signal_id的进程能拿到执行权之后的重复读取会被直接跳过。这个机制就是Redis分布式锁在信号去重上一个非常朴素的应用不需要引入额外组件一个命令就解决了。3. 聚宽端策略改造只产信号不碰交易3.1 策略信号写入Redis的代码实现聚宽端我建议尽量做到策略逻辑只管产信号交易动作全部交给QMT。这样改代码时只用动信号输出这一段原来的策略研究和回测逻辑一行都不用变。如果聚宽运行环境的网络策略允许你直接连接内网Redis那在聚宽研究环境或模拟环境里直接这样写就行import redis import json import time redis_client redis.Redis( host192.168.1.100, port6379, passwordyour_redis_password, decode_responsesTrue ) def emit_signal(code, action, price, volume, strategy_idjq_demo_01): signal { strategy_id: strategy_id, signal_id: f{int(time.time() * 1000)}_{code}_{action}, code: code, action: action, price: price, volume: volume, signal_time: time.strftime(%Y-%m-%d %H:%M:%S), } signal_json json.dumps(signal, ensure_asciiFalse) redis_client.lpush(ftrading:signal:queue:{strategy_id}, signal_json) return signal # 假设你的策略在某处算出了要买600519.SH100股价格1588.0 emit_signal(600519.SH, BUY, 1588.0, 100)这里我强调两个细节。第一decode_responsesTrue一定加上。不加的话Redis返回的是bytes类型你json.loads之前还得手动decode多一层麻烦还容易报错。第二json.dumps(signal, ensure_asciiFalse)保留中文因为如果remark字段里有中文默认的ensure_asciiTrue会把中文转成一串\uXXXX存进Redis后通过可视化工具查看时非常难看。对于无法从聚宽云端直连Redis的情况我的替代方案是在本地跑一个信号转发器聚宽端把信号写入自己的数据库或输出日志本地脚本定时拉取后写入Redis。这个方案虽然多一跳但能兼容那些网络受限的聚宽账号环境原理和直连完全一样。3.2 序列化格式选择与版本兼容信号序列化我用的是JSON而不是Pickle这是底线建议大家不要碰Pickle。原因有三点Pickle是Python专属的二进制格式一旦后续你把QMT端改成其他语言实现比如为了追求低延迟换成C或者Go服务端根本解不了Pickle。Pickle天然存在反序列化安全风险如果Redis被入侵或者有人伪造信号pickle.loads可以执行任意代码这在交易系统里是灾难级别的事故。JSON的字段是自描述的后期加个字段、减个字段两边只要做好容错就不会崩。当然JSON也有一个注意点json.loads对于无法识别的字段是默认忽略的这在做版本兼容时反而是优势。我在实盘中升级过几次信号结构旧的QMT端代码读到新信号不认识新增字段也没关系照样能执行核心字段。3.3 聚宽侧的几个坑本地库、时区、数据对齐聚宽侧改造过程中我真正踩过的坑有三个第一个是时区问题。聚宽平台默认用的是UTC或者平台自己的时区设定signal_time如果不显式格式化成北京时间QMT端打印日志时看到的时间会对不上K线时间排查问题的时候非常容易误判。我建议信号里的时间在聚宽端就转好别把时区转换的任务留给下游。第二个是数据对齐问题。聚宽的股票代码是600519.XSHG、000001.XSHE这种带后缀的格式而QMT端需要的是600519.SH和000001.SZ。我建议直接在聚宽端做一次代码清洗规则很简单XSHG替换成SHXSHE替换成SZ处理完再塞进信号JSONQMT端就不用操心代码转换了。第三个是重复跑批问题。聚宽的策略调度可能一天触发多次如果你的信号生成逻辑没有做当前bar是否已经产出过信号的判断很容易在同一个时间点重复推送多条相同信号。虽然QMT端有幂等锁兜底但聚宽端最好也从源头控制——每个signal_id带着时间戳同一个bar我允许覆盖、不允许追加。4. QMT端xtquant对接从client is null到稳定下单4.1 环境准备与xtquant库引入路径xtquant是QMT配套的Python交易接口库它不是独立安装的第三方库而是跟随QMT终端一起分发的。你装完QMT之后到安装目录下找通常路径类似D:\国金证券QMT交易端\bin.x64\或D:\QMT\userdata_mini里面会有xtquant这个目录。我自己环境的引入方式是直接把xtquant目录复制到项目的site-packages里或者在启动脚本里动态添加路径import sys sys.path.append(rD:\国金证券QMT交易端\bin.x64\Lib\site-packages)然后引入核心模块from xtquant.xttrader import XtQuantTrader from xtquant.xttype import StockAccount from xtquant import xtconstant这里有个常见的环境坑xtquant里的核心接口是用C编译的pyd文件它对Python版本是有要求的。你本地用的Python 3.11或者3.12很可能跑不了QMT内置版本对应的xtquant常见的报错就是ImportError: DLL load failed while importing xttrader。我的建议是尽量用QMT安装目录自带的那套Python环境或者使用与xtquant发布时匹配的Python版本多数情况下是Python 3.8或3.9具体看你QMT版本。4.2 下单主循环读取Redis信号并执行交易QMT端的核心代码分三步初始化交易通道、订阅账户、循环消费信号。import redis import json import time import sys sys.path.append(rD:\国金证券QMT交易端\bin.x64\Lib\site-packages) from xtquant.xttrader import XtQuantTrader from xtquant.xttype import StockAccount from xtquant import xtconstant QMT_PATH rD:\国金证券QMT交易端\userdata_mini ACCOUNT_ID 你的资金账号 def init_trader(): session_id int(time.time()) trader XtQuantTrader(QMT_PATH, session_id) trader.start() connect_result trader.connect() if connect_result ! 0: raise RuntimeError(fQMT连接失败, return code {connect_result}) account StockAccount(ACCOUNT_ID, STOCK) subscribe_result trader.subscribe(account) if subscribe_result ! 0: raise RuntimeError(f账户订阅失败, return code {subscribe_result}) return trader, account def execute_signal(trader, account, signal): code signal[code] volume int(signal[volume]) price float(signal.get(price) or 0) if signal[action] BUY: order_direction xtconstant.STOCK_BUY else: order_direction xtconstant.STOCK_SELL price_type xtconstant.FIX_PRICE if price 0 else xtconstant.LATEST_PRICE order_id trader.order_stock( account, code, order_direction, volume, price_type, price, jq_bridge, signal[signal_id] ) return order_id def consume_loop(trader, account, redis_client): queue_key trading:signal:queue:jq_demo_01 while True: item redis_client.brpop(queue_key, timeout5) if not item: continue _, payload item signal json.loads(payload) dedupe_key ftrading:signal:done:{signal[signal_id]} if not redis_client.setnx(dedupe_key, 1): print(f跳过重复信号 {signal[signal_id]}) continue redis_client.expire(dedupe_key, 3600) try: order_id execute_signal(trader, account, signal) print(f委托已提交: {signal[code]} {signal[action]} {signal[volume]}股, order_id{order_id}) except Exception as exc: print(f下单异常: {exc}) if __name__ __main__: trader, account init_trader() redis_client redis.Redis(host127.0.0.1, port6379, passwordyour_redis_password, decode_responsesTrue) consume_loop(trader, account, redis_client)下单接口order_stock的参数注意几个第一个是账户对象第二个是代码第三个是买卖方向第四个是数量第五个是价格类型第六个是价格第七个是策略名第八个是备注。备注这个位置我直接塞了signal_id这样后续在QMT交易终端里查看委托记录时能直接对应上这单是哪条信号触发的对账非常方便。4.3 常见的client is null问题排查链路搜索热度最高的qmt终端 client is null我也遇到过而且当时排查了很久。这个报错的典型场景是你调用了order_stock结果抛异常提示内容跟client相关。实际上client is null根本原因几乎都是XtQuantTrader对象内部没有和QMT终端建立起有效连接。我梳理了一个固定的排查顺序建议大家照着走检查connect()的返回值。返回0才是成功非0都说明连接失败。失败最常见原因是QMT_PATH写错了或者QMT终端没启动或者QMT登录的是行情模式而不是交易模式。xtquant要求QMT终端必须保持在登录后的交易界面哪怕是最小化也必须挂着。检查subscribe()的返回值。这一步是让交易通道订阅你的资金账户。如果返回非0通常意味着ACCOUNT_ID写错了或者当前QMT登录的账号和你代码里传的账号不一致。检查QMT终端版本。有些券商的QMT终端版本比较老内置的xtquant功能不全甚至没有。如果是这种情况需要找券商客户经理索要最新版的QMT交易终端。检查Python位数。xtquant的pyd文件必须是64位环境如果你的Python是32位的很多模块加载时会直接失败或出现奇怪的问题。重启大法。如果你确认以上步骤都没问题最有效的办法是先关掉QMT终端再关掉你的策略进程然后重新启动QMT终端并登录等它完全进入交易界面能看到持仓和资金之后再启动你的策略脚本。xtquant对连接时序很敏感很多时候就是进程启动顺序不对导致client没有初始化成功。我当时卡在第一步很久最后发现是QMT_PATH指向了userdata_mini但没有先启动终端xtquant根本找不到可以对接的客户端进程。后来我把路径换成实际运行中的QMT客户端目录问题直接消失。4.4 成交回报与仓位同步下单不等于成交这是新手最容易忽略的。order_stock返回的order_id只是委托已被柜台受理的标识最终是否成交、成交多少、成交价多少需要通过订单状态查询或订阅回调来获取。xtquant提供了query_stock_orders方法轮询订单状态也可以注册回调监听订单变化。我实际用的是简单轮询方式orders trader.query_stock_orders(account) for order in orders: print(order.order_status, order.traded_volume, order.traded_price)order.order_status对应的状态在xtquant文档里有一套完整的枚举值已报、已成、已撤、废单等。我建议在桥接程序的日志里把每个委托的signal_id、order_id、状态打出来跑几天后你就能发现规律哪些代码经常废单大概率是停牌股或涨跌停股哪些代码成交率低可能是盘口太薄限价单挂不住。这些数据反过来能指导你优化信号端的价格设计。仓位同步是另一件事。因为桥接方案天然存在聚宽端不知道QMT实际持仓的信息差我采用的办法是每次QMT端启动时全量查询一次真实持仓把持仓结构存到Redis的Hash里聚宽端策略如果需要读取当前持仓不依赖记忆而是从Redis里读这份最新快照。这样做虽然不能做到完全实时但对日频、低频策略完全够用。5. Redis环境部署与运维Windows/Linux一套讲清5.1 Windows下Redis的下载、配置与自启动很多个人量化玩家是在Windows上跑QMT的所以Redis也要部署在Windows上。这里有一个认知需要纠正Redis官方其实并不提供Windows版本你在GitHub上看到的microsoftarchive/redis是微软维护的老版本分支停留在3.x后来tporadowski/redis这个社区分支维护了5.0.14是我实测比较稳定的Windows版本。下载解压后核心配置文件是redis.windows.conf。我建议至少改三处requirepass your_redis_password bind 127.0.0.1 appendonly yesrequirepass是访问密码只要暴露在内网就必须设置。bind 127.0.0.1限制只允许本机访问——如果聚宽跑在本机、QMT也跑在本机完全不需要监听外网。appendonly yes开启AOF持久化防止QMT端还没来得及消费信号、Redis就重启导致信号丢失。Windows下启动Redis很简单在命令行进入Redis目录执行redis-server.exe redis.windows.conf如果想要开机自启可以用sc create Redis binPath 你的路径\redis-server.exe --service-run redis.windows.conf注册成Windows服务。这样QMT和策略脚本重启后Redis先起不会出现下单程序连不上信号源的问题。Linux服务器上部署就常规多了Docker一行命令搞定docker run -d --name redis \ -p 6379:6379 \ -v /data/redis:/data \ redis:7-alpine \ redis-server --requirepass your_password --appendonly yes5.2 用Redis Desktop Manager排查信号数据桥接系统跑起来之后最常做的排查动作就是看Redis里到底有没有信号。命令行redis-cli虽然能查但体验一般。我日常用的是Another Redis Desktop Manager这个开源工具比老牌的Redis Desktop Manager对新版Redis的兼容性更好。连接配置主机、端口、密码后你能直观看到所有的key。我常用的排查套路是查看trading:signal:queue:*这个List的长度如果一直增长说明QMT端没在消费如果稳定说明消费正常。进入某个队列看到具体的JSON内容检查code、price、volume字段是否符合预期。查看trading:signal:done:*这些幂等锁key的出现时间判断信号是什么时候被消费执行的。这些可视化操作看起来不起眼但在盘中出问题的时候它就是你能最快确认信号有没有到Redis、到了之后有没有被读走的路径。我系统上线第一周就靠它抓到一个bugQMT端进程因为异常退出没有自动重启信号全堆在Redis List里数量一直在涨一眼就看出消费端挂了。5.3 生产环境Redis的持久化、密码与主从部署虽然是个人/小团队量化系统但我仍然不建议把Redis当成临时内存随便用。一旦QMT端因为网络问题宕机半个小时Redis里堆积的信号就是你的交易连续性保障。持久化策略我建议AOF和RDB都开。AOF保证秒级恢复不丢数据RDB保证重启加载快。日常运维坚持一个月做一次BGSAVE备份rdb文件到独立磁盘。如果后续资金量大了或者你想跑多台QMT做热备Redis建议升级为主从架构。主节点负责写信号从节点负责读或者多个从节点分别给不同的QMT实例消费。极端的做法是再用哨兵做自动故障转移但对于量化桥接这个场景我实测只要做到主从复制 手动切换就已经足够。多花哨的架构往往意味着多一倍的故障点。密码安全这块除了requirepass还有一个关键参数是protected-mode。如果你bind的是内网网卡同时在跑防火墙策略那么Redis不会暴露出去。千万别图方便设置protected-mode no然后绑定0.0.0.0没有密码的Redis在公网上基本就是裸奔被挖矿程序扫描是分分钟的事。6. 实盘前的最后一道检查与我的经验6.1 上线前自查清单这套桥接系统从我搭好到正式拿真金白银跑中间隔了一周。这一周我反复检查的就是以下几个问题也建议大家逐条过一遍代码格式映射是否验证过我跑通第一笔实盘之前先用模拟账号下了几笔测试单专门验证600519.XSHG到600519.SH的转换逻辑。涨跌停、停牌股票有没有过滤QMT下单接口对停牌股会直接废单对涨跌停股可能拒绝集合竞价单这需要在聚宽信号端就做好预处理不要在QMT端等到废单才意识到。交易时间判断有没有做没有交易时段保护的话如果你在凌晨测试时不小心推送了信号QMT也会把它报给柜台然后得到一个拒单结果。我加了简单的A股交易时段判断和节假日判断非交易时间直接不执行。断线自动重连机制有没有QMT端的主循环里如果BRPOP一直超时不能只打印日志后继续——我认为稳妥的处理是连续N次读不到Redis或Redis报错就重启整个消费脚本用系统级服务保证进程常驻。日志和邮件告警有没有我做了双保险程序日志记录每个信号的完整处理过程同时核心异常下单失败、Redis连不上通过简单的HTTP回调或邮件推送通知。做交易最怕的不是出错而是出错了不知道。6.2 运行三个月后回头看这套架构的边界这套聚宽信号RedisQMT xtquant架构稳定运行了三个月期间经历过分红送股、停复牌、涨跌停这种特殊行情。整体上我觉得它的边界在以下三个场景里会被触到第一是高频策略。如果信号的触发频率在秒级甚至毫秒级Redis本身没问题但QMT端逐笔下单的串行处理会成为瓶颈。再加上聚宽策略信号本身往往以分钟K线为基础这类架构更适合分钟级、日频级别的策略。第二是多账户批量交易。一套QMT终端通常对应一个资金账号如果你要管理多个账户就需要在QMT端循环处理多个StockAccount对象信号队列也要拆分成按账户维度隔离的主题。这个改造本身不复杂但要注意每个账户都要单独subscribe否则很容易出现这个账户下单成功、那个账户默默失败的情况。第三个是策略与交易终端的物理距离。我的部署是聚宽和QMT在同一台Windows主机上Redis走本机回环地址延迟几乎没有。但如果未来你想把策略运行放在云服务器上QMT因为柜台对接关系必须放在券商本地的网络环境里两边通过公网连Redis就要考虑网络波动、Redis加密传输、信号数据泄密等问题复杂度会高一个量级。回头看这套方案最让我满意的不是某一个技术点有多高深而是整条链路的健康度可以随时被观测信号有没有到Redis看一眼QMT有没有消费看一眼成交回报有没有回来查一下接口。对于一个独立开发者或者小团队来说这种每一步都可验证的状态比任何花哨的功能都重要。我到现在还在沿用这套架构做日常实盘后续如果再迭代我大概率会把注意力放在信号生成端的策略质量上而不是再去折腾传输层——因为对这条Redis桥来说它的任务已经完成得足够好了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价