3. 数据模型设计:OHLCV数据结构、时间序列索引、数据清洗与对齐、缺失值处理
做量化回测,数据就是你的命根子。我见过太多人策略写得花里胡哨,结果数据一塌糊涂,回测结果全是幻觉。今天咱们就把数据模型这块地基打牢。
3.1 OHLCV数据结构——最经典的金融数据格式
OHLCV,说白了就是Open(开盘价)、High(最高价)、Low(最低价)、Close(收盘价)、Volume(成交量)。这五个字段,承载了金融市场最核心的价格行为信息。
我个人习惯用Pandas的DataFrame来存储,结构如下:
import pandas as pd
import numpy as np
# 一个标准的OHLCV DataFrame
ohlcv_data = pd.DataFrame({
'open': [100.0, 101.5, 102.3],
'high': [102.0, 103.8, 104.1],
'low': [99.5, 100.2, 101.8],
'close':[101.2, 102.9, 103.5],
'volume':[15000, 18000, 22000]
}, index=pd.to_datetime(['2024-01-02', '2024-01-03', '2024-01-04']))
print(ohlcv_data)
嗯,这里要注意:开盘价和收盘价是交易时段的起点和终点,最高最低价则是这个时段内的极端值。我在项目中遇到过有人把最高价和收盘价搞反,回测出来的夏普比率高得离谱——后来发现是数据错了。
3.2 时间序列索引——数据的灵魂
为什么时间索引这么重要?你想想看,回测的本质就是在时间轴上模拟交易。没有正确的时间索引,你根本不知道什么时候该开仓、什么时候该平仓。
我建议把时间索引设为DatetimeIndex,而不是用字符串:
# 正确做法:使用DatetimeIndex
df.index = pd.to_datetime(df.index)
# 错误做法:保留字符串索引
# df.index = ['2024-01-02', '2024-01-03', ...] # 千万别这么干
为什么?因为DatetimeIndex支持时间切片、频率转换、时区处理。我记得有一次做跨市场套利策略,A股和港股的时间戳格式不一样,用字符串索引处理起来简直噩梦。换成DatetimeIndex后,一行代码搞定对齐。
dayfirst=False 或 format='%Y-%m-%d',避免日期解析错误。我吃过这个亏,数据里混了美式日期格式,结果1月2日被解析成了2月1日。
3.3 数据清洗与对齐——别让脏数据毁了你的策略
真实世界的数据,没有完美的。停牌、除权除息、数据源错误……各种问题等着你。数据清洗,就是把这些脏东西筛出去。
常见的清洗操作:
- 去除重复行:同一个时间戳出现两次,保留第一条或最后一条
- 检查价格合理性:开盘价不能为负,最高价不能低于最低价
- 成交量异常:突然暴增或归零,需要标记出来
# 去除重复索引
df = df[~df.index.duplicated(keep='first')]
# 检查价格合理性
assert (df['high'] >= df['low']).all(), "最高价低于最低价,数据异常!"
assert (df['high'] >= df['open']).all(), "最高价低于开盘价,数据异常!"
# 成交量异常检测
volume_zscore = (df['volume'] - df['volume'].mean()) / df['volume'].std()
outliers = df[volume_zscore.abs() > 3] # 超过3个标准差
数据对齐呢?说白了就是让不同数据源的时间戳对上。比如你要同时用日线数据和分钟线数据,它们的索引长度不一样,必须对齐到同一个时间轴上。
# 对齐到日线时间轴
daily_data = daily_data.reindex(minute_data.index, method='ffill')
reindex 时忘了指定 method,结果对齐后全是NaN,策略直接报错。记住:ffill(向前填充)是金融数据对齐的标配。
3.4 缺失值处理——填还是不填?这是个问题
缺失值在金融数据里太常见了。非交易日、停牌、数据源中断……都会产生NaN。处理方式取决于你的策略类型:
| 策略类型 | 推荐处理方式 | 原因 |
|---|---|---|
| 趋势跟踪策略 | 向前填充(ffill) | 假设价格不变,符合市场实际 |
| 均值回归策略 | 删除缺失行 | 避免引入虚假的回归信号 |
| 高频策略 | 插值法 | 需要连续的价格序列 |
| 多因子模型 | 行业均值填充 | 保持样本量,减少偏差 |
# 向前填充(最常用)
df_filled = df.fillna(method='ffill')
# 线性插值
df_interpolated = df.interpolate(method='linear')
# 删除缺失行
df_dropped = df.dropna()
我个人习惯:先用ffill,再检查剩余缺失值。如果缺失比例超过5%,我会重新审视数据源。别想着用复杂的插值方法去“创造”数据,那是在自欺欺人。
3.5 知识体系总览
下面这张图,把数据模型设计的核心逻辑串起来了:
数据模型设计,说白了就是给策略搭一个干净、可靠的数据舞台。舞台搭不好,再好的演员(策略)也演不出好戏。我做了这么多年量化,最大的感悟就是:花80%的时间处理数据,花20%的时间写策略——这才是正确的比例。
嗯,数据模型这块就聊到这儿。记住:干净的数据 + 正确的时间索引 = 可信的回测结果。别在这上面偷懒,否则后面所有的工作都是白费。