概述
写过几天 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 文件方案的四条硬性要求
不管存储介质是什么,方案都要满足这四条,否则一定会在某次崩溃后出事:
- 原子写入:先写临时文件、确认无误后再替换原文件。写到一半崩溃导致状态文件损坏,比”没有状态文件”更危险——策略可能读到半截 JSON 还当真。文件方案的标配做法是”写临时文件 →
os.replace原子替换”。 - 读取带默认值:文件不存在或损坏时,必须返回一个合法的默认状态,而不是抛异常让策略启动失败。策略永远要能拿到一个合法状态接着跑。
- 按策略实例隔离:每个策略、每个标的、每个账户一份独立状态文件,互不覆盖。命名维度建议包含”策略类型 + 标的 + 账户”。
- 带时间戳和版本号:每份状态记录”什么时候落的盘”和”状态结构版本号”。时间戳用于跨日失效判断,版本号用于未来字段升级时做迁移。
四、状态结构设计三原则
存储介质定下来之后,状态 dict 怎么设计决定了恢复逻辑好不好写。三条原则:
- 扁平优于嵌套:
{'cur_level': 3}比{'grid': {'level': 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 服务),任一存活即可恢复,适合对可用性要求高的策略;
- 落盘节流:高频策略可批量缓存 + 定时刷盘,但代价是崩溃会丢最近几笔——在换手率与崩溃风险之间权衡。
总结
状态储存做扎实,策略才能真正”无人值守”。回顾核心:
- 只存重算不出来的历史决策,能重算的 K 线/指标中间值、真实持仓都不存;
- 存储介质分层:起步一律 JSON 文件,状态量上来再 SQLite,不要起步就上 Redis;
- JSON 四条硬要求:原子写入、读取带默认值、按策略隔离、带时间戳和版本号;
- 结构三原则:扁平优于嵌套、每字段可对账、带当日日期;
- 读写时机:低频策略同步落盘,高频才考虑批量/定时,不要为性能引入丢数据风险;
- 对账是命门:本地状态是猜测,真实持仓是事实,事实永远赢;
- 确认式落盘:成交回调里才更新业务状态,下单时只记待确认;
- 多策略隔离:各自独立状态文件,不共享可变状态;
- 边界区分:状态、行情、交易日志各用各的存储,不要一锅炖。
把这套和 运行状态监控与心跳报警 一起部署,你的 QMT 实盘才真正算”机器可以死,策略的记忆不能死”。
说明:本文包含 AI 创作内容,请自行判断是否适用。本文为方向性方案,落地实现需结合自身策略与环境调整。