
做过聚宽量化的人一旦决定往 QMT 迁移通常都是因为同一个理由聚宽的在线研究环境已经装不下越来越复杂的实盘需求了。我自己也一样回测在聚宽跑得挺舒服但真到要上实盘、要处理盘中分钟级信号、要接入自己的风控和缓存时聚宽那种“网页端跑策略”的封闭感就非常明显。QMT 的好处是本地运行、行情快、支持直接对接券商柜台但代价是很多坑得自己踩。这篇文章不讲怎么安装 QMT也不讲怎么把一个策略原样搬过去而是把我在迁移过程中遇到的、以及帮朋友排查过的高频问题整理成 5 个细节。尤其是 Redis 连接异常那个部分我在生产环境里至少被坑过三回这次一次性说清楚。1. 迁移前想清楚两个平台的底层逻辑差异很多人以为聚宽到 QMT 只是换个 API 名字把 get_price 改成 xtdata 就行。真这么想后面会相当难受。聚宽跑策略是一套“在线托管”的模式你写完策略聚宽负责定时调度、数据推送、下单执行你基本不需要关心进程生命周期。QMT 则完全不同它本质是一个安装在本地或服务器上的行情交易终端策略代码通过内置 Python 解释器跑在终端进程里或者通过 miniQMT 以独立进程方式连接终端运行。这个差异带来三个直接影响第一环境是本地化的。你的第三方库依赖、Python 版本、系统权限全部由自己负责。聚宽里直接import jqdata就有的数据QMT 里要靠xtquant.xtdata从本地数据服务拉甚至要先下载数据。第二策略的执行入口变了。聚宽是框架自动调用initialize、handle_data。QMT 的 Python 策略需要自己写死循环或依赖框架事件很多人第一次写while True: pass时才发现睡着了终端会断开。第三错误处理从“日志面板”变成了“找进程崩溃原因”。QMT 的 Python 进程如果抛异常终端界面常常只给一句报错或者干脆没有任何提示只在 console 里留下一段 traceback。这就逼着我们必须自己补日志、补异常捕获、补进程守护。理解这三点后再开始动代码。下面 5 个细节是我认为迁移路上最值得提前知道的。2. 细节一QMT 安装环境依赖Python 版本和第三方库最容易卡住QMT 内置的 Python 版本通常比较旧不同券商版本可能从 3.6 到 3.8 不等。你本地写好的策略用 pandas 2.0 没问题但 QMT 内置环境可能只支持 pandas 1.3 甚至更老。很多第三方库的编译版本在这个老解释器上压根装不上或者装上了又和 QMT 自带的库冲突。我用过一个券商版本内置 Python 3.7.9numpy 已经是 1.21但再往上就装不上因为 QMT 的可执行环境里某些动态库是静态链接的。这个阶段最典型的报错是Could not find a version that satisfies the requirement pandas2.0.3或者装完了 discover 后ImportError: Something went wrong。实操建议是先搞清楚你手里的 QMT 内置 Python 版本和 pip 路径再决定策略代码的兼容线。# 在QMT终端的Python环境里执行 import sys print(sys.version) # 查pip路径 import subprocess import sys subprocess.check_call([sys.executable, -m, pip, list])不要用你电脑上的全局 Python 去装库更不要用 conda 默认环境去装 QMT 插件。我见过有人把 miniconda 的库目录直接拷进 QMT 的site-packages结果终端连启动都失败最后只能重装。正确的做法有两种一种是用 QMT 终端自带的python可执行文件直接执行-m pip install但要注意这个 Python 可能不在 PATH 里。一般在 QMT 安装目录的.\bin.x64\python\python.exe不同版本路径略有差异。另一种是我现在更推荐的方式如果允许使用 miniQMT 的独立 Python 模式。也就是不依赖 QMT 终端界面直接用你自己本地的 Python 环境推荐 condaPython 3.8 或 3.9连接 QMT 交易服务。这样第三方库随便装版本随意和普通 Python 项目没有区别。前提是你的券商开通了 miniQMT 或“QMT 极简模式”的权限。这个细节最大的坑在于你辛辛苦苦在本地环境跑通了的策略复制到 QMT 内置环境后可能因为一个dataclasses或typing版本问题直接无法 import。所以迁移第一步就应该先在目标环境里跑一个最小依赖测试脚本把 pandas、numpy、xtquant、redis、sqlalchemy 这些核心依赖全部 import 一遍再往下深入。3. 细节二数据接口差异与复权陷阱聚宽的数据接口非常顺手get_price、attribute_history几个函数几乎覆盖所有场景。QMT 里对应的是xtquant.xtdata这个模块既做行情数据服务也做本地数据下载。表面看都能拿到历史K线实际差异不少。第一个坑是返回值格式。聚宽默认给你 pandas DataFrame索引是时间列是 open/close/high/low/volume/amount。QMT 的get_market_data_ex返回的是 dictkey 是股票代码value 是每个字段的 list甚至不是 DataFrame。直接拿聚宽的逻辑来跑大概率在列名上就报错。第二个坑是复权方式。聚宽get_price参数里有 fqpre 做前复权QMT 需要额外调用xtdata.get_market_data前先设置复权参数或下载除权除息信息。这个细节不处理回测和实盘会差很多。不过说实话我建议迁移时先对所有信号和资金曲线做一次“按不复权 复权”的敏感性对比很多因子在复权处理不一致时完全失效。第三个坑是本地数据需要先下载。QMT 默认只保留近期数据如果你需要 5 年日线要么手动在数据管理里下载要么用代码调用xtdata.download_history_data。我在第一次跑回测时直接发现 2020 年之前的数据全是 nan一度以为是接口 bug后来才意识到是本地数据没下全。再补充一个与 Redis 间接相关的点QMT 本地数据读取速度虽快但在策略进程中频繁通过get_market_data_ex拉取分钟数据时可能会卡住主线程。我在做盘中信号运算时会把每日的分钟数据先加载到一个进程内缓存再用 Redis 存跨进程的中间状态。这个组合在后面的迁移中非常实用。4. 细节三交易接口不是拿到账号就能用类外接和权限是分水岭QMT 的下单 API 和聚宽最大的不同是聚宽只要开通了券商权限模拟盘就能随便跑QMT 很多券商要求在客户端里手动开通“极速交易”权限或者单独申请“类外接”接口。所谓“类外接”是指通过 QMT 终端的外接脚本方式绕开手动敲单实现程序化自动交易。如果没开这个权限xttrader下单时会报类似StkType error或委托失败但又不会明确告诉你是权限问题。我当时在模拟盘测试下单一切正常切到实盘环境后发现无法查资金、无法委托反复查代码也没有报错最后才知道是实盘权限没审批。所以迁移之前先和营业部确认清楚三件事MiniQMT 权限、类外接权限、行情站点是否支持本地数据服务。另一个交易接口的坑是下单参数差异。聚宽下单通常传股票代码000001.XSHEQMT 用000001.SZ这种格式而且委托价格、成交类型也要先转换。from xtquant.xttrader import XtQuantTrader from xtquant.xttype import StockAccount from xtquant import xtconstant # 初始化 path rD:\QMT\userdata_mini session_id 12345 trader XtQuantTrader(path, session_id) trader.start() trader.connect() account StockAccount(你的资金账号, STOCK) # 下买单示例 order_id trader.order_stock( account, 000001.SZ, xtconstant.STOCK_BUY, 100, xtconstant.FIX_PRICE, 12.5, 示例策略, )这里有个很不显眼但影响很大的点连接后必须确认trader.check_connect()返回 0 或 true否则所有交易接口都会静默失败。很多人在策略启动时没做这个检查等到盘中才发现账户没连上。我在 QMT 终端里遇到过client is null一部分原因就是 trader 对象没有成功连接特别是多进程下重复创建实例时容易触发。所以交易模块我强烈建议做成一个单例启动时先连接、再校验账户、最后拉一次资金和持仓都正常后才允许策略主循环开始运行。5. 细节四QMT 终端 client is null多半是连接时序问题QMT 的 Python 策略跑在终端进程内部时经常会出现一个让人摸不着头脑的报错client is null。这个报错我在聚宽里从没见过第一次遇到时还以为是环境坏了。排查了很久发现它其实是终端内部与交易后台之间的连接对象还没准备好就被策略代码拿去使用了。典型场景是策略初始化时在xt_trader.connect()之后立刻开始查资金。如果你只调用了 connect 但没有等待回调返回连接成功状态某些版本下就会出现client is null。原因很简单connect 方法本身是异步的终端后台还没有把 client 实例注入到 Python 层的全局变量里。解决办法是加等待逻辑for i in range(30): # 最多等30秒 if trader.check_connect() 0: break time.sleep(1)如果是 miniQMT 独立 Python 模式client is null还可能是因为终端界面没有登录或者登录超时后自动断开而 Python 进程还在运行。这时候需要监控此状态并尝试重新连接。另一个容易出问题的位置是在多线程或定时任务里创建新的 XtQuantTrader 实例。QMT 的 Python 层连接对象不是无限创建的一个进程内创建多个实例、并且没有正确释放后面的实例经常拿不到 client。我建议统一封装为class TraderClient: _instance None _trader None _account None classmethod def get(cls): if cls._instance is None: cls._instance cls() return cls._instance同时把整个策略的交易模块与行情模块分离不要让行情回调里直接调用交易接口。回调里的异常如果没捕获往往会把内部连接状态搞坏下一次再操作就出现 client is null。6. 细节五Redis 连接异常处理实战Redis 在整个迁移里的角色很多人一开始没想到。我在聚宽里不需要持久化策略状态都放在全局变量里。但 QMT 本地多进程或多策略并行时总得有个地方共享信号、缓存中间结果、甚至做分布式锁。Redis 是自然而然的方案。但越自然的方案踩的坑越深。先说 Redis 在量化策略里常见用途存最新行情快照、存策略状态机、做多实例间的锁、发布订阅信号。因为 QMT 本身提供行情服务Redis 更多是用在策略层而不是行情层。我自己的架构是QMT 策略进程把计算好的信号写到 Redis 的 hash 结构里另一个执行进程或者风控进程从 Redis 订阅信号并执行下单两个进程通过 Redis 分布式锁保证同一标的不会重复下单。这个架构很容易因为 Redis 连接异常导致整个策略假死。常见的报错有这几种redis.exceptions.ConnectionError: Error while reading from socket: Connection reset by peerredis.exceptions.TimeoutError: Timeout connecting to serverredis.exceptions.ResponseError: WRONGTYPE Operation against a key holding the wrong kind of valueredis.exceptions.MaxClientsError: max number of clients reached6.1 先从安装和配置开始避坑Redis 本身在 Windows 上没有官方版本很多人第一次用都是在 Windows 上下载老外的迁移版或者从 redis 官网下载 Linux 版再自己编译。这里不是说不能装而是要注意版本和后端配置。我建议不管是 Windows 还是 Linux 服务器下载 Redis 时尽量选 6.2 以上版本因为旧版本在连接池、TLS、ACL 上比较麻烦。Windows 下最简单的做法是用 WSL 或 Docker 跑一个 Redis 容器docker run -d --name redis -p 6379:6379 redis:7-alpine如果你已经在用 Docker那用docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack还能自带 RedisInsight 可视化管理界面。这个东西比命令行友好太多排查 key 类型、过期时间、慢查询都很直观。如果不用 Docker建议用 Redis Desktop Manager 或者 Another Redis Desktop Manager 来可视化查看。我实际用过 ARDM免费且跨平台一旦 Redis 连接异常它比命令行更容易发现是密码错误、端口不通还是服务没起来。6.2 连接异常的第一排查顺序当策略里突然报 Redis 连接异常整个过程最容易犯的错误是一上来就改代码、加重试而不去检查服务本身。排查顺序应该是服务进程是否在跑 - 端口是否监听到 - 密码和 ACL 是否正确 - 防火墙是否挡了 - 网络是否通 - 然后才是代码问题。Linux 下直接用systemctl status redis redis-cli ping ss -lntp | grep 6379Windows 下用services.msc看服务再用redis-cli.exe -h 127.0.0.1 -p 6379 ping。如果 ping 返回 PONG服务基本没问题。如果 ping 不通先看配置里的bind和protected-mode。默认 Redis 只允许127.0.0.1连接如果你的策略进程在另一台机器需要修改bind 0.0.0.0或指定内网 IP同时把protected-mode yes改成 no但生产环境不建议这样裸奔最好设置密码和绑定具体 IP。6.3 连接代码模板连接池加重试降级很多人在策略里每次使用 Redis 都直接用redis.Redis(host...)创建一个新连接并发一高就会把连接数耗尽最终出现max number of clients reached。正确做法是使用连接池。import redis import time from redis.connection import ConnectionPool pool ConnectionPool( host127.0.0.1, port6379, passwordyour_password, db0, max_connections20, decode_responsesTrue, socket_connect_timeout5, socket_timeout5, retry_on_timeoutTrue, ) def get_redis(): return redis.Redis(connection_poolpool)注意decode_responsesTrue否则你 set 一个字符串再 get 出来是 bytes容易踩类型不一致的坑。另外socket_connect_timeout和socket_timeout必须显式设置否则 Redis 服务挂掉时连接操作可能长时间阻塞进而卡住策略主循环。更稳的做法是在关键读写外层加一个带重试和降级的装饰器。比如盘中获取某个信号如果 Redis 连不上就直接读本地线程缓存而不是抛出异常终止策略。def redis_retry(retries3, delay0.5): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for i in range(retries): try: return func(*args, **kwargs) except redis.RedisError as e: if i retries - 1: # 降级到本地缓存或返回默认值 return None time.sleep(delay) return None return wrapper return decorator这个方式能解决绝大多数瞬时抖动问题但不能解决 Redis 服务持续宕机。持续宕机时建议策略状态用本地文件或 QMT 内置缓存兜底等 Redis 恢复后再回放状态。6.4 数据类型用错的辛酸史Redis 提供了五种基本数据类型String、Hash、List、Set、Sorted Set。量化场景里最常用的是 Hash 做多标的状态Sorted Set 做带时间戳的信号队列String 做简单缓存。我在迁移时遇到过WRONGTYPE报错原因是同一个 key之前用 String 存了涨跌幅后来代码改成用 Hash 读写Redis 直接拒绝。这个问题的本质是 key 管理不统一。建议所有 key 统一加前缀比如qmt:signal:000001.SZ、qmt:status:account。同时在使用任何 key 前先通过类型检查或直接用hset写入不要混用。如果确实需要做数据迁移可以临时用一个新 key 代替不要原地replace。Redis 序列化也是容易踩的坑。直接把一个 pandas DataFrame 塞进 Redis最常见的办法是 pickle 序列化但不同 Python 版本的 pickle 兼容性要看版本。更推荐把 DataFrame 转成 JSON 字符串或使用 msgpack。尤其注意 numpy 类型的序列化纯 JSON 无法处理 np.int64需要先 cast。我自己通常是信号、价格快照用 MessagePack 序列化状态机用 JSON成交量缓存用原生字符串加 CSV。宁可多写几行转换代码也不要让反序列化在盘中抛异常。6.5 分布式锁别想当然多进程同时下单时Redis 分布式锁非常能救急。但 QMT 场景有个特殊性如果有多个策略实例同时连到同一个 Redis锁的过期时间必须合理。过期时间太短会导致锁提前释放重复下单太长会导致另一个实例卡死等待。我曾经在一次盘中因锁过期时间设了 10 秒信号处理才 3 秒结果另一个进程也拿到了锁造成重复委托。推荐使用 Redis 官方推荐的 Redlock 思路简化版获取锁时设置唯一 value比如 uuid释放锁时用 Lua 脚本判断 value 再删除保证不会删除别人的锁。import uuid lock_key qmt:lock:000001.SZ lock_value str(uuid.uuid4()) acquired r.set(lock_key, lock_value, nxTrue, ex5) if acquired: try: # 执行下单 pass finally: # Lua脚本安全释放 release if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end r.eval(release, 1, lock_key, lock_value)这里还有一个细节QMT 的行情回调里执行 Redis 操作如果 Redis 网络抖动阻塞了回调线程可能导致行情数据堆积甚至客户端假死。所以我建议所有 Redis 操作都放在独立线程池里执行不要把行情回调直接变成 Redis 同步调用。我当时就是图省事直接在回调里 set结果盘中报了一次连接超时整个终端卡了十几秒。6.6 用可视化管理工具辅助定位排查 Redis 连接异常光看代码不够。我习惯在服务器上装一个 ARDM 或 RedisInsight专门看连接数、内存、慢查询日志。很多时候报max number of clients reached用redis-cli info clients一看connected_clients 已经到几千基本都是某个连接没有正确关闭。代码里每redis.Redis()创建一次连接用完后必须 close 或使用 with 语句。或者干脆统一用连接池就不会有这种问题。另外Redis 的日志也需要打开。Linux 下在 redis.conf 里设置loglevel notice或debugWindows 版在redis.windows-service.conf中同样设置。一旦出现连接异常日志会明确告诉你是因为 AUTH 失败、超时还是被保护的 key。没有日志所有问题都只能靠猜。这个细节我从聚宽迁移过来后真正体会到什么叫“第三方组件引入越多故障点越多”。不过 Redis 一旦配置稳定它对策略架构带来的灵活性还是很值得的。7. 迁移后别急着上实盘先把日志和监控补齐从聚宽迁到 QMT 后我最后悔的事情就是没有第一时间把整套日志系统搭好。聚宽有在线日志QMT 的 console 控制台输出经常被刷新冲掉特别是策略跑一整天后你想看昨晚 2 点的报错控制台早就滚动没了。所以最好在迁移初期就统一用logging写文件同时定期把关键运行指标写入 Redis方便外部监控。我现在的做法是本地日志按天切分同时把每次 Redis 连接状态、是否有异常重试、信号产生时间、下单结果等关键节点写入 Redis 的 Stream 或 String 带 TTL。这样就算 QMT 终端崩溃只要 Redis 没挂就能从 Redis 里看到策略最后运行到哪一步。另外QMT 策略进程偶尔会自己退出尤其client is null或 Redis 异常没被捕获的极端情况。写一个简单的看门狗脚本每隔几分钟检查策略进程是否还在不在就自动拉起能省掉很多半夜惊醒的麻烦。这个脚本也可以用 Python 的subprocess实现不要依赖 Windows 计划任务因为 QMT 终端可能需要登录后才能启动策略。这些工作看起来琐碎但却是从聚宽到 QMT 迁移过程里真正拉开体验差距的地方。聚宽是平台帮你兜底QMT 是你自己当平台运维。准备得越充分迁移后的实盘才会越省心。