跳转至

"生产就绪"的模拟盘,成交表是空的

本文的"对账/落库检查"可一键复现:experiments/reconcile_paper_vs_backtest.py → evidence/reconcile_result.json。 实验环境:WSL2 / Python 3.12 / duckdb 2.0.0(只读取库,不做写入)。

一、现象

我的模拟盘(paper trading)系统在结构上是"完整"的:定时调度、盘前分析、开盘执行、盘中风控、收盘结算、每日报告、看板面板,一样不少。文档里写着"Production Ready"。

当我对着它做一次例行对账时,看到的是:

  • 成交表 trades:0 行
  • 持仓表 positions:0 行
  • 每日绩效表 daily_performance:0 行
  • 行情缓存表 market_data_cache:0 行

四张表,全部为空。这个系统从部署到现在,一笔交易都没有发生过。

而同期项目文档里躺着一条不起眼的记录:数据源因代理配置问题连不上,行情拉取失败,分析无法完成。两件事拼起来,才是完整的故事:没有行情 → 没有信号 → 没有成交,但调度器每天准点运行、日志按时滚动、面板正常显示——系统"健康"地空转。

二、我一开始的错误判断

发现路径就是复现材料里那个对账脚本:例行对账,预期看到成交序列,返回的是 0 行。第一反应是连错了库——换只读方式把四张表逐个数了一遍,全是 0,才接受这不是查询姿势的问题。真正让人安静下来的是翻文档对时间线:v0.6.0 完成报告(commit 0cc3370)里写着 Production Ready,数据获取策略文档里躺着网络与代理排障一节——两份文档早就把“为什么是空的”写好了,只是从来没有人把它们放到一起读。零成交甚至不是一个 failure 事件:它安静到连一条报错日志都没产生过。

复盘时我总结出两个典型的误判:

  1. "系统跑起来了 = 系统在工作"。调度器在跑、日志在写、接口在响应,每个组件单看都是"活的"。但"没有报错"和"产生了正确结果"是两件事——对一个交易系统来说,零成交本身就是最大的异常,只是没有任何监控把它当异常报出来。
  2. "回测结论可以平移到模拟盘"。我默认两边对"一笔交易"的理解是一致的。实际上,这两个引擎对成交的定义根本对不上(下文逐项列)。

三、排查与关键证据

证据 1:四张业务表全部为空(只读查询,见复现材料)。调度、日志、面板的"活跃"和业务数据的"为零"形成了刺眼对比。

证据 2:两个引擎的"成交语义契约"逐项对比(来自代码走读,每一项都可在源码中定位):

维度 回测引擎 模拟盘
成交价基准 信号所在 bar 的收盘价 日线数据的最近一根收盘价,在开盘时刻当作"现价"
佣金/印花税/最低佣金 有费率模型(但见上一篇:实现崩溃,从未执行) 完全没有费用计算
涨跌停约束 有标志位(处理方式是作废信号) 没有任何检查
T+1 无显式实现(日线粒度碰巧成立) 没有任何检查:当天买入的仓位,当天盘中就可能被止损卖出
滑点/部分成交 无 无

同一个"卖出"动作,在回测里是一回事,在模拟盘里是另一回事。两边的盈亏根本不可比。

证据 3:费率配置的来历。回测侧费率(佣金、印花税、最低佣金)来自全局配置文件,模拟盘侧则压根不读它——配置存在,但只对一半系统生效。

四、根因

  1. 缺少一份"成交语义契约"。价格基准、费用模型、约束规则、时钟粒度——这些应该在两个引擎之间共享一个定义(理想的形态是一层公共撮合组件)。缺了它,两个引擎各自演化,语义漂移被代码固化下来。
  2. fallback 链路把"跑通"和"有数据"混为一谈。数据获取侧是 QUANTAXIS → AkShare 的多级回退,任何一级失败都静默跳到下一级,最后连"现价"都拿不到时就跳过这只股票。没有任何一层会喊"我其实一直没拿到过数据"。
  3. 监控的盲区。我监控了进程、日志、接口——全是"系统活性"指标;没有监控"业务产出"指标(成交数、数据新鲜度)。零成交运行了那么久,没有任何告警。

五、约束与取舍

  • 放弃了实时/盘中行情,用日线数据冒充盘中现价。代价:止损、止盈的触发价与真实可成交价脱节;在开盘时刻执行的"现价"其实是上一根日线——这个暧昧还取决于数据源在盘中返回的是昨收还是含当日未完成 bar,行为随数据源版本漂移。
  • 放弃了抽取公共撮合层。代价:两套成交逻辑各自生长,本文列出的每一行差异都是利息。
  • 放弃了滑点建模。代价:模拟盘系统性偏乐观,而且和回测的乐观方向一致地叠加。
  • 补救路径(写给大家):先立契约表,再改代码;监控加一条"连续 N 个交易日零成交即告警"——它比任何进程监控都更能救命。

六、可复用检查清单

  1. 给回测和模拟盘写一张"成交语义契约表"(价格基准 / 费用 / 约束规则 / 时钟粒度),各自对照打钩,不一致处显式声明;
  2. 模拟盘每次取价必须能回答"这是哪根 bar 的哪个价",答不出来就是 bug;
  3. 监控业务产出指标(成交数、数据新鲜度),"连续零成交"必须报警——活性指标救不了空转的系统;
  4. 模拟盘的落库 schema 必须包含费率与拦截原因字段,否则事后无法归因;
  5. 任何策略结论跨引擎引用之前,先用同一组输入验证两边产出同一笔成交。

复现材料

  • 脚本:experiments/reconcile_paper_vs_backtest.py
  • 原始结果:evidence/reconcile_result.json、各表落盘 evidence/paperdb_*.csv
  • 说明:本文发稿时对账实验的"费用拖累量化"部分因成交表为空暂无数字——这本身就是现象的一部分;待模拟盘产生真实成交后,同一脚本可直接产出对比表