3. 数据获取与预处理:从零搭建你的量化数据管道
做量化投资,数据就是你的「原材料」。
我见过太多人,策略逻辑写得天花乱坠,结果数据一塌糊涂。回测跑得飞起,实盘直接翻车。说白了,数据预处理才是整个轮动模型的基石。今天我们就来聊聊,怎么用 akshare 把A股数据拿到手,再把它洗得干干净净。
3.1 用 akshare 获取A股数据
akshare 这个库,我个人非常喜欢。它把国内各种金融数据源整合到了一起,用起来特别顺手。你不需要去爬网页、找接口,一行代码就能拿到数据。
先安装一下:
pip install akshare --upgrade
然后,获取个股的历史行情。比如拿「贵州茅台」练练手:
import akshare as ak
# 获取日K线数据
df = ak.stock_zh_a_hist(symbol="600519",
period="daily",
start_date="20200101",
end_date="20231231",
adjust="qfq") # 前复权
print(df.head())
这里有个小细节——复权。我刚开始做量化时,直接拿原始价格跑策略,结果回测收益虚高。为什么?因为分红送股会导致价格跳空。你想想看,股价突然从100变成90,你的策略以为跌了10%,其实是分红。所以,一定要用前复权或后复权。我个人习惯用前复权,因为它能真实反映历史收益。
3.2 数据清洗:处理缺失值与异常值
数据拿到手,别急着用。先看看有没有「坑」。
我遇到过最离谱的一次,某只股票某天成交量突然变成0,但价格没变。一看,是数据源把停牌日的数据漏填了。这种数据如果不处理,你的轮动模型可能会在停牌日「强行交易」,直接导致回测失真。
3.2.1 缺失值处理
常见的缺失值处理方式有三种:
| 方法 | 适用场景 | 代码示例 |
|---|---|---|
| 向前填充 | 停牌日、非交易日 | df.fillna(method='ffill') |
| 向后填充 | 数据延迟发布 | df.fillna(method='bfill') |
| 插值法 | 少量缺失、趋势明显 | df.interpolate() |
我个人最常用的是向前填充。因为A股有涨跌停和停牌机制,停牌期间价格不变,用前一天的收盘价填充最合理。
# 处理缺失值:向前填充
df = df.fillna(method='ffill')
# 如果开头还有缺失,再向后填充
df = df.fillna(method='bfill')
3.2.2 异常值检测
异常值怎么找?我一般用两种方法:
- 3σ原则:超出均值±3倍标准差的数据,视为异常
- IQR方法:超出Q1-1.5*IQR 或 Q3+1.5*IQR 的数据,视为异常
对于股票数据,我推荐用IQR方法。因为收益率分布往往有厚尾特征,3σ原则会误删太多正常数据。
# 用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)
嗯,这里要注意:不要直接删除异常值。我建议先标记出来,看看是不是数据源错误。如果是,用前后均值替换;如果是真实的市场异动(比如乌龙指),可以考虑保留,但要在策略中做特殊处理。
3.3 数据对齐与重采样
做轮动模型,你手里肯定不止一只股票。不同股票的交易日期可能不一样——有的停牌了,有的刚上市。这时候就需要数据对齐。
我习惯的做法是:先找出所有股票的共同交易日,然后只保留这些日期。
# 假设我们有多个股票的DataFrame,存在字典中
stock_dfs = {'600519': df1, '000858': df2, '600036': df3}
# 获取所有日期索引
all_dates = [df.index for df in stock_dfs.values()]
common_dates = set(all_dates[0])
for dates in all_dates[1:]:
common_dates = common_dates.intersection(set(dates))
common_dates = sorted(common_dates)
# 对齐数据
aligned_dfs = {name: df.loc[common_dates] for name, df in stock_dfs.items()}
至于重采样,如果你做的是周频或月频轮动,就需要把日线数据转成周线或月线。
# 日线转周线:取每周最后一个交易日的数据
weekly_df = df.resample('W').last()
# 日线转月线
monthly_df = df.resample('M').last()
resample 默认按日历周/月对齐。但A股有节假日,会导致某些周只有3个交易日。我建议用 df.resample('W-FRI').last(),以周五为周的最后一天,这样更符合A股的实际交易节奏。
3.4 构建面板数据结构
好了,数据都洗干净了。接下来要把它变成面板数据——也就是我们常说的「三维数据」:时间 × 股票 × 特征。
我一般用两种结构:
- 宽表格式:每一行是一个日期,每一列是一只股票的某个特征(比如收盘价)
- 长表格式:每一行是一个日期+一只股票,包含所有特征
对于轮动模型,我推荐用长表格式。因为后续计算因子、排序、选股时,长表用 groupby 操作特别方便。
import pandas as pd
# 假设我们有多个股票的DataFrame,每个包含 date, close, volume
# 先合并成一个长表
all_data = []
for stock_name, df in aligned_dfs.items():
df['stock'] = stock_name
all_data.append(df)
panel_df = pd.concat(all_data, ignore_index=True)
# 设置多级索引
panel_df = panel_df.set_index(['date', 'stock'])
print(panel_df.head())
最终的面板数据长这样:
| date | stock | close | volume | ... |
|---|---|---|---|---|
| 2023-01-03 | 600519 | 1780.0 | 3200000 | ... |
| 2023-01-03 | 000858 | 135.5 | 1800000 | ... |
| 2023-01-04 | 600519 | 1795.0 | 2800000 | ... |
有了这个结构,后续计算动量因子、波动率、相关性,都只需要一行 groupby 就能搞定。
数据预处理这块,说难不难,说简单也不简单。核心就是细心。我每次拿到新数据,都会先画个分布图、看看缺失率、检查一下日期连续性。这些习惯,帮我避免了很多「回测一时爽,实盘火葬场」的尴尬。
好了,数据已经准备好了。接下来就可以用它来构建我们的轮动模型了。