讯投QMT使用小技巧: 策略状态怎么存——存储介质、读写时机与结构设计

概述

写过几天 QMT 实盘的人都会撞到同一个问题:策略的”记忆”该放哪。

ContextInfo.user_data 只是内存字典,QMT 退出、崩溃、断电、每天重置 之后全部归零;可有些状态是”重算不出来的历史决策”——网格档位、当日已交易次数、未成交委托、当日参数快照——丢了就会重复建仓、超频下单、或者按新行情把阈值跑偏。这些必须存到进程之外。

本文不贴实现代码,只把”状态储存”这件事的工程方向讲清楚:什么该存、存到哪种介质、什么时候读什么时候写、结构怎么设计、多策略怎么隔离。概念理顺了,用 JSON 还是 SQLite、自己写还是引开源库,都只是细节。

一、什么该存:按”能否重算”划线

第一条原则:能重算的不存,存的是重算不出来的历史决策。存太多反而增加状态污染和排查成本。

状态类型 例子 是否存 原因
档位/档数 网格的 cur_level 存 重算会从零档重买,重复建仓
当日已交易次数 防超频 存 丢了当日可能反复下单
目标持仓 today_target_position 存 丢了会重新计算并乱调仓
未成交委托标记 “已挂单待成交” 存 丢了会重复挂单
参数快照 当日中枢价、阈值 存 丢了会按新行情重算,偏离原计划
K 线缓存、指标中间值 计算用 不存 重算即可
实时行情快照 最新价 不存 拉一次就有
持仓表 真实持仓 不存 随时 get_trade_detail_data 查

最后一条特别重要:真实持仓不要存,要查的时候实时查。存了反而会和实盘脱节——存的是”猜测”,实盘账户是”事实”,事实永远赢。这条原则会贯穿后面的对账设计。

二、存储介质选型:按状态量和并发需求分层

存储介质没有银弹,按你的状态量和并发需求分层选择:

介质 优点 缺点 适用场景
JSON 文件 可读、可手工排查、零部署、Python 标准库即可 并发写入弱、大量字段时性能差 个人量化的首选,状态字段几十个以内
SQLite 单文件、SQL 查询、事务支持、并发好 比文件多一点运维、需懂表设计 状态字段多、或要做历史回放
CSV 简单、可读 不支持原子写、不适合频繁更新 只追加不修改的日志类状态
外部 KV 服务(Redis 等) 高性能、跨进程 部署运维成本高 个人过度设计,不推荐起步

建议:起步一律用 JSON 文件,状态量上来再换 SQLite。不要一上来就上 Redis/外部服务——这是新手最常见的过度设计,个人量化的状态量根本到不了那个量级。

三、JSON 文件方案的四条硬性要求

不管存储介质是什么,方案都要满足这四条,否则一定会在某次崩溃后出事:

  1. 原子写入:先写临时文件、确认无误后再替换原文件。写到一半崩溃导致状态文件损坏,比”没有状态文件”更危险——策略可能读到半截 JSON 还当真。文件方案的标配做法是”写临时文件 → os.replace 原子替换”。
  2. 读取带默认值:文件不存在或损坏时,必须返回一个合法的默认状态,而不是抛异常让策略启动失败。策略永远要能拿到一个合法状态接着跑。
  3. 按策略实例隔离:每个策略、每个标的、每个账户一份独立状态文件,互不覆盖。命名维度建议包含”策略类型 + 标的 + 账户”。
  4. 带时间戳和版本号:每份状态记录”什么时候落的盘”和”状态结构版本号”。时间戳用于跨日失效判断,版本号用于未来字段升级时做迁移。

四、状态结构设计三原则

存储介质定下来之后,状态 dict 怎么设计决定了恢复逻辑好不好写。三条原则:

  1. 扁平优于嵌套:{'cur_level': 3} 比 {'grid': {'level': 3}} 更不容易写错,对账时也更直接;
  2. 每个字段都可对账:存的值要能和实盘真实持仓/委托对得上,对不上就以真实为准;
  3. 带当日日期:跨日恢复时要能判断”这个状态是今天的还是昨天的”,过期就丢弃。

一份典型状态应包含这几类字段:策略名、所属交易日(跨日失效用)、业务状态(档位/已交易次数)、未成交委托列表、当日参数快照(中枢价、阈值等)。业务状态是”重算不出来的”,参数快照是”重算会跑偏的”,两类都要存。

五、读写时机:三种策略的权衡

读写时机和存储介质一样重要,选错了会要么频繁落盘拖慢策略、要么崩溃丢数据。三种主流策略:

策略 写入时机 优点 风险 适用
同步落盘 状态每次变化立即写 崩溃零丢失 高频策略会被磁盘 IO 拖慢 低频策略(日/周调仓)
批量缓存 攒一批再写 性能好 崩溃丢最近一批 中频策略(小时/分钟级)
定时刷盘 固定周期写一次 可控、简单 周期内的变化会丢 大多数个人策略的甜区

