2. 金融市场数据获取:使用yfinance、pandas-datareader获取股票、ETF、期货数据。数据清洗与预处理:处理缺失值、异常值、调整股息与拆股。

做统计套利,第一步就是搞数据。这步要是没做好,后面模型再漂亮也是白搭。我见过太多人花几个月调参数,最后发现是原始数据有坑——嗯,我自己也踩过这种坑。

今天咱们就把数据获取和清洗这块彻底讲透。说白了,就是两件事:怎么把数据拿进来,以及怎么把数据弄干净

2.1 数据获取工具选型

Python里能拿金融数据的库不少,但我个人最常用的是两个:yfinancepandas-datareader。为什么?

  • yfinance:雅虎财经的接口,数据全、更新快。股票、ETF、期货都能拿。我项目中90%的场景用它就够了。
  • pandas-datareader:更像一个数据源聚合器,可以连FRED(美联储经济数据)、World Bank等。做宏观因子分析时特别好用。
我的习惯:日常回测用yfinance,需要搭配宏观经济数据时再加pandas-datareader。两个库不冲突,可以混着用。

2.2 使用yfinance获取数据

先装库:

pip install yfinance pandas-datareader

拿股票数据,代码其实就几行:

import yfinance as yf
import pandas as pd

# 获取苹果公司历史数据
aapl = yf.download('AAPL', start='2020-01-01', end='2023-12-31')
print(aapl.head())

返回的DataFrame包含:Open, High, Low, Close, Volume, Adj Close。注意这个Adj Close——调整收盘价,它已经把股息和拆股的影响算进去了。我建议你直接用这一列做回测,别自己手动调,容易出错。

拿ETF也一样:

# 获取标普500 ETF
spy = yf.download('SPY', start='2020-01-01', end='2023-12-31')

期货数据呢?yfinance也能拿,但代码符号有点讲究:

# 黄金期货(GC=F)
gold = yf.download('GC=F', start='2020-01-01', end='2023-12-31')

# 原油期货(CL=F)
oil = yf.download('CL=F', start='2020-01-01', end='2023-12-31')
注意:期货数据有时会有跳空缺口,因为合约换月。yfinance默认返回的是连续合约,但换月时的价格跳跃需要你自己处理。我后面会讲怎么处理。

2.3 使用pandas-datareader获取数据

这个库更适合拿宏观经济数据。比如从FRED拿美国10年期国债收益率:

import pandas_datareader.data as web
import datetime

start = datetime.datetime(2020, 1, 1)
end = datetime.datetime(2023, 12, 31)

# 10年期国债收益率
tnx = web.DataReader('DGS10', 'fred', start, end)
print(tnx.head())

你想想看,做统计套利时,如果只盯着股票价格,很容易忽略宏观环境的变化。我建议你把利率、通胀这些因子也拉进来,作为辅助判断。

2.4 数据清洗与预处理

数据拿到手,别急着跑模型。先看看有没有脏数据。我总结了一套标准流程:

2.4.1 处理缺失值

金融数据缺失的原因很多:节假日、停牌、数据源故障。处理方法有三种:

方法 适用场景 代码示例
向前填充 非交易日的缺失(如周末) df.fillna(method='ffill')
向后填充 数据延迟发布 df.fillna(method='bfill')
插值法 少量随机缺失 df.interpolate()
直接删除 缺失比例极小(<1%) df.dropna()

我个人习惯:先用 df.isnull().sum() 看看缺失分布。如果连续缺失超过3天,我会标记出来,不强行填充——因为这种缺失往往意味着有特殊情况。

避坑指南:我曾经在回测中用了向前填充处理连续5天的缺失,结果模型在那些"假数据"上表现异常好,实盘直接亏了。后来我学乖了:缺失超过3天,直接跳过那段时间。

2.4.2 处理异常值

异常值怎么找?我常用两种方法:

  1. 标准差法:超过均值±3倍标准差的值视为异常
  2. IQR法:低于Q1-1.5*IQR或高于Q3+1.5*IQR的值视为异常
# 标准差法示例
mean = df['Close'].mean()
std = df['Close'].std()
outliers = df[(df['Close'] > mean + 3*std) | (df['Close'] < mean - 3*std)]

# IQR法示例
Q1 = df['Close'].quantile(0.25)
Q3 = df['Close'].quantile(0.75)
IQR = Q3 - Q1
outliers_iqr = df[(df['Close'] < Q1 - 1.5*IQR) | (df['Close'] > Q3 + 1.5*IQR)]

发现异常值后怎么办?别急着删。先看看是不是真实的市场事件——比如2020年3月的暴跌,那可不是异常值,那是真实行情。我一般会结合新闻和成交量来判断。

2.4.3 调整股息与拆股

yfinance的Adj Close已经帮我们处理了股息和拆股。但如果你用的是原始价格(Close),就得自己调。

为什么必须调?举个例子:一只股票100元,分红2元,除息日开盘价会变成98元。如果你用原始价格,会看到一个"下跌",但实际你拿到了2元现金。不调整的话,回测结果会失真。

调整方法:

# 手动计算调整因子(如果yfinance没提供Adj Close)
# 假设有除息日数据
df['Adjustment Factor'] = df['Close'] / df['Close'].shift(1)
# 但说实话,直接用yfinance的Adj Close省心多了
我的建议:直接用Adj Close。yfinance的调整逻辑是业界标准,比自己手动算靠谱。我早期试过自己算,结果跟Bloomberg对不上,查了半天发现是复权方式不同。

2.5 数据清洗完整流程

我把上面这些步骤串成一个函数,每次拿数据后直接调用:

def clean_market_data(df):
    """
    市场数据清洗流水线
    """
    # 1. 只保留调整收盘价
    df = df[['Adj Close']].copy()
    
    # 2. 检查缺失值
    missing_pct = df.isnull().mean()
    if missing_pct > 0.05:
        print(f"警告:缺失比例 {missing_pct:.2%},建议检查数据源")
    
    # 3. 向前填充缺失值(最多连续3天)
    df = df.fillna(method='ffill', limit=3)
    
    # 4. 删除剩余缺失
    df = df.dropna()
    
    # 5. 异常值检测(IQR法)
    Q1 = df['Adj Close'].quantile(0.25)
    Q3 = df['Adj Close'].quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - 3*IQR  # 放宽到3倍IQR,减少误杀
    upper = Q3 + 3*IQR
    df = df[(df['Adj Close'] >= lower) & (df['Adj Close'] <= upper)]
    
    return df

这个函数我用了两年多,基本没出过问题。你拿去改改就能用。

2.6 本章知识体系

下面这张图把数据获取和清洗的流程串起来了:

数据获取与清洗流程 数据源 yfinance pandas-datareader 原始数据 数据清洗步骤 1. 缺失值处理 2. 异常值检测 3. 股息拆股调整 向前填充 / 插值 / 删除 标准差法 / IQR法 使用Adj Close 清洗后的数据

这张图把整个流程分成了三层:数据源 → 获取方式 → 清洗步骤。你每次拿到新数据,按这个流程走一遍,基本不会出大问题。

好了,数据准备这块就讲到这里。记住一句话:数据质量决定模型上限。花80%的时间在数据上,20%的时间建模,这是量化交易里最划算的投入产出比。