第二章:数据获取与清洗——用pandas搞定金融数据

做量化交易,有一句老话叫「垃圾进,垃圾出」。

你策略再牛,模型再花哨,数据一塌糊涂,结果就是白忙活。我入行那会儿,就吃过这个亏——跑回测赚得盆满钵满,实盘一上线直接亏麻。后来一查,原来是数据里混了一堆未来函数和异常值。

所以这一章,咱们就踏踏实实把数据基础打牢。说白了,就是学会用pandas把金融数据收拾得服服帖帖。

2.1 获取金融数据——从哪来,怎么拿

金融数据的来源,我常用的就这几类:

  • 本地CSV文件:最原始,也最可控。很多券商、数据商都提供CSV导出。
  • 在线API接口:比如Tushare、AKShare、Yahoo Finance。实时数据、历史数据都能拉。
  • 数据库:MySQL、InfluxDB,适合团队协作和大规模存储。

我个人习惯,刚开始做研究时先用CSV,简单直接。等策略稳定了,再迁移到数据库。

举个例子,用pandas读CSV:

import pandas as pd

# 读取本地CSV
df = pd.read_csv('stock_600519.csv', parse_dates=['date'], index_col='date')
print(df.head())

这里有个小坑——parse_dates参数一定要加。不然日期列会被当成字符串,后面做时间序列分析时各种报错。我曾经因为这个排查了整整一下午。

如果是拉在线数据,AKShare的写法也很简单:

import akshare as ak

# 获取贵州茅台日线数据
df = ak.stock_zh_a_hist(symbol="600519", period="daily", 
                         start_date="20200101", end_date="20231231")
df['日期'] = pd.to_datetime(df['日期'])
df.set_index('日期', inplace=True)
小提示:在线API有频率限制。我建议每次请求之间加个time.sleep(0.5),别把人家服务器搞崩了。

2.2 处理缺失值——别让NaN毁了你的策略

金融数据里,缺失值太常见了。停牌、节假日、数据源抽风,都会导致NaN。

你想想看,一个策略如果遇到NaN就直接报错,那还怎么跑?

处理缺失值,我一般分三步走:

  1. 先看看缺失情况df.isnull().sum() 一目了然。
  2. 判断缺失原因:是随机缺失,还是结构性缺失?
  3. 选择处理方法:删除、填充、还是插值。

代码示例:

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

# 方法一:直接删除(慎用)
df_drop = df.dropna()

# 方法二:向前填充(我最常用)
df_fill = df.fillna(method='ffill')

# 方法三:线性插值
df_interp = df.interpolate(method='linear')
注意:千万别一上来就dropna!我见过有人把停牌日的数据全删了,结果回测曲线漂亮得不像话,实盘直接翻车。停牌日的数据,用ffill填充更合理。

为什么我偏爱向前填充?因为金融数据有很强的时序相关性。今天的收盘价不知道,用昨天的代替,比凭空猜一个要靠谱得多。

2.3 处理异常值——揪出那些「离谱」的数据

异常值,就是那些明显不符合常理的数据点。比如某股票一天涨了1000%,或者成交量突然少了几个零。

嗯,这里要注意:异常值不一定是错误,也可能是真实事件(比如借壳上市)。所以处理前一定要先确认。

我常用的检测方法:

方法 适用场景 代码示例
Z-score法 数据近似正态分布 (df - df.mean()) / df.std()
IQR四分位法 数据分布未知,有离群点 Q3 + 1.5*IQR
固定阈值法 有业务常识(如涨跌幅限制) df[df > 0.1] = np.nan

实战中,我一般先用IQR法做个初筛,再结合业务逻辑人工复核。举个例子:

# IQR法检测异常值
Q1 = df['close'].quantile(0.25)
Q3 = df['close'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 标记异常值
df['is_outlier'] = (df['close'] < lower_bound) | (df['close'] > upper_bound)

# 处理:替换为NaN,然后填充
df.loc[df['is_outlier'], 'close'] = np.nan
df['close'].fillna(method='ffill', inplace=True)
核心原则:异常值处理要「保守」。宁可漏掉一个,也别误杀一个。误杀会导致数据失真,比异常值本身更可怕。

2.4 数据对齐——让不同频率的数据「对上话」

做量化交易,你经常需要把不同来源、不同频率的数据合并到一起。比如日线数据和5分钟数据,或者股票数据和宏观经济数据。

数据对齐,说白了就是让它们的时间索引保持一致。

pandas里最常用的就是mergejoin

# 两个不同频率的DataFrame
df_daily = pd.DataFrame({'close': [100, 102, 101]}, 
                         index=pd.date_range('2023-01-01', periods=3, freq='D'))
df_minute = pd.DataFrame({'volume': [1000, 1500, 1200]}, 
                          index=pd.date_range('2023-01-01 09:30', periods=3, freq='5T'))

# 对齐到日线频率
df_aligned = df_minute.resample('D').sum().join(df_daily, how='outer')
print(df_aligned)

这里有个细节:how='outer'会保留所有时间点,缺失的填NaN。how='inner'只保留交集。我建议先用outer看看数据覆盖情况,再决定怎么处理。

避坑指南:我曾经在合并两个数据源时,发现日期对不上。查了半天,原来是一个用的UTC时间,一个用的北京时间。差8个小时,数据全乱了。所以对齐前,一定先检查时区!

2.5 重采样——把数据「变」成你想要的频率

重采样,就是把数据从一种频率转换成另一种。比如把1分钟数据合成日线,或者把日线数据降采样到周线。

说白了,就是数据频率的「翻译官」。

pandas的resample方法,我几乎天天用:

# 1分钟数据 -> 日线数据
df_daily = df_1min.resample('D').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'volume': 'sum'
})

# 日线数据 -> 周线数据
df_weekly = df_daily.resample('W').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'volume': 'sum'
})

常用的重采样规则:

  • 'D':日线
  • 'W':周线(默认周日结束)
  • 'M':月线
  • 'Q':季线
  • '5T':5分钟线
  • 'H':小时线
注意:重采样时,聚合函数的选择很关键。开盘价用first,收盘价用last,最高最低用max/min,成交量用sum。别搞混了,不然数据就废了。

我记得有一次,一个同事把周线的开盘价写成了mean,结果回测出来的策略收益高得离谱。一查才发现,他把一周五天的开盘价平均了,相当于用了未来数据。嗯,这种错误,犯一次就长记性了。

小结

这一章的内容,说白了就是数据清洗的「三板斧」:

  • 获取数据:CSV、API、数据库,选你顺手的
  • 处理缺失值:ffill优先,dropna慎用
  • 处理异常值:IQR法+业务复核,保守为上
  • 数据对齐:检查时区,选对合并方式
  • 重采样:选对聚合函数,别用未来数据

下一章,咱们聊聊特征工程——怎么从原始数据里挖出有用的信号。到时候你会发现,数据清洗做得越扎实,后面的工作就越轻松。

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