Files
fidc-backtest-engine/docs/open-source-trading-engine-review-20260906.md
T
2026-09-06 05:39:14 +08:00

7.4 KiB
Raw Blame History

FIDC 开源回测与交易引擎设计审查

审查范围

本次审查直接读取以下只读参考源码。源码位于 /Volumes/T7-Data/WorkSpace/reference-trading;原计划使用的 /Volumes/SystemSSD 在审查时未挂载,因此没有向本机系统盘写入参考仓库。

项目 审查提交 重点
NautilusTrader ac22d5cf4a7e Rust 事件内核、统一回测/实盘组件、时间事件堆
QuantConnect LEAN 23b735d99a35 订阅同步、TimeSlice、Universe 生命周期、惰性集合
Microsoft Qlib 79633dd9506e 表达式缓存、日历切片、内存/磁盘分层
vectorbt 34b6d5935e3e NumPy/Numba 密集数组仿真、紧凑状态数组
Zipline Reloaded 943010b9da84 Pipeline DAG、窗口预取、分段执行、复权读取
Backtrader b853d7c90b67 preload/runonce 与逐 bar/live 模式分离

可借鉴设计

1. 时间轴和执行状态必须统一,但热路径不必经过通用消息总线

NautilusTrader 的 BacktestEngine 复用数据、执行、风险和缓存组件,并用带稳定 序号的最小时间事件堆推进多个时钟。LEAN 用 SubscriptionSynchronizer 将不同订阅 合并到同一个 frontier,再生成唯一 TimeSlice。两者共同证明:回测和实盘应共享 订单、风控和时间语义,而不是共享一段策略特例代码。

FIDC 已经以 decision_dateexecution_date、调度时间和撮合时间构成统一执行合同, 并由同一交易核心服务于回测、模拟盘和实盘。日线全市场选股不应改为逐字段消息 广播;这会给每个股票状态增加分配和动态分派。通用事件总线只保留在订单、成交、 配置审计及外部集成边界。

2. 批量研究计算和事件撮合必须使用不同执行形态

vectorbt 把纯数值组合压入连续数组和编译循环;Backtrader 在历史批量模式使用 preload + runonce,进入 live/replay 或受限内存模式后关闭该路径。Zipline 的 Pipeline 则先生成 DAG 执行计划,按依赖顺序计算,并允许按日期 chunk 控制内存。

FIDC 应继续保持:

  • 日线基础特征、rolling、横截面 rank 和因子挖掘使用 Arrow/NumPy/DuckDB/紧凑数组;
  • 订单、成交、T+1、涨跌停、停牌、公司行动和现金流使用确定性事件撮合;
  • 不把向量化收益外推到存在订单状态和路径依赖的撮合过程;
  • 不让逐 bar 实盘语义退化成预先知道整个未来数组的批量回测语义。

3. 不可变数据按内容身份共享,策略结果和可变账户状态严格隔离

Qlib 的缓存层、Zipline 的预取窗口和 Backtrader 的优化数据预载都说明:相同历史 数据不应由每个策略重复解码。FIDC 当前 Source Lake 的 Parquet/Arrow、冻结 query scope、内容寻址 bundle、进程内 DataSet 和共享 result-store block 已符合这个方向。

共享键必须包含完整数据代际、字段投影、PIT 截止时间、复权口径和窗口。禁止共享:

  • 选股结果、订单、仓位、账户、风控决策和策略局部变量;
  • 缺少 manifest/SHA/PIT 身份的 DataFrame 或 dict
  • 盘中 provisional 数据与正式收盘数据混用的缓存项。

4. 字段和因子要在计划阶段冻结,运行时只物化真正需要的数据

LEAN 的 TimeSliceFactory 复用空集合,并只在收到对应数据时创建集合;Zipline 的 Pipeline 使用执行计划和 refcount 释放中间值。FIDC 已有字段投影、 DailySnapshotView、rolling requirement、Factor DAG 和 numeric bytecode VM。 后续优化必须扩展这些类型化计划,而不是恢复宽 Python 行或每次构建完整 map。

