第三章:数据获取与清洗——套利策略的基石

做套利策略,我最深的体会是什么?

数据决定生死。

模型再漂亮,逻辑再严密,数据一塌糊涂,结果就是垃圾进垃圾出。我见过太多人花90%时间调参数,却不愿意花10%时间处理数据。嗯,这其实是个大坑。

这一章,我们就来聊聊怎么拿到靠谱的行情数据,怎么对齐,怎么处理那些烦人的缺失值和异常值。

3.1 数据源的选择:tushare vs akshare

国内做量化,绕不开两个库:tushare和akshare。我个人习惯两个都备着,因为各有各的脾气。

特性 tushare akshare
数据质量 较高,经过校验 中等,部分接口不稳定
免费额度 有限,需积分 基本免费
数据范围 A股、期货、基金等 A股、期货、外汇、加密货币等
接口稳定性 稳定,有官方维护 偶尔变动,社区维护
适合场景 正式回测、实盘 快速验证、多品种
我的建议:回测用tushare,数据干净。实盘监控用akshare,品种全、更新快。但akshare的接口偶尔会改,记得加异常捕获。

3.2 获取行情数据:代码实战

先看tushare怎么拿日线数据。记得先注册获取token。

import tushare as ts
import pandas as pd

# 初始化
pro = ts.pro_api('你的token')

# 获取平安银行日线数据
df = pro.daily(
    ts_code='000001.SZ',
    start_date='20230101',
    end_date='20231231'
)

# 按日期排序
df = df.sort_values('trade_date')
print(df.head())

akshare的写法更直接,不需要token:

import akshare as ak

# 获取沪深300指数日线
df = ak.stock_zh_index_daily(symbol="sh000300")
print(df.head())
注意:akshare返回的列名和tushare不一样。比如日期列,tushare叫'trade_date',akshare叫'date'。我曾经因为没注意这个,回测结果差了十万八千里。

3.3 数据对齐:套利的核心难点

做套利,最怕什么?

时间戳对不上。

比如你要做沪深300和上证50的配对交易。两个指数的交易时间完全一样,但数据源可能一个返回的是字符串,另一个返回的是datetime对象。或者一个包含非交易日,另一个没有。

我的做法是:统一索引,强制对齐。

# 假设df1是沪深300,df2是上证50
# 统一日期格式
df1['date'] = pd.to_datetime(df1['trade_date'])
df2['date'] = pd.to_datetime(df2['date'])

# 设置为索引
df1.set_index('date', inplace=True)
df2.set_index('date', inplace=True)

# 取交集,只保留共同交易日
common_dates = df1.index.intersection(df2.index)
df1_aligned = df1.loc[common_dates].sort_index()
df2_aligned = df2.loc[common_dates].sort_index()

# 检查是否完全对齐
print((df1_aligned.index == df2_aligned.index).all())
核心原则:对齐后的两个DataFrame,索引必须完全一致。差一个交易日都不行。我一般会加个断言,确保万无一失。

3.4 缺失值处理:别急着删

数据拿到手,第一件事就是检查缺失值。

# 检查缺失值
print(df.isnull().sum())

# 看看缺失比例
missing_ratio = df.isnull().sum() / len(df)
print(missing_ratio)

缺失值怎么处理?分情况:

  • 缺失比例 < 1%:直接填充。用前向填充(ffill)或插值。
  • 缺失比例 1%-5%:看原因。如果是停牌,用复牌后的价格填充。如果是数据源问题,重新拉取。
  • 缺失比例 > 5%:建议放弃这个品种。数据质量太差,套利信号会失真。
# 前向填充(用上一个交易日的数据)
df.fillna(method='ffill', inplace=True)

# 或者线性插值
df.interpolate(method='linear', inplace=True)
避坑指南:我曾经用ffill填充了连续3天的缺失值,结果那3天正好是重大利好后的连续涨停。回测显示收益爆炸,实盘一跑就亏。后来我加了个规则:连续缺失超过2天,直接标记为无效数据。

3.5 异常值检测:别让极端值骗了你

异常值在套利里特别要命。一个极端价格,可能让你误以为出现了套利机会。

常用的检测方法:

  1. 3σ原则:超出均值±3倍标准差的值,视为异常。
  2. IQR方法:超出Q1-1.5IQR或Q3+1.5IQR的值,视为异常。
  3. 百分比法:涨跌幅超过某个阈值(比如10%),视为异常。
# IQR方法检测异常值
def detect_outliers_iqr(series):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    return (series < lower_bound) | (series > upper_bound)

# 应用到收盘价
outliers = detect_outliers_iqr(df['close'])
print(f"检测到 {outliers.sum()} 个异常值")
注意:异常值不一定是错误。比如2015年股灾期间,很多股票单日跌停,这不算异常,是市场行为。我一般会结合涨跌幅限制来判断:超过10%且不是涨跌停的,才标记为异常。

3.6 完整的数据清洗流程

最后,给一个我常用的清洗流水线:

def clean_data(df):
    # 1. 排序
    df = df.sort_index()
    
    # 2. 去重
    df = df[~df.index.duplicated(keep='first')]
    
    # 3. 缺失值处理
    df = df.fillna(method='ffill', limit=2)
    df = df.dropna()
    
    # 4. 异常值处理
    for col in ['open', 'high', 'low', 'close']:
        outliers = detect_outliers_iqr(df[col])
        # 用中位数替换异常值
        df.loc[outliers, col] = df[col].median()
    
    # 5. 检查数据连续性
    date_range = pd.date_range(start=df.index[0], end=df.index[-1], freq='B')
    missing_dates = date_range.difference(df.index)
    if len(missing_dates) > 0:
        print(f"警告:缺失 {len(missing_dates)} 个交易日")
    
    return df

嗯,这套流程我用了好几年,基本没出过大问题。但记住:没有万能的数据清洗方案。每个品种、每个策略,都可能需要微调。

数据准备好了,下一章我们就开始构建真正的套利策略。到时候你会发现,数据清洗花的时间,绝对值得。

本章要点:
  • tushare适合正式回测,akshare适合快速验证
  • 数据对齐必须强制索引一致
  • 缺失值处理要分比例,别一刀切
  • 异常值检测要结合市场背景,别机械套用统计方法

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