3. 回放引擎架构总览
行情回放引擎的本质,是把历史行情数据按时间顺序“重新播放”出来,并允许使用者以任意速度、任意节奏控制播放过程。它既不是简单的文件读取器,也不是普通的定时器封装,而是一套由数据源、事件队列、时钟调度、回放控制器和输出适配层共同协作的运行时系统。理解这五层的职责边界与数据流向,是后续实现变速、暂停、跳转、逐笔回放等功能的基础。
核心原理:五层协作与时间解耦
回放引擎最关键的设计思想是“时间解耦”:真实历史数据带有原始时间戳,而回放过程发生在当前真实时间中。引擎必须把历史时间轴映射到当前时间轴,同时保证事件顺序、节奏和可控性。五层结构各自承担明确职责:
- 数据源层:负责从文件、数据库或消息存档中读取原始行情,按时间戳排序,并做基础清洗与格式统一。
- 事件队列:作为回放引擎的“缓冲池”,按历史时间顺序存放待播放事件,支持批量预取与背压控制。
- 时钟调度:把历史时间差转换为真实等待时间,并根据速度倍率动态调整调度节奏。
- 回放控制器:对外暴露播放、暂停、变速、跳转、单步等控制接口,内部协调队列与时钟。
- 输出适配层:把回放事件转换为目标系统可消费的格式,如回调、WebSocket 推送、策略引擎输入等。
这五层之间是单向数据流为主、控制流为辅的关系。数据从数据源流入事件队列,时钟调度决定何时从队列取出事件,回放控制器决定整体状态,输出适配层负责最终投递。任何一层职责不清,都会导致变速时事件堆积、暂停后时间漂移或跳转后状态不一致。
方法流程:从数据加载到事件投递
一个完整的回放周期通常包含以下步骤。理解这些步骤有助于在实现时定位性能瓶颈与状态错误。
- 初始化数据源:打开文件或连接数据库,读取元信息,确定时间范围与事件总数。
- 预填充事件队列:按时间顺序批量拉取前 N 条事件,避免逐条读取造成 I/O 抖动。
- 启动时钟调度:记录回放起始的历史时间与当前真实时间,建立映射基准。
- 循环取事件:调度器根据下一条事件的历史时间戳计算应等待的毫秒数,等待后取出事件。
- 控制器状态检查:每次取出前检查是否暂停、是否变速、是否跳转,必要时调整基准。
- 输出适配投递:将事件转换为目标格式,调用回调或推送至下游,并记录投递日志。
- 队列补充与结束判断:队列低于水位时补充数据,数据源耗尽且队列为空时结束回放。
关键公式:等待时间 = (下一条事件历史时间 - 当前事件历史时间) / 速度倍率。当速度倍率为 0 时表示暂停,为负数时可用于倒放场景(需数据源支持反向读取)。
案例场景:变速回放中的队列与时钟配合
假设我们回放一段逐笔成交数据,原始时间间隔为 200ms,速度倍率设为 4 倍。时钟调度计算出的真实等待时间为 200/4 = 50ms。此时事件队列需要保证在 50ms 内至少有一条可用事件,否则调度器会空转。若数据源读取较慢,队列水位下降,控制器应自动降速或触发预取,而不是直接抛出异常。
# 回放配置示例
source:
type: file
path: ./data/ticks_20240102.csv
batch_size: 500
queue:
high_watermark: 2000
low_watermark: 300
clock:
speed: 4.0
max_wait_ms: 1000
controller:
auto_pause_on_empty: true
output:
type: callback
target: strategy_engine
当用户从 4 倍速切换到 1 倍速时,控制器需要重新计算基准时间,避免已等待的时间被重复计算。常见做法是:在变速瞬间记录“当前事件历史时间”和“当前真实时间”,后续等待时间基于新基准重新推导。这样即使频繁变速,也不会出现时间累积误差。
建议在时钟调度中维护一个“虚拟当前时间”,每次取出事件后将其推进到该事件的历史时间。这样跳转、暂停、变速都只需修改虚拟时间与真实时间的映射关系,逻辑更统一。
误区与复盘:常见架构陷阱
| 误区 | 表现 | 修正方向 |
|---|---|---|
| 数据源与调度耦合 | 调度器直接读文件,变速时 I/O 阻塞 | 引入事件队列作为缓冲,数据源只负责生产 |
| 用 sleep 直接模拟时间差 | 变速后等待时间未除以倍率,节奏错误 | 时钟调度统一计算等待时间,支持倍率参数 |
| 暂停时仍推进虚拟时间 | 恢复后事件瞬间爆发,节奏失真 | 暂停时冻结虚拟时间与真实时间的映射 |
| 输出适配层做业务判断 | 策略逻辑混入回放引擎,难以复用 | 适配层只做格式转换与投递,保持薄层 |
| 队列无水位控制 | 内存暴涨或频繁空转 | 设置高低水位,动态预取与背压 |