第二章:金融市场数据基础——股票数据获取与预处理
做量化交易,数据就是你的弹药库。弹药不行,再好的策略也白搭。
这一章,咱们就聊聊怎么搞到靠谱的股票数据,以及拿到手之后怎么处理。我见过太多人,模型跑得飞起,结果一查,数据里全是坑。嗯,咱们别走那条弯路。
2.1 数据源的选择:Tushare vs AKShare
国内做量化,绕不开两个库:Tushare 和 AKShare。我个人习惯两个都装,因为各有各的脾气。
| 特性 | Tushare | AKShare |
|---|---|---|
| 数据质量 | 高,经过校验 | 中等,部分来自公开接口 |
| 获取方式 | 需要 token,有积分限制 | 免费,无需注册 |
| 数据范围 | A股、港股、期货、基金等 | A股为主,部分期货 |
| 速度 | 较快(付费更快) | 较慢,受限于源网站 |
| 适合场景 | 正式策略、回测 | 快速验证、学习 |
说白了,Tushare 是正规军,AKShare 是游击队。做图神经网络选股,我建议你至少用 Tushare 的 pro 版。为什么?因为 GNN 对数据质量极其敏感,一个缺失值可能让整个图结构都歪掉。
我的经验: 有一次我用 AKShare 拉数据做回测,结果发现某只股票连续停牌三天,AKShare 直接返回了 NaN。Tushare 会给你标记停牌状态,还附带复牌日期。这个细节,在构建时间序列图时太重要了。
2.2 数据获取实战
先看 Tushare 怎么用。你需要在官网注册,拿到 token。
import tushare as ts
# 设置 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())
AKShare 更直接,不用注册:
import akshare as ak
# 获取日线数据
df = ak.stock_zh_a_hist(
symbol="000001",
period="daily",
start_date="20230101",
end_date="20231231",
adjust="qfq" # 前复权
)
print(df.head())
小技巧: 我建议你写个函数,把两个库封装一下。Tushare 挂了用 AKShare 兜底,反之亦然。别问我为什么知道要这么做……
2.3 数据清洗与预处理
数据拿到手,别急着用。先看看有没有脏数据。我一般按这个流程走:
- 检查列名——统一成英文小写,方便后面处理
- 检查数据类型——日期是不是 datetime,价格是不是 float
- 检查缺失值——这个后面单独讲
- 检查异常值——比如某天涨幅超过 100%,那肯定是数据错了
# 数据清洗示例
def clean_stock_data(df):
# 统一列名
df.columns = [col.lower() for col in df.columns]
# 日期转换
df['trade_date'] = pd.to_datetime(df['trade_date'])
# 排序
df = df.sort_values('trade_date')
# 去除停牌日(成交量为0)
df = df[df['vol'] > 0]
# 去除异常值
df = df[df['close'] > 0]
return df
注意: 千万别直接 dropna() 完事。我曾经这么干过,结果发现某只股票因为停牌被删了半个月数据,导致图结构里这只股票和其他股票的边全断了。GNN 模型直接崩了。
2.4 时间序列对齐
做图神经网络,你需要把多只股票的数据对齐到同一个时间轴上。说白了,就是保证所有股票在同一天都有数据。
为什么会这样?因为 GNN 的每一层卷积,都是在同一个时间切片上聚合邻居信息。如果时间轴没对齐,模型学到的就是错位的信息。
# 时间序列对齐
def align_time_series(data_dict):
"""
data_dict: {stock_code: DataFrame}
返回对齐后的 DataFrame,列为股票代码,行为日期
"""
# 获取所有日期的并集
all_dates = set()
for df in data_dict.values():
all_dates.update(df['trade_date'].unique())
all_dates = sorted(all_dates)
# 构建对齐矩阵
aligned = pd.DataFrame(index=all_dates)
for code, df in data_dict.items():
df = df.set_index('trade_date')['close']
aligned[code] = df
return aligned
核心要点: 对齐之后,检查一下每只股票的数据长度。如果某只股票缺失太多(比如超过 20%),我建议直接剔除。否则它会成为图里的「噪声节点」。
2.5 缺失值处理
这是最头疼的部分。金融数据缺失的原因五花八门:停牌、节假日、数据源抽风……
我的处理策略分三级:
| 缺失比例 | 处理方法 | 适用场景 |
|---|---|---|
| < 5% | 前向填充(ffill) | 日常波动,停牌1-2天 |
| 5% - 20% | 插值法(线性/多项式) | 连续停牌,但复牌后趋势明确 |
| > 20% | 剔除该股票 | 长期停牌、退市风险 |
# 缺失值处理
def handle_missing_values(aligned_df, method='ffill', max_missing=0.2):
# 计算每只股票的缺失比例
missing_ratio = aligned_df.isnull().mean()
# 剔除缺失过多的股票
keep_cols = missing_ratio[missing_ratio <= max_missing].index
df = aligned_df[keep_cols]
# 处理剩余缺失
if method == 'ffill':
df = df.fillna(method='ffill').fillna(method='bfill')
elif method == 'interpolate':
df = df.interpolate(method='linear', limit_direction='both')
return df
避坑指南: 我曾经用前向填充处理一只停牌两周的股票,结果复牌当天直接跌停,模型完全没反应过来。后来我加了一个规则:如果连续缺失超过 5 天,就用市场平均收益率来填充,而不是简单的前向填充。
2.6 知识体系总览
下面这张图,把本章的核心逻辑串起来了。你想想看,从数据源到 GNN 模型,中间要经过多少道工序。
你看,从数据源到 GNN 输入,中间每一步都可能出问题。我见过最离谱的一次,是有人直接用原始数据训练,结果模型学到的全是停牌模式——停牌就涨,复牌就跌。这哪是选股模型,分明是停牌预测器。
最后说一句: 数据预处理占整个项目 70% 的时间,这很正常。别嫌烦,把这一步做扎实了,后面的模型搭建就是水到渠成的事。