3、数据获取与清洗:多源数据接口、数据对齐、缺失值处理、异常值检测
做配对交易的朋友,十有八九都栽在数据上。我见过太多策略回测漂亮,一上实盘就崩,最后查出来是数据源没对齐。说白了,数据是量化交易的命根子,尤其是多品种配对,数据一乱,整个策略就废了。
这一章,我把自己这些年踩过的坑、总结的经验,全盘托出。咱们从数据获取开始,一步步把数据洗干净。
3.1 多源数据接口:别把鸡蛋放一个篮子里
我个人习惯,至少准备两个数据源。为什么?你想想看,万一主数据源宕机,你的策略就瞎了。我在项目中遇到过某头部数据商半夜维护,结果我的夜盘策略直接断粮,亏了不少。
常用的数据接口有这些:
| 数据源 | 特点 | 适用场景 |
|---|---|---|
| Wind/Polygon | 权威、稳定、贵 | 机构级、回测基准 |
| Yahoo Finance/Alpha Vantage | 免费、易用、延迟高 | 个人研究、低频策略 |
| 交易所直连(CTP/IB) | 实时、精准、门槛高 | 高频、实盘交易 |
| 数据库(InfluxDB/ClickHouse) | 自建、灵活、维护成本 | 团队协作、历史数据 |
代码层面,我习惯封装一个统一的数据获取接口。这样切换数据源时,只需要改配置,不用改业务逻辑。
# 统一数据接口示例
class DataFetcher:
def __init__(self, source='wind'):
self.source = source
self.conn = self._connect(source)
def get_bars(self, symbol, start, end, freq='1d'):
# 统一返回格式:DataFrame with columns [open, high, low, close, volume]
if self.source == 'wind':
return self._from_wind(symbol, start, end, freq)
elif self.source == 'yahoo':
return self._from_yahoo(symbol, start, end, freq)
# 可以继续扩展
def _from_wind(self, symbol, start, end, freq):
# 实际调用Wind API
pass
def _from_yahoo(self, symbol, start, end, freq):
# 实际调用yfinance
pass
3.2 数据对齐:时间戳是最大的敌人
多品种配对,最头疼的就是时间对齐。不同交易所、不同品种,交易时间不一样。A股9:30开盘,港股9:00,美股9:30(夏令时)。你拿A股和港股做配对,时间戳对不上,算出来的价差全是错的。
我曾经犯过一个低级错误:把A股和美股的数据直接拼接,没考虑时区。结果回测收益曲线漂亮得不行,实盘一跑就亏。后来发现,美股数据比A股晚了12个小时,价差计算完全错位。
对齐的核心原则:以最慢的数据频率为基准,统一时间戳。
- 将所有数据转为UTC时间戳
- 按目标频率(如1分钟、1天)重采样
- 使用前向填充或插值,确保每个时间点都有数据
- 剔除非共同交易时段的数据
代码实现上,我推荐用pandas的reindex和asfreq。嗯,这里要注意:重采样时,一定要指定填充方法,不然会出现大量NaN。
import pandas as pd
def align_data(df1, df2, freq='1min'):
# 统一时间索引
df1.index = pd.to_datetime(df1.index, utc=True)
df2.index = pd.to_datetime(df2.index, utc=True)
# 生成共同时间轴
start = max(df1.index.min(), df2.index.min())
end = min(df1.index.max(), df2.index.max())
common_idx = pd.date_range(start, end, freq=freq)
# 重采样并对齐
df1_aligned = df1.reindex(common_idx, method='ffill')
df2_aligned = df2.reindex(common_idx, method='ffill')
return df1_aligned, df2_aligned
3.3 缺失值处理:别直接删,先看看为什么
数据缺失是常态。但缺失的原因不同,处理方式也不同。我一般把缺失值分成三类:
- 随机缺失:网络波动、API限流。这种可以插值或填充。
- 非随机缺失:停牌、节假日。这种不能随便填充,要标记出来。
- 结构性缺失:新品种上市、退市。这种需要重新考虑配对组合。
我的处理流程是这样的:
- 先统计缺失比例。超过20%的品种,直接剔除。
- 判断缺失类型。停牌日、节假日,用NaN标记,后续计算时跳过。
- 随机缺失用线性插值或前向填充。我个人偏好前向填充,因为金融数据有记忆性。
- 最后做一次平滑,防止插值引入的毛刺。
def handle_missing(df, max_missing_ratio=0.2):
# 统计缺失比例
missing_ratio = df.isnull().sum() / len(df)
if missing_ratio > max_missing_ratio:
raise ValueError(f"缺失比例过高: {missing_ratio:.2%}")
# 标记停牌日(假设volume为0表示停牌)
df.loc[df['volume'] == 0, ['open', 'high', 'low', 'close']] = None
# 前向填充
df = df.fillna(method='ffill')
# 如果开头还有缺失,用后向填充
df = df.fillna(method='bfill')
return df
3.4 异常值检测:别让一个坏数据毁了整个策略
异常值检测,说白了就是找那些「不合理」的数据。比如某只股票突然涨了100倍,或者成交量突然为0。这些数据如果不处理,配对交易的价差计算会完全失真。
我常用的方法有三种:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| Z-score | 基于均值和标准差 | 正态分布的数据 |
| IQR(四分位距) | 基于中位数和分位数 | 非正态分布、有极端值 |
| 滚动窗口法 | 基于局部统计量 | 时间序列、趋势明显 |
我个人最常用的是滚动窗口法。为什么?因为金融数据有趋势,全局的Z-score会把趋势中的正常波动误判为异常。比如一只股票从10块涨到100块,用全局Z-score看,中间很多点都是异常,但实际上它们是正常的上涨趋势。
def detect_outliers_rolling(df, column='close', window=40, n_std=3):
# 计算滚动均值和标准差
rolling_mean = df[column].rolling(window=window).mean()
rolling_std = df[column].rolling(window=window).std()
# 计算上下界
upper_bound = rolling_mean + n_std * rolling_std
lower_bound = rolling_mean - n_std * rolling_std
# 标记异常
outliers = (df[column] > upper_bound) | (df[column] < lower_bound)
# 处理异常值:用滚动中位数替换
df.loc[outliers, column] = df[column].rolling(window=window).median()
return df, outliers
3.5 知识体系总览
说了这么多,我画了一张图,把数据获取与清洗的整个流程串起来。你照着这个框架做,基本不会出大问题。
这张图把整个流程分成了四层:数据源层、对齐层、清洗层、输出层。每一层都有明确的输入输出。你按照这个流程走,数据质量基本有保障。
公众号:蓝海资料掘金营,微信deep3321