
回测曲线再漂亮实盘跑不起来都是白搭。这次聊的是4.2.2这块内容QMT实盘部署核心目标只有一个——让策略真正在实盘环境里跑起来。我自己从backtrader回测切到QMT实盘时踩了不少坑行情断流、重复下单、忘记处理停牌、日志打了一整天结果全是无效记录每一步都可能让策略报废。这篇博文会把部署前准备、系统接入、交易主循环设计、稳定性建设到常见问题排查完整过一遍适合已经写好策略、正打算接入实盘的Python量化交易者也适合想了解QMT部署流程的初学者参考。1. 部署前准备环境确认与策略改造把策略从回测搬到实盘最忌讳的就是“回测代码拿来直接用”。QMT部署前有两件事必须想清楚用哪个端跑以及策略逻辑怎么从历史数据驱动改成实时事件驱动。这两件事没做透后面所有步骤都是在给线上埋雷。1.1 先搞清楚要部署到哪个端QMT部署路径通常有两条直接在QMT交易终端自带的Python环境里写策略以及通过MiniQMT模式把外部Python程序连接进去。实盘部署我强烈建议走MiniQMT这条路原因是策略代码完全在自己熟悉的IDE里维护版本管理、第三方库安装、日志处理都自由得多不用被终端内置编辑器限制。MiniQMT本质是提供了一套Python接口库——xtquant外部程序通过它连接QMT交易客户端然后做行情订阅、账户查询、委托下单。需要注意大部分券商的QMT版本都需要在客户端里开通“Python量化和MiniQMT运行环境”权限同时设置一个独立的交易连接密码。这个密码和登录密码是两回事后面初始化连接时会用到。部署前还有一件容易被忽略的事确认你的QMT客户端是券商定制版还是迅投原版。不同券商的定制版本在接口细节、数据权限、交易前置地址上都有差异最稳妥的做法是直接找开户券商要一份支持文档确认你用的这个版本支持xtquant外部调用而不是在通达信、同花顺的扩展API上硬套。1.2 策略逻辑从“算好再买”改成“来了再算”回测策略和实盘策略在逻辑推导上可以完全一致但执行方式差异很大。回测典型写法是把整个时间段的K线拉下来一次性算出所有均线、指标然后逐根K线模拟买卖。实盘环境下你根本不会有“未来数据”每时每刻能拿到的只有截至当前的最新行情。举一个最经典的双均线例子。回测时你会这样写df[ma5] df[close].rolling(5).mean() df[ma20] df[close].rolling(20).mean() df[signal] 0 df.loc[df[ma5] df[ma20], signal] 1 df.loc[df[ma5] df[ma20], signal] -1但实盘时行情是源源不断推过来的你需要在每一次收到新K线或新tick时把最新收盘价追加到序列末尾再重新计算最新一组均线然后判断是否发生“金叉”或“死叉”。改造后的逻辑大概长这样def calc_ma(candles, period): if len(candles) period: return None closes [c.close for c in candles[-period:]] return sum(closes) / period def check_cross(candles): if len(candles) 21: return 0 ma5_now calc_ma(candles, 5) ma20_now calc_ma(candles, 20) ma5_prev calc_ma(candles[:-1], 5) ma20_prev calc_ma(candles[:-1], 20) if ma5_now ma20_now and ma5_prev ma20_prev: return 1 if ma5_now ma20_now and ma5_prev ma20_prev: return -1 return 0这个函数就是实盘策略的核心素材每次都基于“当前已经发生的K线”计算信号不提前知道下一根K线是什么。把回测代码里的信号计算部分抽出来改写成这种“增量计算最新状态判断”的形式是QMT实盘部署中最关键的一步。1.3 因子与历史数据提前落地如果你的策略不只用均线还依赖外部因子比如基本面数据、另类因子、或者自定义指标那么部署前必须把因子数据落成本地文件或者数据库。原因很简单开盘后每一秒都很贵盘中临时下载历史因子、重新计算指标不仅让策略错过最佳入场时机还可能因为网络阻塞导致程序卡死。用xtquant自带的xtdata接口可以提前把历史K线数据下载到本地缓存。一般在正式部署前一天晚上跑一次数据下载任务把策略涉及的标的日线、分钟线都拉到本地。上线后盘中只需要增量订阅tick或者最新K线策略信号的计算延迟能控制在毫秒级。这里还有一个容易踩的坑回测的时候你可能用了复权价格但实盘委托下单时用的是原始价格。所以策略在实盘运行时务必把“信号计算用的复权价”和“下单用的实际价格”分开处理否则可能出现回测里价格很顺滑、实盘里下单价格和信号价格对不上的情况。2. 系统接入让QMT先认你这个外部程序这个阶段的目标是让外部Python程序通过xtquant成功连上QMT终端并且能查到账户资产、持仓和可用资金。表面看只是几个初始化的调用实际跑起来有很多细节我按步骤拆开讲。2.1 xtquant安装与第一个连接先说安装。不同券商给的xtquant获取方式不太一样常见的有两种一种是在QMT安装目录下找到类似bin.x64/xtquant的文件夹把这个路径加入Python的site-packages另一种是直接用pip安装xtquant。我建议优先用券商提供的版本因为交易所接口、数据协议这类底层都在变券商配套的xtquant版本经过适配兼容性更有保证。初始化连接的基本代码框架是这样的import time from xtquant.xttrader import XtQuantTrader from xtquant.xttype import StockAccount # 这里的path填QMT的userdata路径以实际安装目录为准 path rD:\qmt\userdata_mini session_id int(time.time()) xt_trader XtQuantTrader(path, session_id) xt_trader.start() xt_trader.connect() account StockAccount(你的资金账号) xt_trader.subscribe(account) # 查询资产确认连通 asset xt_trader.query_stock_asset(account) print(asset.cash, asset.total_asset)几个容易出问题的点提前说明path不是QMT的安装根目录而是存放用户数据的userdata_mini目录不同券商的路径前缀可能不同最好在开发环境里打印返回结果确认。session_id必须唯一尤其是同一台机器上跑多个实例时重复的session_id会导致连接互相踢掉。用时间戳生成基本够用。StockAccount传入的是你的资金账号不是普通登录账号注意区分。账号类型默认是普通股票账户如果你用信用账户初始化时要传对应的账户类型。2.2 交易时段与账户状态判断连接成功之后不要立刻写一个“无限循环判断信号”的脚本就完事。实盘程序必须具备基础的交易时段判断能力。A股每天930到1130、1300到1500是连续交易时段915到925是集合竞价节假日和周末不交易。如果你不在代码里做判断很容易出现凌晨三点触发买入信号、程序真的把单子报出去——这种事在实盘里真的有人遇到过。判断交易时段有两种做法。一种是写死时间区间简单但遇到节假日会出错。另一种是调用xtdata里的交易日历接口比如from xtquant.xtdata import get_trading_calendar先拉取一年内的交易日列表判断当天是否为交易日再叠加时间区间判断这样能正确处理调休和法定假期。我自己的做法是启动时先拉取交易日历缓存到本地一个set里盘中所有判断都以“本地交易日历 当前时间”为准杜绝瞎下单。2.3 下单接口与资金持仓管理xtquant的下单接口是order_stock最基本的买入调用看一下from xtquant import xtconstant order_id xt_trader.order_stock( account, 600000.SH, xtconstant.STOCK_BUY, 100, xtconstant.FIX_PRICE, 12.50, my_strategy, first_order )参数分别是账户、证券代码、买卖方向、委托数量、价格类型、价格、策略名、备注。有几个细节新手很容易踩股票代码要带后缀上交所是.SH深交所是.SZ不带后缀或者写错会导致下单失败。A股买入数量必须是100股整数倍卖出时可以卖出零股但系统一般会先按100股批次处理。价格类型区别很大FIX_PRICE是限价单LATEST_PRICE是市价单。回测里可以假设按收盘价成交实盘如果你想控制成交成本尽量用限价单同时设置合理的超价处理逻辑避免价格稍微一动就永远成交不了。下单前千万别忘了查持仓和资金。最典型的错误策略信号触发后程序没查当前持仓就再次买入导致一个票买到超过自己设定的仓位上限。我习惯在每次下单前做四层检查当前可用资金是否充足、当前持仓是否已到目标仓位、是否有未完成的挂单、当前时间是否可交易。这四层检查全部通过才真正调用order_stock。3. 让策略跑起来交易主循环的设计连接打通、账号能登录之后剩下的事情就是设计“从行情到订单”的完整通路。这个过程的核心不是写几个函数而是要设计一个可靠的消息流转框架行情进来、信号算出、风控检查、下单、回报处理。3.1 实时行情订阅回调和轮询怎么选xtquant的行情订阅有两种常见姿势。第一种是回调式调用subscribe_quote并传入回调函数行情每变动一次回调函数就会被调用一次适合需要快速响应的策略。第二种是轮询式程序每隔一段时间主动去拉最新行情适合低频策略比如每分钟判断一次趋势。我个人的经验是对大多数个人量化场景轮询已经足够而且轮询比回调更容易控制节奏。回调式最大的问题是回调函数运行在主接收线程里一旦回调里执行了耗时操作比如数据库写入、网络请求会阻塞后续行情的接收导致行情越积越多延迟急剧上升。如果你一定要用回调记得在回调里只把最新行情塞进队列由另一个线程消费队列、执行信号计算和下单。用xtdata订阅行情的回调模式大概是from xtquant.xtdata import subscribe_quote def on_quote(datas): # 这里只做一件事塞队列 quote_queue.put(datas) subscribe_quote([600000.SH, 000001.SZ], on_quote)然后在策略主线程里while True: quote quote_queue.get() signal check_cross(append_candle(quote)) if signal ! 0: place_order_if_ok(signal, quote)这样就把“行情接收”和“策略决策”解耦开行情接收永远不被阻塞策略决策按自己节奏跑。3.2 定时任务调度别用sleep硬扛实盘策略经常需要在特定时间点做事开盘前准备、盘中定期扫描、收盘后生成当日交易摘要。很多人第一反应是在主循环里time.sleep(30)然后扫一遍但这种方式在开盘前、收盘后这些时间点特别容易失控——进程一直空转费资源不说调度逻辑还容易越写越乱。推荐的做法是用APScheduler这类调度库把时间点任务和循环轮询拆成两套机制。举个例子部署一个低频趋势策略可以这样调度from apscheduler.schedulers.blocking import BlockingScheduler scheduler BlockingScheduler(timezoneAsia/Shanghai) scheduler.add_job(prepare_data, cron, day_of_weekmon-fri, hour9, minute10) scheduler.add_job(run_daily_check, cron, day_of_weekmon-fri, hour15, minute30) scheduler.start()prepare_data负责开盘前的数据准备检查连接、拉取当日初始化数据、更新持仓、清理昨晚的日志临时文件。run_daily_check负责收盘后处理生成当日交易报告、计算当日盈亏、整理未完成订单。盘中信号则继续用主循环轮询互不干扰。这个结构看起来简单但实际维护时你会感激这种代码分层——功能点之间不纠缠出问题容易定位。3.3 从信号到订单的完整链路一个稳定的实盘策略从收到行情到订单确认回报应该有一条明确的链路。每收到一帧最新行情依次做这几件事把最新K线追加到内部缓存更新均线等指标计算。计算当前信号买入、卖出、无操作。如果产生了信号先检查该方向是否已经有未完成订单防止重复下单。检查持仓、资金、交易时间全部通过才下单。下单后把订单号记录到内存字典同时写入日志。等待委托回报回调更新订单状态如果超时未成交视策略需要做撤单或重发处理。这里面最容易出问题的就是第3步。比如网络延迟导致策略重复收到同一根K线如果没做幂等判断一个买入信号可能被触发两次产生两笔买入订单。我习惯给每个信号设置一个全局递增序号在下单前检查“当前信号序号是否已经处理过”处理过就直接跳过。这个序号机制看起来笨但真的能挡掉很多隐性故障。4. 稳定性建设断线重连、日志监控与风控前置策略能不能赚钱是一回事跑得稳不稳定是另一回事。QMT实盘部署里有一个残酷现实终端会断线、网络会波动、进程会崩溃。如果你没有一套兜底机制策略可能从某一天起就不更新持仓状态了而你还以为它在正常跑。4.1 断线重连与异常兜底xtquant的连接状态可以通过is_connected()检查断线后重新调用connect()可以恢复连接。但有个细节重连成功之后订阅关系会丢失你必须重新订阅账户和行情重新查询一次持仓和资金把所有内部状态恢复到连接前。我自己的重连逻辑是放在一个独立线程里的定时检查任务每5秒检测一次def watchdog(): while True: try: if not xt_trader.is_connected(): log.warning(连接断开尝试重连) xt_trader.connect() time.sleep(1) if xt_trader.is_connected(): # 重新订阅账户 xt_trader.subscribe(account) # 重新查询持仓修正本地状态 refresh_positions() # 重新订阅行情 reload_quote_subscription() except Exception as e: log.exception(watchdog异常: %s, e) time.sleep(5)注意断线重连后的状态恢复一定要做好。持仓状态如果和真实账户不一致下一次下单时的仓位判断就会出错轻则多下单重则仓位失控。4.2 日志与监控出问题时唯一能依靠的东西实盘策略出了bug你不会有第二次机会去现场抓问题所有线索只能来自日志。很多个人量化交易者不重视日志只在控制台print程序跑几天之后窗口早就关掉了。我的建议是至少做到三件事第一日志写到文件用按大小或按日期滚动的方案避免单个文件无限增长。第二每条关键操作都记录结构化信息时间、动作、证券代码、价格、数量、订单号、错误信息。举个例子logging.info([ORDER] time%s actionBUY code%s price%.2f volume%d order_id%s, now, code, price, volume, order_id) logging.info([FILL] time%s code%s traded_price%.2f traded_volume%d order_id%s, now, code, fill_price, fill_volume, order_id) logging.error([ERROR] time%s msg%s context%s, now, exc, context)第三把日志和实时告警分开。日志负责沉淀告警负责提醒。实盘阶段遇到委托失败、账户资金不足、连续多笔异常撤单都应该在日志里打ERROR级同时把核心错误信息单独写到一个alarm.log文件里方便每天收盘后检查。想做得更细还可以在程序里加一个“当日连续亏损超过阈值就暂停交易”的状态开关这个开关不仅能控制风险也相当于一个内置的“熔断模块”。4.3 风控前置在策略外面再套一层保险策略本身可能已经很完善但实盘里总会出现策略没考虑到的特殊情况停牌复牌、涨跌停、除权除息、临时停牌。所以必须在策略之外再加一层独立风控。这层风控不依赖策略逻辑是纯粹的风险限制单笔下单金额不能超过总资产的固定比例比如5%。单票持仓市值不能超过总资产的固定比例比如20%。当日累计亏损超过设定阈值停止开新仓。策略信号触发后必须满足“距离上一次同方向下单超过N秒”的冷却条件。代码实现可以是策略下单前调用一个风控类class RiskManager: def __init__(self, max_order_pct, max_pos_pct, max_loss_pct): self.max_order_pct max_order_pct self.max_pos_pct max_pos_pct self.max_loss_pct max_loss_pct def check(self, asset, position, order_amount, daily_pnl): if order_amount asset.total_asset * self.max_order_pct: return False, 单笔金额超限 if position.market_value asset.total_asset * self.max_pos_pct: return False, 单票仓位超限 if daily_pnl -asset.total_asset * self.max_loss_pct: return False, 当日亏损超限 return True, ok这个风控类一定要独立于策略文件千万不要把风控逻辑和策略代码写在同一个函数里。理由很简单哪天你改策略信号不小心把风控同时改坏了那才是真正的灾难。5. 常见问题排查与实战体会最后一个实际部署中大家问得最多的部分出了报错怎么处理、模拟和实盘的差距在哪、有哪些经验值得提前知道。我整理了一份高频问题对照表再聊聊几次实战下来的感受。5.1 高频错误对照表错误现象可能原因排查与解决办法连接失败提示权限不足QMT终端未开启Python外部访问权限检查券商QMT客户端设置确认MiniQMT独立连接密码已设置查询资产返回None资金账号类型错误或未订阅账户确认StockAccount传的是资金账号而非登录账号确认subscribe调用过订阅行情失败数据权限不足或行情前置未连接确认QMT客户端已登录且行情连接正常提前用xtdata下载历史数据验证接口下单返回-1或错误码股票代码后缀错误、非交易时段、资金不足检查代码是否带.SH/.SZ后缀检查时间判断逻辑打印订单错误信息重复下单信号无幂等处理或回调重复触发给每个信号加全局唯一序号队列消费前检查是否已处理成交价与信号价偏差大使用了市价单或限价单超价不足实盘尽量用限价单并用盘口最优买一卖一价作为基准价设置合理的超价档位出现报错时第一步永远是看完整错误信息而不是猜。xtquant的很多错误码和终端本身的日志是关联的QMT终端界面里也有一份运行日志两边对照着查定位效率高得多。5.2 模拟与实盘的关键差异仿真环境跑得再顺实盘还是会遇到新问题。最大的差异是成交行为仿真环境通常假设“价格到了就能成交”但实盘要考虑盘口深度、大单拆单、涨跌停流动性丧失等问题。比如你策略在涨停价挂买单仿真里能成交实盘可能排队排队最后收盘都没成交。其次是订单状态管理。实盘订单有“未报、待报、已报、部成、已成、已撤、废单”等多种状态每一个状态都对应一个回调。你必须在代码里完整处理这些回调把订单状态和本地持仓状态同步起来。很多人在仿真阶段不关注这些因为仿真撮合快订单状态更新几乎瞬间完成但实盘里部成单、撤单失败这些事频频发生。所以我的建议是先跑一个月的模拟盘且模拟盘期间要专门做“破坏性测试”。比如模拟中途手动断网、手动关闭QMT终端、在错误的时间手动下单看程序能不能自动恢复。如果这些场景都能扛住再考虑用最小资金上实盘。5.3 几点实战经验最后分享几个我自己跑实盘时攒下的经验这些在接口文档里基本看不到第一部署QMT实盘策略的机器不要用笔记本。不是说笔记本性能不够而是笔记本容易合盖休眠、移动断网、系统自动更新重启。我自己的做法是一台长期开机的迷你主机配一个UPS电源保证断电后还能稳定运行一段时间给策略留出平仓和记录的机会。第二别在盘中改代码。哪怕你发现了明显的bug也要等收盘后再改。盘中修改策略代码轻则引入新bug重则导致持仓状态错乱得不偿失。我吃过一次亏盘中想修一个日志级别的小问题结果把全局变量覆盖了直接导致一个持仓票没有按计划卖出。第三每次开盘前跑一次自检清单。我写了一个很简单的health_check()函数开盘前自动检查QMT是否连接、账户资金是否正常、今日交易日历是否确认、策略本地缓存数据是否有最新K线。这些检查全部通过后策略才真正进入盘中运行状态。别小看这一步它能把很多低级错误挡在开盘之前。实盘部署的本质是把一个“在数据上成立的策略”变成一个“在真实环境里能持续运行的系统”。你在部署上多花一天时间做稳定性建设后面就能少花十天时间在盘中救火。工程上的这些琐碎细节才是策略能不能长期跑下去的关键。