当前性能事实

2026-09-06 在 177 使用同一冻结五年策略得到:

  • restart/cold17.985s;数据准备 14.659s;引擎 2.627s
  • process-hot:总耗时中位 3.245s;数据准备 0.006s;引擎 2.629s
  • 26,088 笔成交、收益 0.9219861819172002、canonical SHA b42fea66237d06eadb24f6b8c9e2760e7319fe3699f315b99e01f433ef2aa234 在所有重复运行中一致;
  • 当前剖面热点为 numeric VM 12.39%、临时股票状态构建 10.17%、复权均线 6.82%
  • Source Lake/进程共享缓存已经把同 bundle 数据准备从 14.659s 降到 0.006s 所以再把 DataFrame 改成 dict 或扩大 DuckDB 连接数不是当前热路径优化。

两项受开源类型化执行启发的候选已真实验证并删除:

  1. 编译期枚举化全部 VM 标识符仅减少约 1.44% instructions,五年引擎中位只改善 0.19%,却净增约 562 行;
  2. VM 槽位代际复用仅减少约 0.48% instructions,五年引擎中位回退约 0.27%

完整证据见 docs/evidence/numeric-vm-binding-generation-rejection-20260906.json

FIDC 后续优化顺序

P0:冷数据路径一次构建、分段发布、跨策略共享

以冻结 bundle 的完整身份构建 canonical base panel,并按年份或有界日期段发布 只读 mmap/Arrow 段;父任务只扫描一次 Source Lake,worker 只映射所需段。每日增量 只生成变化尾段,历史段保持同 SHA。必须以 process-cold 14.659s 为基线,证明 冷启动下降且 RSS、PIT、复权和结果 SHA 不变。

P1numeric VM 使用类型化 helper opcode 或经证据支持的 super-instruction

当前字符串标识符绑定不是主要成本。下一候选应在编译期把常见 rolling helper、 比较和短路组合编译为类型化 opcode,减少解释器指令数,同时保留 helper 懒求值和 错误信息。必须对主策略、第二种持仓数策略、通用自定义排序策略和分钟策略分别 A/B。

P1:结果事件按消费者需求分层

撮合事实保持完整不可变;页面摘要、曲线、持仓和交易视图从 typed result store 按需投影。禁止每次回测向 PostgreSQL 写入大矩阵,也禁止为了列表或概览解码全部 事件。优化目标是 resultSeconds + finalizationSeconds,不能删减审计事实换速度。

P2:分钟线有界流式窗口

参考 Zipline 的窗口预取和 Nautilus 的有序事件迭代器,按时间段加载分钟 RecordBatch,保持持仓、订单和指标 ring buffer 有界;不能一次展开全市场全区间 分钟 Python 对象,也不能在 chunk 边界丢失 corporate action、T+1 或订单队列状态。

明确不采用

  • 不为单次策略把日循环拆成多线程;路径依赖会增加同步开销并破坏确定性。
  • 不通过增加 worker、DuckDB 槽位或扩大 HTTP 窗口掩盖单任务热点。
  • 不把全量历史数据复制成每个 worker 独占的 dict/DataFrame 缓存。
  • 不直接引入另一个框架的事件总线、账户或撮合实现;只借鉴机制并用中国市场合同验收。
  • 不缓存策略结果,也不以 request hash 返回旧结果代替真实回测。

验收矩阵

任何性能候选至少覆盖:

合同 必须保持
五年主策略 收益、成交数、canonical/result-store SHA、终态审计
五年第二策略 不同持仓数下的同一组证据
通用 rank 策略 非 market-cap 特例排序仍正确
分钟策略 调度点、分钟成交价、滑点、成交量限制和 SHA
冷/热运行 data/engine/result/finalization 分段、RSS、instructions/cycles

任一合同漂移、只有微基准改善、或真实 wall/RSS 变差时,候选必须删除并保留拒绝证据。