金融数据获取与清洗:从原始数据到可用特征

做量化投资,第一步就是搞数据。没有数据,再牛的模型也是空中楼阁。我个人习惯把数据获取和清洗看作一个整体流程——拿到原始数据只是开始,真正花时间的往往是后面的清洗工作。这一章我们就来聊聊怎么用 Python 拿到 A 股和美股的数据,然后把它收拾得干干净净,喂给模型吃。

核心要点:数据质量直接决定模型效果。脏数据进,垃圾模型出。这个道理我在项目里吃过不少亏才真正理解。

4.1 数据源选择:Tushare、Baostock 与 Yahoo Finance

国内做量化,常用的数据源就那么几个。我最早用的是 Tushare,后来发现 Baostock 在某些场景下更省事。美股嘛,Yahoo Finance 基本是标配。

数据源 覆盖市场 是否需要注册 免费额度 我的评价
Tushare Pro A股、期货、基金 是(需Token) 积分制,基础够用 数据全,但接口有时限流
Baostock A股、指数 否 完全免费 简单粗暴,适合入门
Yahoo Finance 全球股票、ETF 否 免费 美股首选,但偶尔抽风

我的建议:如果你做 A 股策略,优先用 Baostock 做快速原型,上线前再用 Tushare 做数据校验。美股直接上 yfinance 库,省心。

4.2 获取 A 股数据:Tushare 实战

先看 Tushare。你需要先去官网注册拿到 token,然后安装 tushare 库。我个人习惯把 token 写在环境变量里,而不是硬编码在代码中——你想想看,万一代码传到 GitHub 上,token 就暴露了。

import tushare as ts
import pandas as pd

# 初始化(token 从环境变量读取)
ts.set_token('你的token')
pro = ts.pro_api()

# 获取平安银行日线数据
df = pro.daily(
    ts_code='000001.SZ',
    start_date='20230101',
    end_date='20231231',
    fields='ts_code,trade_date,open,high,low,close,vol'
)

print(df.head())

嗯,这里要注意。Tushare 的 daily 接口默认返回的是最近一年的数据,而且有频率限制。我曾经在写回测框架时,一次性拉了 3000 只股票的数据,结果被限流了半小时。所以批量获取时,建议加个 time.sleep。

再看 Baostock。它不需要注册,直接 pip install baostock 就能用。接口风格和 Tushare 不太一样,但数据质量不错。

import baostock as bs
import pandas as pd

# 登录
lg = bs.login()
# 获取数据
rs = bs.query_history_k_data_plus(
    "sh.600000",
    "date,open,high,low,close,volume",
    start_date='2023-01-01',
    end_date='2023-12-31',
    frequency='d',
    adjustflag='3'  # 复权类型:3 表示不复权
)
# 转为 DataFrame
df = rs.get_data()
print(df.head())
bs.logout()

避坑指南:Baostock 的股票代码格式是 sh.600000 或 sz.000001,和 Tushare 的 000001.SZ 不一样。另外,记得每次用完要调用 bs.logout(),否则连接会泄漏。我曾经因为这个被运维同事找上门...

4.3 获取美股数据:Yahoo Finance 实战

美股数据我用 yfinance 库。它封装了 Yahoo Finance 的接口,用起来非常顺手。安装方式:pip install yfinance。

import yfinance as yf

# 下载苹果公司数据
aapl = yf.download(
    'AAPL',
    start='2023-01-01',
    end='2023-12-31',
    auto_adjust=True  # 自动调整价格(拆股、分红)
)

print(aapl.head())

这里有个细节:auto_adjust=True 会自动调整历史价格,把拆股和分红的影响算进去。我个人建议一直开着这个参数,否则你拿到的价格和实际交易价格会有偏差。

如果想同时获取多只股票,可以传一个列表:

tickers = ['AAPL', 'MSFT', 'GOOGL']
data = yf.download(tickers, start='2023-01-01', end='2023-12-31')
# 返回的是 MultiIndex DataFrame
print(data['Close'].head())  # 只看收盘价

小技巧:yfinance 偶尔会因为网络问题超时。我习惯加个重试机制,比如用 tenacity 库做自动重试。另外,美股数据有盘前盘后价格,如果你做日内策略,可以设置 interval='1m' 获取分钟数据。

4.4 数据清洗:处理缺失值与异常值

数据拿到手,第一件事就是检查质量。我见过太多人直接拿原始数据跑模型,结果回测曲线漂亮得不行,实盘一塌糊涂——说白了,就是数据里有坑。

