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是性价比最高的选择
- 查询优化:分区+索引+预聚合,三管齐下
- 写入优化:批量写入,按时间排序
嗯,这套方案我在生产环境跑了两年多,每天处理几十万条波动率数据,查询响应时间基本控制在100毫秒以内。你照着这个思路去设计,应该不会出大问题。
公众号:蓝海资料掘金营,微信deep3321