第三章 数据模块设计:数据源接入、数据清洗、数据存储、数据对齐
做量化交易,有一句话我特别认同:「垃圾进,垃圾出」。你策略再牛,模型再花哨,底层数据是脏的、乱的、对不齐的,那结果就是一场自嗨。
这一章,咱们就聊聊数据模块怎么设计。说白了,就是解决四个问题:数据从哪来、怎么洗干净、存到哪去、怎么对齐。我做了这么多年回测系统,踩过的坑有一半都在数据层。今天我把这些经验掰开了讲给你听。
核心观点:数据模块是整个回测系统的地基。地基不稳,楼盖得越高,塌得越快。
3.1 数据源接入:别只盯着一个渠道
数据源接入,听起来简单,不就是调个API或者读个CSV吗?嗯,确实不复杂,但坑很多。
我个人习惯把数据源分成三类:
- 实时API:比如交易所的WebSocket接口,适合做高频或准实时策略
- 历史数据库:比如从Wind、Tushare、AKShare拉下来的日线、分钟线
- 本地文件:CSV、Excel、Parquet,适合做离线研究和回测
我在项目中遇到过一个问题:同一个股票,从两个不同API拿到的数据,开盘价差了0.01元。你想想看,0.01元对单笔交易影响不大,但回测跑1000笔,误差就累积起来了。所以我的建议是:尽量固定一个主数据源,其他作为校验。
小技巧:接入数据时,顺手记录数据源的「元信息」——比如数据版本、拉取时间、源地址。这样出问题时能快速定位。
3.2 数据清洗:80%的时间花在这里
数据清洗,说白了就是「把脏数据变成干净数据」。我见过太多人,数据拉下来直接扔进回测引擎,结果跑出来的夏普比率高得离谱——一查,原来是前复权没处理好。
清洗环节,我一般做这几件事:
- 去重:同一个时间戳出现两条记录,保留哪条?我通常保留后一条,因为可能是修正数据。
- 补缺失值:停牌日、节假日,数据是空的。用前值填充还是插值?看策略类型。趋势策略用前值,均值回归策略用插值。
- 异常值处理:突然出现一个涨跌幅1000%的数据点,大概率是数据错误。我一般用3倍标准差法剔除。
- 复权处理:前复权还是后复权?我个人习惯用前复权,因为更贴近实际交易价格。
# 一个简单的清洗流程示例(伪代码)
def clean_data(df):
# 1. 去重
df = df.drop_duplicates(subset=['timestamp', 'symbol'])
# 2. 排序
df = df.sort_values('timestamp')
# 3. 填充缺失值(前向填充)
df = df.fillna(method='ffill')
# 4. 异常值处理(3倍标准差)
mean = df['close'].mean()
std = df['close'].std()
df = df[(df['close'] > mean - 3*std) & (df['close'] < mean + 3*std)]
return df
注意:清洗逻辑一定要可复现。我曾经因为清洗脚本改了没记录,导致前后两次回测结果对不上,查了三天才发现是填充方式变了。
3.3 数据存储:选对格式,事半功倍
数据存哪里?怎么存?这个问题看似简单,但选错了后面会很难受。
我根据使用场景,给出几个建议:
| 场景 | 推荐格式 | 原因 |
|---|---|---|
| 小规模回测(<1GB) | CSV / Parquet | 简单直接,方便查看 |
| 中规模(1-100GB) | HDF5 / Parquet | 压缩率高,读取快 |
| 大规模(>100GB) | ClickHouse / InfluxDB | 列式存储,查询性能极佳 |
| 需要频繁更新 | SQLite / PostgreSQL | 支持事务,方便增量写入 |
我个人最常用的是Parquet格式。为什么?因为它压缩率高,读取速度快,而且支持列式存储——你只需要读取需要的列,不用把整个表都加载到内存。对于动辄几千万行的分钟线数据,这个优势太明显了。
经验之谈:数据存储时,按「品种+日期」分区。比如 data/symbol=000001/date=2024-01-01.parquet。这样查询特定日期、特定股票的数据,速度能快10倍以上。
3.4 数据对齐:回测精度的命门
数据对齐,是数据模块里最容易被忽视、但影响最大的环节。
什么叫数据对齐?举个例子:你有两只股票A和B。A在9:30:00有数据,B在9:30:05才有数据。如果你直接拿这两个时间点的数据做计算,那结果就是错的——因为时间戳没对齐。
对齐的核心是统一时间基准。我一般这么做:
- 确定目标频率:比如1分钟、5分钟、日线
- 生成标准时间轴:从起始日期到结束日期,按目标频率生成时间戳序列
- 重采样:把原始数据映射到标准时间轴上。用前值填充还是后值填充?看策略逻辑
- 检查对齐结果:对齐后,检查每个时间点是否都有数据。如果有缺失,标记出来
# 数据对齐示例(伪代码)
def align_data(df_list, freq='1min'):
# 生成标准时间轴
start = min([df.index.min() for df in df_list])
end = max([df.index.max() for df in df_list])
standard_index = pd.date_range(start, end, freq=freq)
# 对每个数据框重采样
aligned_dfs = []
for df in df_list:
aligned = df.reindex(standard_index, method='ffill')
aligned_dfs.append(aligned)
return aligned_dfs
我曾经踩过的坑:做跨市场回测时,A股和港股的开盘时间不同,节假日也不同。直接对齐会导致大量缺失值。后来我加了一个「交易日历」模块,专门处理不同市场的交易时间差异。嗯,这个坑希望大家别踩。
3.5 数据模块的整体架构
最后,我画一张图,把数据模块的完整架构展示出来。你写代码的时候,可以照着这个结构来组织:
data_module/
├── data_source/ # 数据源接入层
│ ├── api_source.py # API数据源
│ ├── db_source.py # 数据库数据源
│ └── file_source.py # 文件数据源
├── data_clean/ # 数据清洗层
│ ├── dedup.py # 去重
│ ├── fillna.py # 补缺失
│ └── outlier.py # 异常值处理
├── data_storage/ # 数据存储层
│ ├── parquet_store.py # Parquet存储
│ ├── hdf5_store.py # HDF5存储
│ └── sql_store.py # SQL存储
├── data_align/ # 数据对齐层
│ ├── resampler.py # 重采样
│ └── calendar.py # 交易日历
└── data_pipeline.py # 数据管道(编排以上所有模块)
这个架构的好处是各层解耦。你想换数据源?只改 data_source 层。想换存储格式?只改 data_storage 层。其他代码完全不用动。
总结一下:数据模块设计,核心就四个字——干净、对齐。数据干净了,对齐了,回测结果才有参考价值。否则,你就是在用错误的数据,验证错误的策略,得出错误的结论。
好了,这一章就聊到这里。数据模块是回测系统的基石,花再多时间打磨都值得。下一章咱们聊聊策略引擎的设计——那才是真正烧脑的地方。