第二章:金融市场数据基础——股票数据获取与预处理

做量化交易,数据就是你的弹药库。弹药不行,再好的策略也白搭。

这一章,咱们就聊聊怎么搞到靠谱的股票数据,以及拿到手之后怎么处理。我见过太多人,模型跑得飞起,结果一查,数据里全是坑。嗯,咱们别走那条弯路。

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 数据清洗与预处理

数据拿到手,别急着用。先看看有没有脏数据。我一般按这个流程走:

  1. 检查列名——统一成英文小写,方便后面处理
  2. 检查数据类型——日期是不是 datetime,价格是不是 float
  3. 检查缺失值——这个后面单独讲
  4. 检查异常值——比如某天涨幅超过 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 模型,中间要经过多少道工序。

金融数据预处理流程 数据源 Tushare / AKShare 数据获取 日线/分钟线/财务数据 数据清洗 去停牌/去异常/统一格式 时间序列对齐 统一时间轴/多股票对齐 缺失值处理 前向填充/插值/剔除 特征工程 收益率/波动率/技术指标 GNN 模型输入 关键指标 • 缺失率 < 5%:前向填充 • 缺失率 5-20%:插值法 • 缺失率 > 20%:剔除

你看,从数据源到 GNN 输入,中间每一步都可能出问题。我见过最离谱的一次,是有人直接用原始数据训练,结果模型学到的全是停牌模式——停牌就涨,复牌就跌。这哪是选股模型,分明是停牌预测器。

最后说一句: 数据预处理占整个项目 70% 的时间,这很正常。别嫌烦,把这一步做扎实了,后面的模型搭建就是水到渠成的事。

专注资料整理