4.4.1 缺失值处理

金融数据常见的缺失原因:停牌、节假日、数据源漏传。先看看缺失情况:

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

# 查看缺失比例
missing_ratio = df.isnull().mean()
print(missing_ratio[missing_ratio > 0])

处理方式取决于缺失比例和业务场景:

  • 缺失 < 5%:用前向填充(ffill)或插值。我个人偏好 ffill,因为金融数据通常是延续的。
  • 缺失 5%-20%:可以考虑用线性插值,或者用同类股票的均值填充。
  • 缺失 > 20%:直接删掉这只股票或这个时间段。别硬填,会引入噪声。
# 前向填充
df_filled = df.fillna(method='ffill')

# 线性插值
df_interpolated = df.interpolate(method='linear')

# 删除缺失过多的列
df_cleaned = df.dropna(thresh=len(df) * 0.8, axis=1)

注意:千万别用均值填充金融时间序列。你想想看,某天股票停牌,你用过去30天的均价去填,这完全扭曲了价格走势。我曾经见过一个策略,因为用均值填充停牌数据,回测年化收益虚高了 15%。

4.4.2 异常值处理

异常值在金融数据里很常见,比如涨跌停、数据录入错误、极端行情。我一般用两种方法检测:

  • Z-score 方法:计算每个值偏离均值的标准差倍数,超过 3 倍视为异常。
  • IQR 方法:用四分位距,超出 [Q1-1.5*IQR, Q3+1.5*IQR] 的视为异常。
from scipy import stats

# Z-score 方法
z_scores = stats.zscore(df['close'].dropna())
abs_z = np.abs(z_scores)
outliers_z = df[abs_z > 3]

# 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)]

处理异常值,我的原则是:先理解原因,再决定处理方式。如果是涨跌停导致的,保留;如果是数据错误,直接删掉或用前后值替换。

4.5 数据对齐与重采样

做多资产策略时,不同股票的交易日期可能不一样。A 股有节假日,美股也有自己的假期。这时候就需要对齐数据。

4.5.1 数据对齐

对齐的核心思想:以某个时间轴为基准,把其他数据映射过来。我通常用 pandas 的 merge 或 reindex。

# 假设我们有两只股票的数据
df_000001 = pro.daily(ts_code='000001.SZ', start_date='20230101', end_date='20231231')
df_600000 = pro.daily(ts_code='600000.SH', start_date='20230101', end_date='20231231')

# 以 trade_date 为键做外连接
aligned = pd.merge(
    df_000001[['trade_date', 'close']].rename(columns={'close': '000001_close'}),
    df_600000[['trade_date', 'close']].rename(columns={'close': '600000_close'}),
    on='trade_date',
    how='outer'
).sort_values('trade_date')

# 填充缺失值(非交易日)
aligned = aligned.fillna(method='ffill').dropna()

经验之谈:对齐时用 outer join 保留所有日期,然后用 ffill 填充。这样能保证时间序列的连续性。如果你用 inner join,可能会丢掉一些交易日,导致回测结果偏差。

4.5.2 重采样

重采样就是把高频数据转为低频数据,或者反过来。比如把日线数据转为周线、月线。这在做长周期策略时很常用。

# 设置日期为索引
df = df.set_index('trade_date')
df.index = pd.to_datetime(df.index)

# 日线转周线
weekly = df.resample('W').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'vol': 'sum'
})

# 日线转月线
monthly = df.resample('M').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'vol': 'sum'
})

print(monthly.head())

重采样时,不同字段的聚合方式不一样:开盘价取第一个,收盘价取最后一个,最高价取最大值,成交量取总和。这个逻辑别搞混了。

4.6 本章知识体系

下面这张图总结了数据获取与清洗的完整流程,从数据源到最终可用的特征矩阵:

金融数据获取与清洗流程 金融数据获取与清洗流程 数据源 数据获取 原始数据 缺失值处理 异常值检测 数据对齐 重采样(日/周/月) 可用特征矩阵 💡 关键原则: 先理解数据,再动手清洗。缺失值不要盲目填充,异常值要区分涨跌停和错误数据。

从数据源到最终的特征矩阵,每一步都有坑。我刚开始做量化时,觉得数据清洗是最无聊的部分,总想跳过。后来发现,花在清洗上的每一分钟,都会在模型效果上得到回报。说白了,数据干净了,模型自然就靠谱了。