建议:个人量化策略(月/周/日调仓)直接用同步落盘,状态变化本就不频繁,IO 成本可忽略;只有分钟级以上的策略才需要考虑批量或定时。不要为了性能优化而引入”丢数据”的风险——个人策略的瓶颈从来不在落盘 IO。

六、对账:存储只是”猜测”,实盘才是”事实”

这是状态储存最容易被忽视、也最关键的一步。本地状态可能和实盘不一致——比如落盘了”档位 3”但下单其实被拒了,或者崩溃发生在”下单成功但还没回调”之间。

永远不要无脑信任本地状态,要用真实持仓反向校验:本地状态里凡是和持仓相关的字段,都拿 get_trade_detail_data 返回的真实持仓再算一遍,两边不一致就以真实为准、回写本地。比如网格的档位 = 真实持仓 ÷ 每格份数,轮动策略的目标持仓 = 真实持仓表。

一句话:本地状态是”猜测”,真实持仓是”事实”,事实永远赢。这一步把状态储存从”自欺”变成”可信”,能挡住 90% 的”重启后乱下单”事故。

七、确认式落盘:成交回调里才写,下单时不写

新手最常见的错误是”下单即更新状态”——单子还没成交就把档位改了,结果撤单或拒单后状态错位。正确做法是成交回调里才落盘,下单时只记”待确认”:

  • 下单时:把委托 ID 记进”待确认列表”,但不动业务状态(档位、已交易次数);
  • 成交回调里:识别这笔成交属于哪类动作(用 passorder 传进去的 userOrderId 标记),更新业务状态、落盘,并从”待确认列表”移除;
  • 撤单/拒单回调里:只从”待确认列表”移除,业务状态不动。

这样未成交的委托永远不会污染状态。回调用法详见 委托/成交回调的正确用法。

八、多策略状态隔离

一台 QMT 跑多个策略时,每个策略只读写自己的状态文件,互不串门。命名维度建议包含:策略类型 + 标的 + 账户,比如 grid_510300、rotate_普通账户、cond_600519。

隔离之后还有一个常被忽略的点:多策略之间不能共享可变状态。如果 A 策略改了某份状态、B 策略又读它,崩溃恢复时根本无法判断以谁为准。共享的应该是只读的公共数据(如交易日历),可变业务状态必须各自独立。

九、与行情数据库、交易日志的边界

状态储存容易和另外两类存储混淆,划清边界能避免过度设计:

  • 状态储存:存”策略当前在哪个状态”,量小、频繁更新、字段按业务定——本文主题;
  • 行情数据库:存历史 K 线/Tick,量大、按日追加、字段固定——见历史行情数据下载与本地缓存;
  • 交易日志:存”每笔成交的记录”,只追加不修改、用于归因分析——和状态储存是两回事,不要用状态文件去当交易日志。

不要用一个存储方案去扛三类需求。状态用 JSON 文件、行情用 Parquet/DuckDB、交易日志用 SQLite 追加表,各司其职。这是 数据地基 那篇”按用途分存储”原则在 QMT 实战层的兑现。

十、增强方向

  • 从文件升级到数据库:状态字段多、或需要历史回放时,把存储从 JSON 文件换 SQLite,接口不变、存储升级;
  • 状态版本回滚:保留最近 N 次落盘的副本,出问题时能回滚到上一版,方便排查”状态是怎么变脏的”;
  • 双写热备:关键状态同时写本地 + 一个外部服务(如自己的小 HTTP 服务),任一存活即可恢复,适合对可用性要求高的策略;
  • 落盘节流:高频策略可批量缓存 + 定时刷盘,但代价是崩溃会丢最近几笔——在换手率与崩溃风险之间权衡。

总结

状态储存做扎实,策略才能真正”无人值守”。回顾核心:

  1. 只存重算不出来的历史决策,能重算的 K 线/指标中间值、真实持仓都不存;
  2. 存储介质分层:起步一律 JSON 文件,状态量上来再 SQLite,不要起步就上 Redis;
  3. JSON 四条硬要求:原子写入、读取带默认值、按策略隔离、带时间戳和版本号;
  4. 结构三原则:扁平优于嵌套、每字段可对账、带当日日期;
  5. 读写时机:低频策略同步落盘,高频才考虑批量/定时,不要为性能引入丢数据风险;
  6. 对账是命门:本地状态是猜测,真实持仓是事实,事实永远赢;
  7. 确认式落盘:成交回调里才更新业务状态,下单时只记待确认;
  8. 多策略隔离:各自独立状态文件,不共享可变状态;
  9. 边界区分:状态、行情、交易日志各用各的存储,不要一锅炖。

把这套和 运行状态监控与心跳报警 一起部署,你的 QMT 实盘才真正算”机器可以死,策略的记忆不能死”。


说明:本文包含 AI 创作内容,请自行判断是否适用。本文为方向性方案,落地实现需结合自身策略与环境调整。


赞赏作者

赞赏码

本页面赞赏完全自愿,属于读者无偿鼓励,不提供任何商品、服务、答疑、内容解锁等对价。您可随意选择是否赞赏以及赞赏金额。

Share