25、波动率曲面数据库设计:数据模型设计、时序数据库选择、数据查询优化

做波动率曲面分析,最头疼的事是什么?

我个人觉得,不是模型算不准,而是数据存不下、查不动。

你想想看,一个完整的波动率曲面,每天要记录几十个期限、几十个行权价,再加上实时更新的隐含波动率数值。一天下来就是几千条数据,一年几百万条。要是做回测,五年十年的数据堆上去,普通的关系型数据库直接就跪了。

这一章,我就把我在生产环境中踩过的坑、总结出来的经验,一次性讲清楚。

25.1 数据模型设计:别把曲面存成二维表

很多新手会这么设计表结构:

CREATE TABLE vol_surface (
    id BIGINT AUTO_INCREMENT,
    trade_date DATE,
    option_code VARCHAR(20),
    strike_price DECIMAL(10,2),
    maturity VARCHAR(10),
    implied_vol DECIMAL(8,4),
    PRIMARY KEY (id)
);

看着挺规整对吧?但实际跑起来,你会发现查询慢得像蜗牛。

为什么?因为波动率曲面本质上是三维结构——时间、行权价、波动率。你把它拍平成二维表,每次查询都要做大量的行列转换。

核心思路:用宽表存储曲面快照,用窄表存储历史序列。

我建议这样设计:

表1:曲面快照表(宽表)

CREATE TABLE vol_surface_snapshot (
    snapshot_time TIMESTAMP,
    underlying VARCHAR(10),
    expiry_group VARCHAR(20),
    -- 行权价作为列名,例如:
    k_090 DECIMAL(8,4),  -- 90%行权价对应的波动率
    k_095 DECIMAL(8,4),
    k_100 DECIMAL(8,4),
    k_105 DECIMAL(8,4),
    k_110 DECIMAL(8,4),
    -- 不同期限同理
    PRIMARY KEY (snapshot_time, underlying, expiry_group)
);

表2:历史波动率序列表(窄表)

CREATE TABLE vol_history (
    ts TIMESTAMP,
    underlying VARCHAR(10),
    strike DECIMAL(10,2),
    maturity DATE,
    implied_vol DECIMAL(8,4),
    delta DECIMAL(5,4),
    PRIMARY KEY (ts, underlying, strike, maturity)
);

宽表适合做「某个时刻的曲面可视化」,窄表适合做「某个行权价的历史走势分析」。两者互补,缺一不可。

避坑指南:我曾经把宽表设计成动态列,每次新增行权价就ALTER TABLE加一列。结果生产环境跑了一个月,表结构变得乱七八糟。后来改成固定列+预留列的方式,才稳定下来。

25.2 时序数据库选择:别用MySQL硬扛

说实话,MySQL不是不能存时序数据,但你要做好心理准备——查询慢、存储膨胀、维护成本高。

我对比过几个主流方案:

数据库 写入性能 查询性能 压缩比 学习成本
InfluxDB 极高 10:1
TimescaleDB 5:1
ClickHouse 极高 极高 8:1
MySQL 1:1

我个人最推荐的是TimescaleDB。为什么?

  • 它基于PostgreSQL,SQL语法完全兼容,迁移成本低
  • 自动分区(Hypertable),按时间分块,查询只扫必要的数据
  • 压缩比不错,而且支持实时聚合

如果你追求极致性能,可以上ClickHouse。但说实话,对于波动率曲面这种数据量(每天几万到几十万条),TimescaleDB完全够用。

注意:InfluxDB的Flux查询语言学习曲线陡峭,而且版本迭代快,我有个项目用了InfluxDB 1.x,后来升级到2.x,查询语法全变了,重构代码花了两周。如果你团队不大,建议选SQL兼容的数据库。

25.3 数据查询优化:让查询飞起来

数据存好了,怎么查得快?

我总结了三个核心技巧:

技巧1:时间分区 + 索引

在TimescaleDB中,创建Hypertable后,一定要在查询字段上加索引:

-- 创建超表
SELECT create_hypertable('vol_history', 'ts');

-- 添加索引
CREATE INDEX idx_vol_underlying ON vol_history (underlying, ts DESC);
CREATE INDEX idx_vol_strike ON vol_history (underlying, strike, ts DESC);

这样查询「某只股票最近30天的波动率」时,数据库只会扫描对应的分区,而不是全表。

技巧2:预聚合

实时计算曲面统计量很慢。我习惯用物化视图提前算好:

CREATE MATERIALIZED VIEW vol_surface_daily AS
SELECT 
    date_trunc('day', ts) AS day,
    underlying,
    strike,
    maturity,
    AVG(implied_vol) AS avg_vol,
    MAX(implied_vol) AS max_vol,
    MIN(implied_vol) AS min_vol
FROM vol_history
GROUP BY 1, 2, 3, 4;

前端展示日线图时,直接查这个物化视图,速度能快几十倍。

技巧3:批量写入,别一条一条插

很多新手会写循环插入:

for row in data:
    cursor.execute("INSERT INTO vol_history VALUES (...)")

千万别这么干!改成批量:

# 用executemany或者COPY命令
cursor.executemany("""
    INSERT INTO vol_history (ts, underlying, strike, maturity, implied_vol)
    VALUES (%s, %s, %s, %s, %s)
""", batch_data)

我实测过,批量写入比逐条插入快100倍以上。

一个小技巧:写入时把数据按时间排序后再插入,能减少磁盘碎片,查询性能也会提升。这个细节很多人不知道。

25.4 整体架构图

下面这张图,是我在实际项目中用的架构:

数据源 交易所行情 / 场外报价 数据清洗 去重 / 插值 / 异常检测 TimescaleDB 时序数据存储 查询优化层 物化视图 / 索引 / 分区 预聚合 / 缓存 应用层 曲面可视化 / 策略回测 风险分析 / 定价引擎 Redis 缓存 热点数据加速 监控告警 数据延迟 / 异常检测 波动率曲面数据库架构图 数据流方向:左 → 右,上 → 下

这个架构的核心思想是:分层解耦。数据源只管生产,清洗层只管处理,存储层只管存,查询层只管快。每一层各司其职,出了问题也好排查。

总结一下:

  • 数据模型:宽表存快照,窄表存历史,别混在一起
  • 数据库选型:TimescaleDB是性价比最高的选择
  • 查询优化:分区+索引+预聚合,三管齐下
  • 写入优化:批量写入,按时间排序

嗯,这套方案我在生产环境跑了两年多,每天处理几十万条波动率数据,查询响应时间基本控制在100毫秒以内。你照着这个思路去设计,应该不会出大问题。


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