第15章:系统架构设计:数据存储与回放机制

做波动率曲面异常检测,最怕什么?

怕数据丢了,怕回测结果复现不了,怕生产环境出了bug却找不到根因。

我早年吃过这个亏。有一次策略在实盘跑得好好的,突然某天下午开始疯狂报错。我查了半天日志,发现是某个期权链的波动率曲面出现了「尖刺」——但等我回头想复盘时,发现原始tick数据已经被覆盖了。嗯,从那以后,我再也不敢轻视数据存储与回放机制的设计。

15.1 数据存储的核心诉求

波动率曲面数据,说白了就是不同行权价、不同到期日的隐含波动率构成的二维矩阵。但实际落地时,它比想象中复杂得多。

我个人习惯把数据存储的需求拆成三层:

  • 原始层:交易所来的tick级期权行情,包括买卖价、成交量、未平仓量
  • 计算层:经过插值、平滑后的波动率曲面快照
  • 异常层:检测出的异常点、异常区间、以及对应的风险标签

你想想看,如果这三层数据混在一起存,查询效率会非常低。而且一旦需要回放某个历史时刻的曲面状态,你得能精确还原当时的计算上下文。

核心原则:存储设计必须支持「时间旅行」——任意时刻的曲面状态都能完整重建。

15.2 存储方案选型

我在项目中遇到过几种方案,这里直接给结论:

存储类型 适用场景 我的建议
关系型数据库(PostgreSQL) 元数据、配置、异常标签 必选,用于管理资产信息和检测规则
时序数据库(InfluxDB / ClickHouse) 高频曲面快照、tick数据 强烈推荐,写入和查询性能远超传统方案
列式存储(Parquet + S3) 历史回放、批量分析 适合做离线回测和模型训练
内存数据库(Redis) 实时检测、热数据缓存 用于生产环境的低延迟异常检测

为什么我推荐ClickHouse?因为它对时间序列数据的压缩比极高,而且支持SQL语法。我曾经用PostgreSQL存一天的期权tick数据,磁盘占用2.3GB;换成ClickHouse后,同样的数据只用了280MB。你想想看,这差距有多大。

15.3 数据回放机制设计

回放机制是整个系统的「时光机」。没有它,你根本没法验证异常检测算法在历史数据上的表现。

我设计的回放机制包含三个核心组件:

  1. 快照管理器:每隔固定时间(比如1分钟)保存一次完整的曲面状态
  2. 增量日志:记录两次快照之间的所有变化事件
  3. 回放引擎:根据时间戳,从最近的快照开始,逐条回放增量事件
小技巧:快照间隔不要设得太短。我一般设5分钟一次快照,配合增量日志,既能保证回放精度,又不会占用太多存储空间。

回放引擎的核心逻辑其实不复杂,但有个坑要注意——事件顺序。同一个时间戳内,可能有多个事件同时发生(比如多个期权合约同时更新)。这时候必须按合约ID排序,否则回放出来的曲面会乱掉。

class ReplayEngine:
    def __init__(self, snapshot_store, event_store):
        self.snapshot_store = snapshot_store
        self.event_store = event_store
    
    def replay(self, target_time):
        # 找到最近的快照
        snapshot = self.snapshot_store.get_nearest(target_time)
        current_state = snapshot.data
        
        # 获取快照之后的所有事件
        events = self.event_store.get_events(
            start=snapshot.timestamp,
            end=target_time
        )
        
        # 按时间戳和合约ID排序
        events.sort(key=lambda e: (e.timestamp, e.contract_id))
        
        # 逐条回放
        for event in events:
            current_state.apply(event)
        
        return current_state

15.4 异常数据的特殊处理

检测出来的异常点,不能简单丢掉。我见过太多团队把异常数据直接删掉,结果回测时发现策略表现「异常好」——因为所有坏样本都被清除了。

正确的做法是:

  • 异常数据单独存储,打上标签(比如「数据错误」「市场冲击」「流动性不足」)
  • 回放时提供「净化模式」和「原始模式」两种选项
  • 异常标签本身也要作为特征,输入到后续的风险模型中
警告:千万不要在存储层直接修改原始数据。我曾经见过有人把异常点的波动率值改成NaN,结果回放时整个曲面插值算法崩溃。正确的做法是保留原始值,另开一列标记异常状态。

15.5 系统架构总览

说了这么多,咱们用一张图把整个架构串起来。

波动率曲面异常检测系统 - 数据存储与回放架构 数据源层 交易所Tick数据 | 期权链快照 | 市场深度数据 计算与检测层 曲面构建 | 异常检测算法 | 风险评分 存储层 原始数据存储 (ClickHouse + Parquet) 异常标签存储 (PostgreSQL) 快照与增量日志 (Redis + 文件系统) 回放与验证层 回放引擎 | 历史场景复现 | 策略回测验证

15.6 生产环境的一些细节

最后分享几个我在生产环境中踩过的坑:

  • 写入吞吐量:期权tick数据量很大,尤其是美股市场。我建议用批量写入,每100ms攒一批再写,别一条一条insert
  • 数据压缩:波动率曲面数据有很强的时空相关性,用delta-of-delta压缩算法效果很好,压缩比能达到20:1
  • 冷热分离:最近7天的数据放SSD,7天以上的放HDD或对象存储。查询时透明路由,用户无感知
  • 校验和机制:每个数据块写入时计算CRC32,回放时校验。我遇到过磁盘静默错误导致曲面数据损坏的情况,有了校验和就能及时发现
我的习惯:每天凌晨跑一次全量数据校验,对比原始数据和存储数据的哈希值。虽然有点费时间,但能保证数据一致性。做量化交易,数据就是命根子。

好了,关于数据存储与回放机制,核心要点就这些。记住一句话:存储设计决定了你能回溯多远的历史,回放机制决定了你能多精确地还原历史。这两点做好了,异常检测和风险规避才有坚实的基础。

公众号:蓝海资料掘金营,微信deep3321