第三章 数据模块设计:数据源接入、数据清洗、数据存储、数据对齐

做量化交易,有一句话我特别认同:「垃圾进,垃圾出」。你策略再牛,模型再花哨,底层数据是脏的、乱的、对不齐的,那结果就是一场自嗨。

这一章,咱们就聊聊数据模块怎么设计。说白了,就是解决四个问题:数据从哪来、怎么洗干净、存到哪去、怎么对齐。我做了这么多年回测系统,踩过的坑有一半都在数据层。今天我把这些经验掰开了讲给你听。

核心观点:数据模块是整个回测系统的地基。地基不稳,楼盖得越高,塌得越快。

数据模块核心流程 数据源接入 数据清洗 数据存储 数据对齐 API / 数据库 / CSV 去重 / 补缺 / 异常值 Parquet / HDF5 / SQL 时间戳对齐 / 频率统一 数据流方向:从左到右,环环相扣 每个环节出问题,都会影响回测结果的真实性 ⚠️ 常见误区:跳过清洗直接对齐,数据质量无法保证

3.1 数据源接入:别只盯着一个渠道

数据源接入,听起来简单,不就是调个API或者读个CSV吗?嗯,确实不复杂,但坑很多。

我个人习惯把数据源分成三类:

  • 实时API:比如交易所的WebSocket接口,适合做高频或准实时策略
  • 历史数据库:比如从Wind、Tushare、AKShare拉下来的日线、分钟线
  • 本地文件:CSV、Excel、Parquet,适合做离线研究和回测

我在项目中遇到过一个问题:同一个股票,从两个不同API拿到的数据,开盘价差了0.01元。你想想看,0.01元对单笔交易影响不大,但回测跑1000笔,误差就累积起来了。所以我的建议是:尽量固定一个主数据源,其他作为校验

小技巧:接入数据时,顺手记录数据源的「元信息」——比如数据版本、拉取时间、源地址。这样出问题时能快速定位。

3.2 数据清洗:80%的时间花在这里

数据清洗,说白了就是「把脏数据变成干净数据」。我见过太多人,数据拉下来直接扔进回测引擎,结果跑出来的夏普比率高得离谱——一查,原来是前复权没处理好。

清洗环节,我一般做这几件事:

  1. 去重:同一个时间戳出现两条记录,保留哪条?我通常保留后一条,因为可能是修正数据。
  2. 补缺失值:停牌日、节假日,数据是空的。用前值填充还是插值?看策略类型。趋势策略用前值,均值回归策略用插值。
  3. 异常值处理:突然出现一个涨跌幅1000%的数据点,大概率是数据错误。我一般用3倍标准差法剔除。
  4. 复权处理:前复权还是后复权?我个人习惯用前复权,因为更贴近实际交易价格。
# 一个简单的清洗流程示例(伪代码)
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 层。其他代码完全不用动。

总结一下:数据模块设计,核心就四个字——干净、对齐。数据干净了,对齐了,回测结果才有参考价值。否则,你就是在用错误的数据,验证错误的策略,得出错误的结论。

好了,这一章就聊到这里。数据模块是回测系统的基石,花再多时间打磨都值得。下一章咱们聊聊策略引擎的设计——那才是真正烧脑的地方。

蓝海数据掘金营,专注资料整理