4. 数据准备与预处理:获取股票行情数据、行业分类数据对齐、数据清洗与去极值

做配对交易,数据是地基。地基没打好,后面模型再漂亮也是白搭。我见过太多人一上来就搞协整检验,结果数据里全是脏值,最后回测曲线漂亮得不行,实盘一跑就崩。说白了,数据预处理这一步,花再多时间都值得。

这一节,咱们就聊聊怎么把原始数据收拾得服服帖帖。我会把我在实际项目中踩过的坑、用过的招儿,都摊开来跟你讲。

4.1 获取股票行情数据

行情数据是配对交易的基础原料。你需要的是日频的收盘价,最好是后复权价格。为什么?因为前复权会扭曲历史收益率,而配对交易恰恰依赖历史价格关系。

我个人习惯用 akshare 或者 tushare 来拉数据。代码很简单,但有几个细节要注意。

import akshare as ak
import pandas as pd
import numpy as np

# 获取股票日线数据
def fetch_stock_data(symbol, start_date, end_date):
    df = ak.stock_zh_a_hist(
        symbol=symbol, 
        period="daily", 
        start_date=start_date, 
        end_date=end_date, 
        adjust="qfq"  # 前复权
    )
    # 我习惯用后复权,但akshare默认前复权
    # 所以这里我手动算一下后复权
    df['close_adj'] = df['收盘'] * (df['复权因子'] if '复权因子' in df.columns else 1)
    return df[['日期', 'close_adj']]
注意: 不同数据源对复权的处理方式不一样。我曾经用过一个数据源,它的前复权会把历史价格算成负数,导致配对比例完全乱套。所以拿到数据后,先画个图看看,价格曲线是不是平滑的。

数据频率上,日线就够了。配对交易不是高频策略,日线级别的信号足够你捕捉均值回归的机会。当然,如果你做的是日内配对,那就得用分钟线了。

4.2 行业分类数据对齐

行业中性化的前提是——你得知道每只股票属于哪个行业。这里有个坑:不同分类标准下,同一只股票可能被分到不同行业。比如茅台,在申万一级里是食品饮料,在GICS里是必需消费。

我建议统一用申万一级行业分类。原因很简单:A股市场里,申万分类用得最广,而且更新及时。

# 获取申万行业分类
def get_industry_map():
    # 这里用akshare获取行业分类
    industry_df = ak.stock_board_industry_name_em()
    # 转换成字典:股票代码 -> 行业名称
    stock_industry = {}
    for _, row in industry_df.iterrows():
        board_code = row['板块代码']
        # 获取该行业下的成分股
        members = ak.stock_board_industry_cons_em(symbol=board_code)
        for _, m in members.iterrows():
            stock_industry[m['代码']] = row['板块名称']
    return stock_industry
小技巧: 行业分类每年都会调整。我建议每季度更新一次行业映射表。别偷懒,我见过有人用了一年前的分类,结果某只股票已经被调出原行业了,配对信号完全失真。

数据对齐这一步,说白了就是把行情数据和行业数据按股票代码拼起来。注意,有些股票可能没有行业分类(比如刚上市的新股),这些股票我建议直接剔除。

4.3 数据清洗

数据清洗是脏活累活,但必须干。常见的脏数据包括:

  • 缺失值:停牌、节假日、数据源缺失
  • 异常值:价格跳空、数据录入错误
  • 非交易数据:新股上市首日、ST股

我的处理原则是:宁可少一些股票,也不要让脏数据污染模型。

def clean_data(df):
    # 1. 删除全为空的行
    df = df.dropna(how='all')
    
    # 2. 填充少量缺失值(用前向填充)
    df = df.fillna(method='ffill')
    
    # 3. 删除仍有缺失的列(停牌太久)
    df = df.dropna(axis=1, thresh=len(df) * 0.8)
    
    # 4. 剔除ST股
    # 假设你有一个ST股票列表
    st_stocks = ['600519']  # 示例
    df = df.drop(columns=[col for col in df.columns if col in st_stocks], errors='ignore')
    
    return df
核心原则: 配对交易依赖两只股票之间的相对关系。如果其中一只停牌太久,另一只还在交易,那配对关系就断了。我一般要求连续交易天数不低于总天数的80%。

4.4 去极值处理

去极值,说白了就是把那些离谱的数据点拉回来。比如某只股票一天涨了20%,这明显是异常。如果不处理,它会严重扭曲协整检验的结果。

常用的方法有两种:

  1. MAD法(中位数绝对偏差):稳健性更好,不受极端值影响
  2. 百分位法:简单粗暴,直接砍掉上下1%的数据

我个人偏好MAD法。为什么?因为百分位法会砍掉真实的市场波动,而MAD法只处理那些真正离谱的值。

def mad_winsorize(series, n=5):
    """
    MAD法去极值
    n: 阈值,一般取3-5
    """
    median = series.median()
    mad = np.median(np.abs(series - median))
    upper = median + n * mad
    lower = median - n * mad
    return series.clip(lower, upper)

# 应用到所有股票
df_clean = df.apply(mad_winsorize, n=5)
注意: 去极值是对每只股票单独做,而不是对整个面板数据做。你想想看,茅台和工商银行的波动率差了几个数量级,放在一起做去极值,茅台的数据会被削成什么样?

阈值的选择也很关键。n=3 比较严格,适合稳健性要求高的场景;n=5 比较宽松,适合想保留更多市场信息的场景。我一般先用 n=5 看看效果,如果发现还有明显异常,再调低到 n=4。

4.5 知识体系总览

下面这张图,把数据准备与预处理的整个流程串起来了。你可以把它当作一个检查清单,每一步做完就打个勾。

数据准备与预处理流程 获取行情数据 日频收盘价(后复权) 行业分类对齐 申万一级行业分类 数据清洗 缺失值、异常值、ST股 去极值处理 MAD法 / 百分位法 数据对齐 时间序列对齐 最终输出 干净、对齐的日频价格矩阵 关键检查点 检查复权方式是否正确 确认行业分类版本是否最新 验证清洗后数据量是否合理

嗯,这张图把整个流程串起来了。你从左上角开始,先拿行情数据,再对齐行业分类,然后清洗、去极值,最后对齐时间序列。每一步都有坑,但只要你按这个流程走一遍,出来的数据质量绝对靠谱。

我的经验: 数据预处理这一步,我一般会花整个项目30%的时间。别觉得浪费,后面建模、回测、实盘,每一步都会感谢你当初的认真。我曾经因为偷懒没做去极值,结果协整检验的p值全是0.00,我还以为发现了大机会,后来才发现是数据异常导致的假阳性。

好了,数据准备这块就聊到这儿。记住一句话:垃圾进,垃圾出。数据干净了,后面的工作才有意义。


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