3. 数据准备与预处理:数据源选择、数据清洗、对齐时间戳、计算价差序列

做量化交易,尤其是价差回归策略,有一句老话我特别认同:「垃圾进,垃圾出」。你策略再牛,模型再花哨,数据源选错了,或者数据没洗干净,后面全是白忙活。我自己就吃过这个亏——有一回跑回测,收益曲线漂亮得不像话,结果发现是数据里混了前复权因子没处理干净,虚胖。

所以这一章,咱们就踏踏实实把数据准备工作捋清楚。说白了,就是四步走:选数据源 → 清洗脏数据 → 对齐时间戳 → 算出价差序列。每一步都有坑,我尽量把踩过的坑都给你指出来。

3.1 数据源选择:别在源头上翻车

我个人习惯,数据源至少要满足三个条件:

  • 覆盖全:你要做的品种,历史K线得够长,至少3-5年吧?
  • 精度够:分钟级数据最好,日线做价差有点糙。
  • 接口稳:别动不动断流,或者数据突然少一段。

常用的数据源我列一下,供你参考:

数据源 优点 缺点 我常用的场景
Wind / 聚宽 国内股票期货覆盖全,质量高 收费,接口有时限 股票配对交易
Binance / OKX API 免费,实时性好,历史数据全 只限加密货币 数字货币价差套利
Yahoo Finance 免费,美股ETF方便 数据偶尔有缺失,前复权处理需注意 美股ETF价差回测
本地CSV/Parquet 完全可控,离线可用 需要自己维护更新 生产环境回测
我的小建议:刚开始做研究,用免费API就够了。但一旦要上实盘,我建议你至少准备两个数据源做交叉验证。我曾经遇到过Binance和OKX的同一时刻BTC价格差了0.3%,不是滑点,是数据源本身的时间戳对齐问题。

3.2 数据清洗:把脏东西筛出去

拿到原始数据后,别急着算价差。先做三件事:

3.2.1 处理缺失值

金融数据里,缺失值很常见——节假日、停牌、网络波动。我的处理原则是:

  • 日内分钟数据:如果连续缺失超过5根K线,直接丢弃当天数据。
  • 日线数据:用前一个交易日填充,或者用插值法。
  • 千万别用0填充!那会把你的价差序列搞出巨大的假信号。

3.2.2 剔除异常值

什么叫异常值?比如某根K线的价格突然跳了10%,明显是数据录入错误。我一般用3倍标准差法或者百分位截断法(比如去掉上下0.5%的极端值)。

import pandas as pd
import numpy as np

def clean_outliers(df, column='close', method='zscore', threshold=3):
    """
    剔除异常值,返回布尔索引
    """
    if method == 'zscore':
        z = np.abs((df[column] - df[column].mean()) / df[column].std())
        return df[z < threshold]
    elif method == 'percentile':
        lower = df[column].quantile(0.005)
        upper = df[column].quantile(0.995)
        return df[(df[column] >= lower) & (df[column] <= upper)]
注意:剔除异常值后,一定要检查一下时间序列是否连续。我曾经因为剔除太多,导致某个月份只剩3天数据,回测结果完全失真。

3.2.3 复权处理

做股票价差,复权是绕不开的坎。我的建议是:统一用后复权。前复权会改变历史价格,导致价差序列出现「假突破」。后复权虽然当前价格看起来不对,但价差关系是真实的。

3.3 对齐时间戳:让两个品种「同步」

价差策略的核心,是同一时刻两个品种的价格差。如果时间戳没对齐,你算出来的价差就是「关公战秦琼」。我遇到过最坑的一次,是A股和港股配对,A股中午休市,港股不休,结果价差序列在中午那段完全乱套。

对齐时间戳,我一般分三步:

  1. 统一时区:全部转成UTC+0,避免夏令时问题。
  2. 统一频率:比如都转成1分钟K线,用前一根K线的收盘价填充缺失。
  3. 剔除不重叠时段:比如A股和港股,只保留两者都交易的时间段。
def align_timestamps(df1, df2, freq='1min'):
    """
    对齐两个DataFrame的时间戳,按指定频率重采样
    """
    # 统一索引为datetime类型
    df1.index = pd.to_datetime(df1.index)
    df2.index = pd.to_datetime(df2.index)
    
    # 重采样到统一频率
    df1_resampled = df1.resample(freq).last().ffill()
    df2_resampled = df2.resample(freq).last().ffill()
    
    # 取交集时间
    common_index = df1_resampled.index.intersection(df2_resampled.index)
    
    return df1_resampled.loc[common_index], df2_resampled.loc[common_index]
核心要点:对齐时间戳后,一定要检查一下两个序列的长度是否一致。如果差太多,说明数据源本身就有问题,别硬算。

3.4 计算价差序列:从价格到信号

数据洗干净、时间也对齐了,终于可以算价差了。价差的计算方式,取决于你的策略假设:

  • 简单价差:P1 - P2,适用于同品种不同到期日,或者同板块股票。
  • 对数价差:ln(P1) - ln(P2),适用于价格差异较大的品种,能消除量纲影响。
  • 回归残差:用OLS回归算出协整系数,然后取残差。这是最严谨的做法,但计算量也最大。

我个人习惯,先用简单价差快速验证,如果效果不错,再上回归残差。别一上来就搞复杂的,容易过拟合。

def calculate_spread(price1, price2, method='simple'):
    """
    计算价差序列
    """
    if method == 'simple':
        spread = price1 - price2
    elif method == 'log':
        spread = np.log(price1) - np.log(price2)
    elif method == 'ols':
        # 用OLS回归计算残差
        import statsmodels.api as sm
        X = sm.add_constant(price2)
        model = sm.OLS(price1, X).fit()
        spread = model.resid
    else:
        raise ValueError("Unknown method")
    
    return spread

3.5 本章知识体系总览

下面这张图,是我自己画的数据准备流程。你跟着走一遍,基本不会出大问题。

数据准备与预处理流程 数据源选择 Wind / Binance / CSV 数据清洗 缺失值 / 异常值 / 复权 对齐时间戳 统一时区 / 频率 / 时段 计算价差序列 简单 / 对数 / OLS残差 每一步都可能有坑,建议交叉验证 数据质量 = 策略效果 × 0.8
避坑指南:我曾经在计算价差时,直接用两个品种的收盘价相减,结果发现价差序列有很强的自相关性。后来才发现,是因为两个品种的停牌时间不同,导致时间戳没对齐。从那以后,我每次算价差前,都会先画一张时间戳重叠图,看一眼再动手。

好了,数据准备这块就这些。你按这个流程走一遍,基本不会出大问题。下一章咱们就开始正式做参数优化了——不过在那之前,先把数据搞干净,这是最笨但最有效的办法。

专注资料整理