docs: map open-source engine designs to FIDC

This commit is contained in:
boris
2026-09-06 05:39:14 +08:00
parent 52c7831bf6
commit cda249e9b4
@@ -0,0 +1,133 @@
# 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_date``execution_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/cold`17.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 变差时,候选必须删除并保留拒绝证据。