4. 数据获取与处理:用pandas搞定金融数据
做量化回测,数据就是你的弹药。弹药不行,枪法再好也白搭。
我见过太多人,策略逻辑写得漂漂亮亮,一跑回测全是坑。为什么?数据没处理好。缺失值、时间错位、频率不一致……这些问题不解决,回测结果就是垃圾。
这一章,咱们就聊聊怎么用pandas把金融数据收拾得服服帖帖。
核心要点:数据获取 → 清洗 → 对齐 → 重采样,每一步都决定回测的生死。
4.1 数据获取:从哪里来?
金融数据的来源很多。我个人习惯用本地CSV文件,或者直接从数据库拉。为啥?稳定。你想想看,回测跑几个小时,中间网络断了,数据没拉全,那得多崩溃。
先看一个最简单的例子,从CSV读数据:
import pandas as pd
df = pd.read_csv('stock_data.csv',
parse_dates=['date'],
index_col='date')
print(df.head())
嗯,这里要注意。很多新手直接读CSV,日期列还是字符串。你后面做时间序列分析,肯定要转成datetime类型。所以我习惯在read_csv里直接指定parse_dates参数,一步到位。
如果是多只股票的数据,我建议用字典或者分层索引来组织:
# 多只股票数据组织
data = {
'000001.SZ': pd.read_csv('000001.csv', index_col='date', parse_dates=True),
'600001.SH': pd.read_csv('600001.csv', index_col='date', parse_dates=True)
}
# 或者用Panel(现在推荐用MultiIndex)
df_multi = pd.concat(data, names=['stock', 'date'])
我在项目中遇到过一个问题:不同数据源的时间戳格式不一样。有的用YYYY-MM-DD,有的用YYYY/MM/DD,还有的带时区。这时候统一用pd.to_datetime()处理一下,省心。
4.2 数据清洗:脏数据是回测的毒药
数据拿到手,第一件事不是分析,是清洗。你想想看,金融数据里什么妖魔鬼怪都有:
- 停牌期间的数据是空的
- 除权除息导致价格跳变
- 交易异常导致的价格尖峰
- 数据源自己搞错了
我一般按这个顺序清洗:
- 检查重复行:同一个时间点出现两条数据,肯定有问题
- 检查异常值:价格突然涨了100倍,多半是数据错误
- 处理缺失值:这个后面单独讲
- 检查时间连续性:交易日之间有没有跳空
代码示例:
# 1. 删除重复行
df = df[~df.index.duplicated(keep='first')]
# 2. 检测异常值(比如价格超过均值3个标准差)
mean = df['close'].mean()
std = df['close'].std()
df = df[(df['close'] > mean - 3*std) &
(df['close'] < mean + 3*std)]
# 3. 检查时间连续性
date_range = pd.date_range(start=df.index.min(),
end=df.index.max(),
freq='B') # 仅交易日
missing_dates = date_range.difference(df.index)
if len(missing_dates) > 0:
print(f"缺失交易日: {missing_dates[:5]}")
警告:千万不要直接删除异常值就完事了。我曾经在回测中删掉了一个"异常"的涨停数据,结果策略在真实交易中遇到涨停时完全失效。先搞清楚异常的原因,再决定怎么处理。
4.3 缺失值处理:填还是不填?
金融数据里缺失值太常见了。停牌、节假日、数据源延迟……都会导致缺失。
处理缺失值,我总结了三种策略:
| 策略 | 适用场景 | 代码 |
|---|---|---|
| 向前填充 | 停牌期间,用最后交易价格 | df.fillna(method='ffill') |
| 向后填充 | 数据延迟,用下一个交易价格 | df.fillna(method='bfill') |
| 插值法 | 日内数据,用线性插值 | df.interpolate() |
| 删除 | 缺失比例太高,直接放弃 | df.dropna() |
我个人最常用的是向前填充。为什么?因为停牌期间,你确实无法交易,用最后的价格是合理的。但要注意:如果停牌时间太长(比如超过10个交易日),向前填充的意义就不大了。
# 实际项目中的处理逻辑
def handle_missing(df, max_ffill_days=5):
"""处理缺失值,最多向前填充5天"""
# 先向前填充
df_filled = df.fillna(method='ffill', limit=max_ffill_days)
# 超过限制的,标记为NaN
mask = df.isna().cumsum() > max_ffill_days
df_filled[mask] = np.nan
# 最后删除剩余NaN
return df_filled.dropna()
小技巧:处理缺失值时,我习惯先看看缺失的分布。用df.isna().sum()看看每列缺失了多少,用df.isna().sum(axis=1).value_counts()看看哪些时间点缺失最多。这样心里有数。
4.4 数据对齐:让多只股票步调一致
做多因子模型或者配对交易时,你需要多只股票的数据在同一个时间点上对齐。但问题是,A股不同股票的交易日期不完全一样——有的停牌了,有的刚上市。
pandas的align方法就是干这个的:
# 假设有两只股票的数据
stock_a = data['000001.SZ']['close']
stock_b = data['600001.SH']['close']
# 对齐到共同的交易日
aligned_a, aligned_b = stock_a.align(stock_b, join='inner')
print(f"对齐前: {len(stock_a)} 天, {len(stock_b)} 天")
print(f"对齐后: {len(aligned_a)} 天")
join='inner'取交集,join='outer'取并集。我一般用inner,因为回测时你只能在所有股票都交易的日子进行买卖。用outer的话,会出现大量缺失值,处理起来麻烦。
但这里有个坑:如果你用inner,可能会丢掉很多数据。比如某只股票停牌半年,那这半年的数据全没了。我曾经在回测一个沪深300策略时,因为一只股票停牌太久,导致回测期缩短了三分之一。
我的建议是:先看看停牌股票的数量。如果只是少数几只,可以暂时剔除。如果很多,那就用outer,然后填充缺失值。
4.5 重采样:把数据变成你想要的频率
不同策略需要不同频率的数据。日频策略用日线,高频策略用分钟线。但数据源可能只提供日线,你需要自己转成周线或月线。
pandas的resample方法,说白了就是按时间窗口聚合数据:
# 日线转周线
weekly = df['close'].resample('W').last() # 取每周最后一个交易日
# 日线转月线
monthly = df['close'].resample('M').last()
# 分钟线转日线
daily = df['close'].resample('D').ohlc() # 生成OHLC数据
常用的重采样规则:
| 规则 | 说明 | 示例 |
|---|---|---|
| D | 日历日 | 每天一个数据点 |
| B | 交易日 | 跳过周末和节假日 |
| W | 每周 | 默认周日结束 |
| M | 每月 | 月末最后一天 |
| Q | 每季度 | 季度末 |
重采样时,聚合函数的选择很重要。比如转周线,用last还是mean?我习惯用last,因为周线策略通常是在周五收盘时交易,用最后的价格更合理。但如果你做的是周均线策略,用mean可能更合适。
# 一个完整的重采样示例
def resample_ohlc(df, freq='W'):
"""将日线数据重采样为指定频率的OHLC数据"""
resampled = pd.DataFrame({
'open': df['open'].resample(freq).first(),
'high': df['high'].resample(freq).max(),
'low': df['low'].resample(freq).min(),
'close': df['close'].resample(freq).last(),
'volume': df['volume'].resample(freq).sum()
})
return resampled.dropna()
注意:重采样时,volume(成交量)要用sum,不能用last或mean。这个错误我犯过,回测结果看起来不错,但实际交易时发现成交量对不上,策略完全失效。
4.6 知识体系总览
说了这么多,咱们用一张图把数据获取与处理的流程串起来:
这张图把整个流程串起来了。从数据获取开始,经过清洗、缺失值处理、对齐,最后重采样成你需要的频率。每一步都环环相扣,少一步都不行。
好了,数据获取与处理这块就聊到这儿。记住一句话:数据质量决定回测质量。花80%的时间在数据处理上,剩下20%写策略,这才是量化交易的正确姿势。
公众号:蓝海资料掘金营,微信deep3321