资讯动态

从聚宽到QMT:量化策略迁移的5个关键细节与避坑指南

发布时间:2026/9/20 18:04:20 来源:尧图企业网站定制
身边做量化的朋友最近讨论最多的话题就是“要不要从聚宽搬到QMT”。有一说一聚宽在研究层面的体验确实友好数据全、接口顺手、社区资料多但到了实盘阶段尤其是想跟券商柜台直连、降低信号延迟、把策略完整部署在自己可控的环境里QMT几乎是绕不开的选择。这个迁移过程说难不算难说简单也绝对不简单。我自己前后折腾了两周把策略代码翻了个底朝天中间还撞上一个Redis连接异常的问题排查到半夜才彻底解决。今天就把迁移过程中必须知道的5个细节揉碎了讲清楚希望能帮后面的人少踩几个坑。先交代一下背景这套策略是中低频的A股轮动原来在聚宽上跑研究、回测和模拟交易。迁移目标是将策略放到本地的QMT环境里运行用Redis做盘中状态缓存和信号存储。整个改造涉及数据接口、交易接口、运行模式、中间件和本地数据几大块每一块都有和聚宽完全不同的处理方式。零基础迁移也不用慌理解差异之后一步一坑地填基本都能搞定。1. 迁移前先想清楚聚宽和QMT到底差在哪1.1 一个做研究一个做交易聚宽是个典型的研究型量化平台它的核心价值是让你快速验证思路。写策略的时候不需要关心服务器在哪、数据从哪里来、账户怎么连一句get_price()就能拿到结构化好的行情财务数据也齐回测撮合模型做得比较理想化适合做因子研究、策略迭代和逻辑验证。QMT就不一样了它本质上是一个本地交易终端数据落在本地策略逻辑通过Python回调运行底层的处理是C做的行情推送、下单链路天生比云端平台快。代价是很多细节要自己负责——数据要下载、依赖要装、账户要管理、进程要守护、掉线要重连甚至中间的缓存队列都得自己去维护。所以迁移前的第一件事是调整心态聚宽到QMT不是复制粘贴策略代码而是把研究原型改造成生产环境下的交易程序。这个心态不摆正后面每一步都会觉得别扭。从另一个角度看这两套工具是可以共存的。我在实际操作中保留了一个习惯继续用聚宽做日度因子分析和策略迭代QMT负责实盘执行。两边通过数据库或文件做信号交换互不干扰。这样既保留了聚宽的研究效率又拿到了QMT的实盘能力也算是一种过渡方案。1.2 云端托管和本地终端的本质区别聚宽是云端的上传代码之后由平台的服务器定时调度、执行、推送结果。它的好处是部署门槛低但坏处是你对运行环境没有控制权也没法真正介入下单链路。QMT的策略跑在本地你完全可以决定跑在哪台机器上、用什么依赖版本、如何跟数据库交互。这种自主可控对中低频策略来说价值明显盘中出了任何问题都能第一时间看日志、翻数据、做排查。不过也正因如此本地环境的稳定性就变成了你的责任Redis这类中间件才会出现在整个技术栈里。我个人觉得如果是资金量不大、策略频率不高、不想折腾环境的人继续用聚宽完全没问题。但如果你倾向于“把策略做成一套自己能完全掌控的流程”这个迁移是值得做的。下面要讲的5个细节基本就是迁移中的核心堵点。2. 细节一数据接口的返回值结构习惯全都要改2.1 聚宽的数据习惯太舒服了聚宽的数据接口做得非常傻瓜化最常用的get_price()直接返回一张DataFrame索引是时间列是open/high/low/close/volume这些字段。取数据基本就是一行df get_price(600000.SH, count100, frequency1d, fields[close, volume]) last_close df[close].iloc[-1]之后不管你是做均线、算动量、还是画图拿到的都是一个活在内存里的标准表格结构心智负担很低。加上聚宽的get_fundamentals()能够直接拿到财务指标、估值数据研究阶段几乎不用操心数据清洗的问题。习惯了这套操作之后迁移到QMT的第一天就会觉得别扭——它的xtdata接口不是不给数据而是给的格式比较贴近底层需要自己转换。2.2 QMT的xtdata返回的是dict套DataFrameQMT的数据接口核心是xtdata我用的最多的是get_market_data_ex()。这个函数返回的并不是一张简单的DataFrame而是一个dict。不同版本下dict的键可能是股票代码也可能是字段名这点比较坑。常见的调用方式如下from xtquant import xtdata # 建议先下载历史数据到本地 xtdata.download_history_data(600000.SH, 1d, 20240101, 20240401) # 读取行情 data xtdata.get_market_data_ex( field_list[close, volume], stock_list[600000.SH], period1d, start_time20240101, end_time20240401 ) # 返回结构示例{600000.SH: DataFrame} df data[600000.SH] last_close df[close].iloc[-1]如果你传的是多只股票返回的dict就会包含多个key每个key对应一只股票的DataFrame。按股票代码取数据就可以。但有个特殊情况某些版本里如果你不传field_list或者只传部分参数它返回的dict可能是按字段分组的也就是说key是close、volumevalue是按股票和时间组织的表格。我对这个变化的建议很简单拿数据之后先print(data.keys())看一眼结构再决定怎么取值不要想当然。2.3 别忽略复权、时区和停牌处理聚宽的日线数据默认帮你处理了复权选项你只要指定fqpre或fqpost就行。QMT这边同样有dividend_type参数比如front、back、none但在使用前必须清楚自己的策略是前复权还是后复权否则回测和实盘数据对不上。更隐蔽的坑是时区。QMT的K线时间戳用的是本地时区的datetime当你在DataFrame里看到index是一个datetime64或时间字符串时记得确认它的时区属性。我的策略在迁移时出现过盘中信号计算偏移一小时的问题最后定位到就是因为把时间当成UTC处理了一遍。另外QMT本地数据里停牌日的K线可能缺失也可能填充取决于数据版本。在迁移时建议对目标股票池做一次停牌日清洗保证策略拿到的数据格式一致。这些细节在聚宽里都被平台封装好了到了QMT之后全得自己面对。3. 细节二交易接口的语义差异比你想的大3.1 按金额下单 vs 按股数下单聚宽的下单接口设计得很人话你不需要关心一手是多少股、当前价格是多少直接表达你的目标就可以# 把仓位调整到5万元市值 order_target_value(600000.SH, 50000) # 或者直接调整目标股数 order_target(600000.SH, 1000)QMT的底层接口就直白得多它是按股数下单的from xtquant.xttrader import XtQuantTrader from xtquant import xtconstant # 创建交易通道 trader XtQuantTrader(rD:\qmt\userdata_mini, int(time.time())) trader.start() trader.connect() # 账户订阅 account StockAccount(你的资金账号) trader.subscribe(account) # 按股数下单注意需要自己处理手数取整 volume int(50000 / price / 100) * 100 trader.order_stock(account, 600000.SH, xtconstant.STOCK_BUY, volume, xtconstant.FIX_PRICE, price, strategy, memo)这里有好几个必须注意的地方。首先是手数取整A股一手是100股如果你不取整报单可能直接被柜台拒绝。我在迁移时写了个专门的工具函数把所有目标金额统一转成实际可报的股数确保计算结果和订单数量对得上。其次是price_type可以是限价FIX_PRICE、最新价、对手价等多种类型。聚宽里你只要说“按市价买”平台会自动处理QMT则需要你非常明确地指定价格类型和价格。实盘环境下用对手价和最新价在流动性不同的股票上表现差异很大这块需要自己测试后固定下来。3.2 账户、连接和交易状态管理聚宽里的context.portfolio已经帮你封装好了总资产、持仓、可用资金这些信息你直接读就行。QMT这边需要自己管理交易通道的生命周期。如果策略需要异步获取成交回报一般要自定义一个回调类class MyCallback(XtQuantTraderCallback): def on_stock_order(self, order): print(f委托更新: {order.stock_code}, 数量 {order.order_volume}) def on_stock_trade(self, trade): print(f成交回报: {trade.stock_code}, 价格 {trade.traded_price}) trader.register_callback(MyCallback())回调机制本身不复杂但多了这层之后你的逻辑就不能写成“下单后立刻假设成交”。聚宽的回测里order_target_value之后下一行代码读持仓就能看到变化QMT实盘中成交回报是异步的可能会晚几十毫秒甚至几秒只能通过回调或轮询持仓来确认。我当时在这块踩了一个很实际的坑策略里下单后马上调用query_stock_asset()查资金发现可用资金还是下单前的值导致重复下单。后来改成维护一个“委托中”标志位等回调确认成交后再更新状态问题才解决。3.3 回测和实盘的行为差异要提前接受聚宽的回测通过率会给你一种“实盘也能这样”的错觉。QMT虽然也有回测框架但它同样是在本地上跑的撮合逻辑和实盘柜台相比仍然有差距。迁移后最好的验证方式不是盯着回测曲线而是先跑一段时间的仿真交易把回测信号、仿真信号、实盘信号放在一起比对。很多人在迁移后第一个月会焦虑觉得收益不如聚宽回测。这大概率不是迁移出了问题而是回测模型本身就偏乐观。QMT的价值是让你用更真实的路径去执行策略而不是让收益曲线变得更好看。想清楚这点心态就不会崩。4. 细节三策略触发方式从“定时任务”变成“行情回调”4.1 聚宽的定时任务很简单聚宽的时间调度非常直观初始化时声明好几点干什么就行def initialize(context): run_daily(open_position, 09:35) run_daily(close_position, 14:50) def open_position(context): # 早上开盘后调仓 pass这种模式的优点是逻辑清晰你不用关心K线什么时候到到点就执行。缺点也很明显——它在云端执行调度精度受平台影响且和行情变化的耦合度不高。4.2 QMT的handlebar机制QMT的Python策略入口主要是init()和handlebar()。handlebar会在每根K线生成时被调用相当于“行情过来一次你就跑一次逻辑”。这在分钟级或日线级策略里非常常用但它的触发频率和你的K线周期强相关。def init(ContextInfo): pass def handlebar(ContextInfo): bar_time ContextInfo.get_bar_timetag(ContextInfo.barpos) # 判断是否到了当天的操作时间点再执行所以如果你原来在聚宽里写的是“每天9:35执行一次”到了QMT里不能直接照搬你得在handlebar里加时间判断。我通常是先判断当前K线时间是不是目标时间窗口再进入信号计算逻辑。另外一个容易被忽视的点是handlebar只在策略运行期间触发如果你连接的是实时行情那开盘期间它会一直工作但如果是回测它会把历史K线按顺序回放。两种情况本质是同一种模型但盘中你还要额外关心断线重连的问题。QMT的行情连接一旦断开handlebar就静默了需要一个看门狗逻辑定时检查并重连。4.3 部署环境的依赖问题这点对应网上大家经常搜的“qmt安装环境依赖”。QMT在Windows下安装相对顺滑正常装完就能用。但很多人想把策略部署到Linux服务器上这时候就麻烦了。你可以用xtquant配合miniQMT模式跑策略但前提是你得先把主终端在Windows机器上登录一遍保证账号状态正常然后在Linux上装好Python依赖。我就遇到过在Linux上装完库之后xtdata初始化报错的问题最后发现是缺少某些底层依赖库和库文件路径不对。如果你的服务器是干净环境建议提前把libgomp、unixODBC这类常见依赖装上再检查一下xtquant包是否完整。另外本地部署还意味着你必须有稳定的网络环境。虽然QMT断线后会自动重连但重连期间策略是不跑的。中低频策略还好如果是日内策略这个空档期就非常致命。加上聚宽不是本地环境不存在这个问题所以很多人会低估“网络稳定性”在迁移后的重要性。5. 细节四Redis连接异常处理与缓存设计5.1 为什么要引入Redis聚宽平台内部帮你打理好了所有中间状态到了QMT本地环境多个模块之间要共享数据就变得特别现实。比如盘中策略A负责计算信号策略B负责执行下单这两个进程之间怎么通信再比如策略重启后原来的持仓状态、当天已买标识、当日操作记录都存哪里我选择用Redis来承载这些功能。它是内存级数据库读写速度快数据结构也丰富非常适合做缓存和进程间通信。而且在量化场景里Redis分布式锁也是常见的并发控制手段可以避免多个进程同时下单或者同时修改状态。需要注意的是Python里使用Redis时存进去的Python对象必须序列化。我经常看到有人直接把dict塞给redis.set然后报错这是因为Redis只接受字符串或字节流。实际开发中建议根据场景选择JSON序列化或者pickle序列化import redis import json r redis.Redis(host127.0.0.1, port6379, db0, decode_responsesTrue) # 存一段JSON strategy_state {day: 20240401, position: 600000.SH, shares: 5000} r.set(strategy:state, json.dumps(strategy_state)) # 读出来并反序列化 state json.loads(r.get(strategy:state))如果存的是自定义对象可以用pickle但pickle在跨语言跨进程场景下兼容性差能不用尽量不用。5.2 Windows下安装Redis的坑很多人第一次在Windows上装Redis就卡住了。Redis官网已经不再提供Windows官方安装包现在常用的是年久但稳定的tporadowski/redis开源Windows构建版或者是直接开启WSLWindows Subsystem for Linux装Linux版本。我的推荐是如果只是开发调试下载Redis-x64-5.0.14.1.zip或类似版本解压后直接运行redis-server.exe --maxmemory 512M如果你是在Linux服务器上正式部署那直接用官方Docker镜像更方便一条命令搞定不用折腾依赖docker run -d --name redis -p 6379:6379 -v /data/redis:/data redis:7.0 --appendonly yes为什么建议加--maxmemory因为Redis默认内存不设上限如果当缓存用而没配淘汰策略内存会被慢慢撑爆一旦内存满了Redis会表现为“客户端连接正常但读写全部超时”这个现象极其隐蔽一般人都不会第一时间想到是内存问题。5.3 连接异常处理从切换到重试标题里特意提到了Redis连接异常处理这块是我踩得最深的地方。几个典型故障我都列在下面第一种ConnectionRefusedError: [WinError 10061]或者是redis.exceptions.ConnectionError。这种通常是服务没启动、端口不对或者防火墙拦掉了6379端口。排查方式很直接先redis-cli ping如果返回PONG说明服务正常再去检查Python代码里的host和port。Windows下还要特别留意默认只能本机访问外部IP会被保护模式拦截。第二种TimeoutError连接超时。很多情况下是服务端连接数满了或者是网络包被防火墙丢掉了。开始时我以为是自己数据传输太多排查到最后发现是连接没释放每次短连接都new一个客户端最终把连接池打满后续请求全部卡死。原本我在策略里只要写数据就redis.Redis(...)新建一个连接跑一段时间后连接数飙到几百甚至上千。后来全部改成用一个全局连接池设置socket_connect_timeout和socket_timeout并用一个带重试的客户端封装彻底解决了问题。这里分享一个我目前在用的工具类可以直接抄作业import redis import time from redis.connection import ConnectionPool class RedisClient: def __init__(self, host127.0.0.1, port6379, db0, passwordNone, max_connections20, timeout3, retries3): self.pool ConnectionPool( hosthost, portport, dbdb, passwordpassword, max_connectionsmax_connections, socket_connect_timeouttimeout, socket_timeouttimeout, decode_responsesTrue ) self.retries retries def get_conn(self): return redis.Redis(connection_poolself.pool) def set_with_retry(self, key, value, exNone): for attempt in range(self.retries): try: conn self.get_conn() conn.set(key, value, exex) return True except Exception as e: print(fRedis set error, attempt{attempt 1}, error{e}) time.sleep(0.2 * (attempt 1)) return False def get_with_retry(self, key): for attempt in range(self.retries): try: conn self.get_conn() return conn.get(key) except Exception as e: print(fRedis get error, attempt{attempt 1}, error{e}) time.sleep(0.2 * (attempt 1)) return None核心思路就三条连接池复用、超时设置、失败重试。这三点做好Redis连接这个环节基本就稳了。第三种异常是ResponseError: NOAUTH Authentication required这是Redis配置了密码而你没传密码。解决方案就是在连接参数里显式填password或者用CONFIG SET requirepass重新配置。不要在文档里硬编码密码建议从环境变量或者配置文件读取。第四种是max number of clients reached和第二种原因类似也是连接泄漏。遇到这种情况先redis-cli info clients看当前的连接数再检查代码是否是每次请求都新建客户端。正经项目里连接池必须全局唯一。除此之外盘中长时间运行后连接被服务端断开也是真实存在的现象。Redis默认有超时配置timeout 0不主动断开但有些环境配置了非零值策略跑几小时后就出现偶发写数据失败。我的应对方式很简单就是重试机制配合心跳每几分钟在策略里执行一次ping保持连接活跃。5.4 Redis可视化管理工具排查Redis问题的时候有个可视化工具会顺手很多。我用得比较多的有Redis Desktop Manager和Another Redis Desktop Manager两者功能类似都可以直观查看key、查看过期时间、手动修改值、观察内存占用。特别是在验证“策略是否把状态正确写入Redis”的时候可视化工具比命令行高效得多。比如策略说它已经写入了strategy:state你直接查这个key能看到对应的JSON结构数据有没有写对一目了然。这也是我在迁移过程中反复使用的调试手段建议上手时就把这个工具装好。6. 细节五本地数据完整性与财务数据补齐6.1 历史数据必须先下载聚宽跑回测的时候数据是平台准备好的你不需要关心。QMT则要求你先下载历史数据到本地不然xtdata.get_market_data_ex()返回的数据范围可能不完整。我自己在迁移初期就犯过一个错拿get_market_data_ex拉数据发现某只股票的K线数量比其他股票少了一截还以为是策略逻辑问题折腾半天才发现是本地历史数据没下全。常规做法是提前批量下载from xtquant import xtdata stock_list [600000.SH, 000001.SZ, 601318.SH] xtdata.download_history_data2( stock_list, period1d, start_time20200101, end_time20240401 )download_history_data2支持批量多股票下载速度很快。下载完之后再跑回测或实盘能避免大量盘中读不到数据的尴尬。注意这个操作也可能遇到网络问题尤其是数据量大的时候建议分批次下载下载完成后打印返回值或检查本地文件目录确认所有股票都下载成功。6.2 财务数据差异让人头疼聚宽里最爽的除了行情就是财务数据。一个get_fundamentals()就能拉到PE、PB、ROE、净利润同比、毛利率这些指标而且非常规整。QMT的xtdata.get_financial_data()虽然有但字段覆盖度和易用性跟聚宽不是一个量级某些字段要么取不到要么只能取最新值没法做历史序列分析。如果你手上的策略依赖财务因子比如PE或ROE的排名筛选迁移前就要做仔细评估。我的做法是保留一条单独的财务数据同步链路在聚宽上导出因子的历史序列存到本地数据库或文件中QMT策略启动时直接读取使用。这样既保留了聚宽的数据质量又绕开了QMT财务数据不全的问题。6.3 数据对齐校验迁移完成后一定要做数据对齐校验。我建议选取同一只股票、同一段时间分别用聚宽和QMT拉取日线收盘价对比结果是否一致。不要只看最后一天要看整个序列的差异比例尤其是除权除息日附近。复权方式不同、停牌处理不同、数据类型不同都可能导致两条K线在个别日期有差异。如果策略对价格敏感这种差异会直接影响信号计算。我通常会写一个简单脚本比较两个平台的close序列允许误差设置为0.1%超过就报警。这个检查做一次后面就能静心跑策略。7. 常见问题速查表为了方便日常排查我把迁移过程中遇到的问题整理成了一张速查表里面也有网上搜得到的“QMT终端 client is null”这类报错。7.1 Redis连接异常速查异常现象常见原因排查与处理ConnectionRefusedError: [WinError 10061]Redis服务未启动或端口/主机配置错误命令行redis-cli ping验证服务状态检查host和portTimeoutError网络不可达、服务端卡顿、连接数打满检查防火墙、info clients连接数、改用连接池和重试NOAUTH Authentication required服务端配置了requirepass客户端没传密码连接参数中配置正确的passwordmax number of clients reached客户端连接泄漏使用全局连接池限制max_connections检查代码是否反复建连写数据偶发失败长时间运行后连接过期增加心跳、设置socket_timeout和重试机制内存持续上涨未配置maxmemory和淘汰策略redis-server --maxmemory 512M --maxmemory-policy allkeys-lru7.2 QMT常见报错速查报错常见原因处理方式client is null主终端未启动或外部脚本没有正确连接到QMT进程先打开QMT终端并登录再运行Python脚本检查session_id是否冲突account invalid资金账号错误或未订阅账户确认账号类型和资金账号调用trader.subscribe(account)order rejected股数未按手取整、停牌、涨停不可买打印错误回报检查下单数量是否为100的整数倍xtdata keyerror股票代码格式错误或数据未下载使用带后缀的代码如600000.SH提前执行下载接口handlebar不触发行情连接断开、策略未启动检查终端连接状态配置看门狗自动重连结尾最后再说点实在的。迁移过程中最消耗精力的其实不是某一行代码怎么写而是两套平台的思维模式转变——从“平台帮我搞定一切”到“我自己负责环境、数据和容错”。Redis连接异常这类问题在聚宽里根本不会遇到但一旦到了本地环境它就是你的基础设施出问题时不光要会修更要在设计阶段就避免。我个人的体会是把连接池、重试、心跳、序列化这四件套提前做好Redis这块能省掉80%的盘中烦恼。至于QMT本身的数据和接口差异多用几周自然就顺手了。如果你正准备迁移别急着把代码全部重写先拿一只股票、一个简单的均线策略跑通全链路再逐步替换成你的真实策略整个过程会从容很多。

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

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

免费获取报价