2. 数据源验证:历史行情数据完整性检查、Tick级数据对齐、除权除息数据校验
做回测系统这么多年,我踩过最大的坑,往往不是策略逻辑写错了,而是数据本身有问题。你想想看,如果喂给策略的数据都是错的,那回测结果再漂亮,也不过是垃圾进垃圾出。这一章,我们就来聊聊数据源验证这件事。
说白了,数据验证就是给你的回测系统装上一道安检门。我个人的习惯是,宁可花30%的开发时间在数据校验上,也不愿意花300%的时间去排查一个由数据错误导致的诡异回测结果。
核心观点:数据验证不是可有可无的步骤,而是回测系统的生命线。没有经过严格验证的数据,回测结果毫无意义。
2.1 历史行情数据完整性检查
完整性检查,说白了就是看看你的数据有没有「缺斤少两」。我在项目中遇到过好几次,回测跑得好好的,突然某一天收益率曲线出现一个诡异的跳变,查了半天才发现是某只股票缺了一天的数据。
完整性检查通常包括以下几个方面:
- 时间连续性检查:交易日是否连续?有没有缺失的日期?
- 数据字段完整性:每条记录的开盘价、最高价、最低价、收盘价、成交量是否齐全?
- 时间戳合法性:时间戳是否在合理范围内?有没有未来的数据?
- 重复数据检查:同一时间点是否存在多条记录?
这里我分享一个我常用的检查脚本片段:
def check_data_integrity(df, symbol):
"""
检查历史行情数据的完整性
"""
issues = []
# 1. 检查时间连续性
df['date'] = pd.to_datetime(df['date'])
date_range = pd.date_range(start=df['date'].min(),
end=df['date'].max(),
freq='B') # 仅交易日
missing_dates = date_range.difference(df['date'])
if len(missing_dates) > 0:
issues.append(f"缺失交易日: {len(missing_dates)} 天")
# 2. 检查字段完整性
required_fields = ['open', 'high', 'low', 'close', 'volume']
for field in required_fields:
null_count = df[field].isnull().sum()
if null_count > 0:
issues.append(f"{field} 字段存在 {null_count} 个空值")
# 3. 检查价格合理性
if (df['high'] < df['low']).any():
issues.append("存在最高价低于最低价的异常记录")
return issues
小技巧:对于A股市场,我建议使用「交易日历」而非简单的「工作日」来判断连续性。因为A股有调休、春节等特殊安排,直接用工作日会误报很多「缺失」。我一般会从交易所官网下载官方的交易日历。
2.2 Tick级数据对齐
Tick级数据,说白了就是逐笔成交数据。这东西精度高,但坑也多。我记得有一次做高频策略回测,发现策略在某个时间点突然大量开仓,查了半天才发现是Tick数据的时间戳没对齐。
为什么会这样?因为不同交易所、不同数据源的时间戳精度可能不一样。有的精确到毫秒,有的精确到微秒,甚至有的只精确到秒。你想想看,如果两个数据源的时间戳精度不一致,那对齐的时候就会出现偏差。
Tick级数据对齐的核心步骤:
- 时间戳标准化:将所有数据源的时间戳统一到相同的精度(建议使用微秒级)
- 时钟漂移校正:检查并校正不同数据源之间的时钟偏差
- 数据交叉验证:用多个数据源互相验证,找出不一致的记录
- 异常值过滤:剔除明显不合理的数据点(比如价格超出当日涨跌停板)
我曾经遇到过一个经典案例:某数据源的Tick数据在每天开盘前5分钟会「提前」出现一些成交记录。后来发现是数据源的时间戳没有考虑集合竞价阶段。嗯,这里要注意,不同交易所的集合竞价规则不一样,处理方式也不同。
def align_tick_data(tick_df, exchange='SH'):
"""
Tick级数据对齐
"""
# 1. 标准化时间戳到微秒
tick_df['timestamp'] = pd.to_datetime(tick_df['timestamp'],
unit='us')
# 2. 根据交易所规则过滤
if exchange == 'SH':
# 上交所:9:15-9:25 集合竞价,9:30-11:30, 13:00-15:00 连续竞价
market_hours = tick_df['timestamp'].dt.time.between(
pd.Timestamp('09:15:00').time(),
pd.Timestamp('15:00:00').time()
)
tick_df = tick_df[market_hours]
# 3. 检查时间戳单调性
if not tick_df['timestamp'].is_monotonic_increasing:
tick_df = tick_df.sort_values('timestamp')
return tick_df
警告:千万不要直接对Tick数据进行「线性插值」来填补缺失值!Tick数据是离散事件,不是连续信号。我曾经见过有人用插值法补Tick数据,结果回测出来的夏普比率高得离谱,一上实盘就崩了。
2.3 除权除息数据校验
除权除息,这是回测系统里最容易出问题的地方之一。我刚开始做回测的时候,就吃过这个亏。策略在分红除息日前后表现异常,查了三天才发现是复权数据没处理好。
除权除息数据校验的核心要点:
- 复权方式一致性:前复权、后复权、不复权,三种方式必须统一
- 除权除息日准确性:核对交易所公告,确保除权除息日没有偏差
- 复权因子计算:验证复权因子的计算是否正确
- 分红送转数据:检查每股分红、送股比例等数据是否准确
我个人习惯的做法是,维护一个独立的「除权除息事件表」,而不是依赖数据源提供的复权数据。这样我可以自己控制复权逻辑,出了问题也能快速定位。
def validate_corporate_actions(corp_df, price_df):
"""
校验除权除息数据
"""
errors = []
for idx, row in corp_df.iterrows():
ex_date = row['ex_date']
symbol = row['symbol']
# 获取除权除息日前后价格
before_price = price_df.loc[
(price_df['symbol'] == symbol) &
(price_df['date'] < ex_date),
'close'
].iloc[-1]
after_price = price_df.loc[
(price_df['symbol'] == symbol) &
(price_df['date'] >= ex_date),
'close'
].iloc[0]
# 计算理论除权价
dividend = row['dividend']
bonus_ratio = row['bonus_ratio']
theoretical_price = (before_price - dividend) / (1 + bonus_ratio)
# 检查实际价格与理论价格的偏差
deviation = abs(after_price - theoretical_price) / theoretical_price
if deviation > 0.01: # 允许1%的偏差
errors.append(f"{symbol} 在 {ex_date} 除权价偏差 {deviation:.2%}")
return errors
避坑指南:我曾经遇到过数据源把「除权除息日」和「股权登记日」搞混的情况。这两个日期差一天,但影响巨大。我的建议是,一定要从交易所官网或权威数据源获取除权除息日历,不要完全依赖第三方数据源。
2.4 数据验证框架图
下面这张图是我在实际项目中使用的数据验证框架,你可以参考一下:
这张图展示了我常用的数据验证流程。从原始数据源开始,经过三个并行验证模块(完整性检查、Tick级对齐、除权除息校验),然后进入数据清洗与标准化层,最终输出验证通过的数据。每个模块之间都有明确的输入输出接口,方便独立测试和调试。
2.5 实战经验总结
做了这么多年回测系统,我总结了几条数据验证的「铁律」:
- 永远不要相信任何数据源:不管数据源多权威,都要做交叉验证
- 验证要自动化:每次数据更新后自动运行验证脚本,不要手动检查
- 保留原始数据:清洗后的数据可以覆盖,但原始数据一定要备份
- 记录验证日志:每次验证的结果都要记录下来,方便追溯问题
最后提醒一句:数据验证不是一次性工作,而是持续的过程。数据源可能会更新、交易所规则可能会变化、新的数据问题可能会出现。我建议每周至少运行一次完整的数据验证流程,确保数据质量始终在线。