金融数据获取与清洗:从原始数据到可用特征
做量化投资,第一步就是搞数据。没有数据,再牛的模型也是空中楼阁。我个人习惯把数据获取和清洗看作一个整体流程——拿到原始数据只是开始,真正花时间的往往是后面的清洗工作。这一章我们就来聊聊怎么用 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 本章知识体系
下面这张图总结了数据获取与清洗的完整流程,从数据源到最终可用的特征矩阵:
从数据源到最终的特征矩阵,每一步都有坑。我刚开始做量化时,觉得数据清洗是最无聊的部分,总想跳过。后来发现,花在清洗上的每一分钟,都会在模型效果上得到回报。说白了,数据干净了,模型自然就靠谱了。