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')
⚠️ 注意: 千万不要直接 dropna() 删除所有缺失行。我曾经这么干过,结果某只股票因为停牌一周,直接少了5行数据,导致后续的滚动窗口计算全部错位。数据对齐出了问题,整个策略都废了。

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 就能搞定。

核心要点: 面板数据是轮动模型的「发动机」。数据质量决定了策略的上限。我见过太多人花90%的时间写策略,只花10%的时间处理数据。结果呢?策略越写越复杂,收益却越来越差。我的建议是——花60%的时间在数据上,30%在回测,10%在策略。数据干净了,策略自然就出来了。
数据获取与预处理流程 ① 数据获取 akshare 获取A股行情 ② 数据清洗 缺失值 + 异常值处理 ③ 对齐与重采样 共同交易日 + 周/月线 ④ 面板数据结构 时间×股票×特征 关键操作 • stock_zh_a_hist() • 前复权/后复权 • 多股票批量获取 关键操作 • ffill / bfill • IQR异常检测 • 标记 vs 删除 关键操作 • 共同日期交集 • resample('W-FRI') • 对齐后合并 关键操作 • 长表格式 • 多级索引 • groupby 准备 最终产出:干净、对齐、标准化的面板数据

数据预处理这块,说难不难,说简单也不简单。核心就是细心。我每次拿到新数据,都会先画个分布图、看看缺失率、检查一下日期连续性。这些习惯,帮我避免了很多「回测一时爽,实盘火葬场」的尴尬。

好了,数据已经准备好了。接下来就可以用它来构建我们的轮动模型了。

专注资料整理