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个小时,价差计算完全错位。

对齐的核心原则:以最慢的数据频率为基准,统一时间戳

对齐步骤:
  1. 将所有数据转为UTC时间戳
  2. 按目标频率(如1分钟、1天)重采样
  3. 使用前向填充或插值,确保每个时间点都有数据
  4. 剔除非共同交易时段的数据

代码实现上,我推荐用pandas的reindexasfreq。嗯,这里要注意:重采样时,一定要指定填充方法,不然会出现大量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限流。这种可以插值或填充。
  • 非随机缺失:停牌、节假日。这种不能随便填充,要标记出来。
  • 结构性缺失:新品种上市、退市。这种需要重新考虑配对组合。
避坑指南: 我曾经直接把缺失值用前一天的收盘价填充,结果遇到停牌复牌后的跳空缺口,策略直接爆仓。停牌期间的数据,绝对不能填充,必须标记为无效。

我的处理流程是这样的:

  1. 先统计缺失比例。超过20%的品种,直接剔除。
  2. 判断缺失类型。停牌日、节假日,用NaN标记,后续计算时跳过。
  3. 随机缺失用线性插值或前向填充。我个人偏好前向填充,因为金融数据有记忆性。
  4. 最后做一次平滑,防止插值引入的毛刺。
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看,中间很多点都是异常,但实际上它们是正常的上涨趋势。

我的经验: 滚动窗口的窗口大小,一般设为20-60个周期。太短了容易误判,太长了反应迟钝。我习惯用40个周期,配合3倍标准差。
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 知识体系总览

说了这么多,我画了一张图,把数据获取与清洗的整个流程串起来。你照着这个框架做,基本不会出大问题。

数据获取与清洗流程 多源数据接口 Wind / Yahoo / 交易所 数据缓存层 本地数据库 / CSV 统一接口封装 DataFetcher 类 数据对齐 统一时间戳 → 重采样 → 前向填充 → 剔除非共同时段 核心:以最慢频率为基准 缺失值处理 分类判断 → 插值/填充 → 标记停牌 缺失>20%直接剔除 异常值检测 滚动窗口法 → 标记 → 替换 窗口40周期,3倍标准差 干净、对齐、可用的配对数据

这张图把整个流程分成了四层:数据源层、对齐层、清洗层、输出层。每一层都有明确的输入输出。你按照这个流程走,数据质量基本有保障。

最后说一句: 数据清洗没有一劳永逸的方案。不同品种、不同频率、不同策略,对数据的要求都不一样。我的建议是,每次接入新品种,先花一周时间做数据质量测试。别急着跑策略,先把数据搞明白。

公众号:蓝海资料掘金营,微信deep3321