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
4.4 去极值处理
去极值,说白了就是把那些离谱的数据点拉回来。比如某只股票一天涨了20%,这明显是异常。如果不处理,它会严重扭曲协整检验的结果。
常用的方法有两种:
- MAD法(中位数绝对偏差):稳健性更好,不受极端值影响
- 百分位法:简单粗暴,直接砍掉上下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 知识体系总览
下面这张图,把数据准备与预处理的整个流程串起来了。你可以把它当作一个检查清单,每一步做完就打个勾。
嗯,这张图把整个流程串起来了。你从左上角开始,先拿行情数据,再对齐行业分类,然后清洗、去极值,最后对齐时间序列。每一步都有坑,但只要你按这个流程走一遍,出来的数据质量绝对靠谱。
好了,数据准备这块就聊到这儿。记住一句话:垃圾进,垃圾出。数据干净了,后面的工作才有意义。
公众号:蓝海资料掘金营,微信deep3321