第二章:金融数据基础——股票数据获取、清洗、存储与质量检查

做量化投资,数据就是你的弹药库。弹药不行,枪法再好也白搭。这一章,咱们就聊聊怎么搞到靠谱的股票数据,怎么把它收拾干净,再找个地方好好存起来。

我个人习惯把数据流程分成四步:获取、清洗、存储、检查。每一步都有坑,我踩过不少,今天一并告诉你。

2.1 股票数据获取:tushare vs akshare

国内做量化,绕不开两个库:tushare 和 akshare。它们都是 Python 的金融数据接口,但风格完全不同。

对比项 tushare akshare
数据源 自建数据库,稳定 爬取公开网站,更新快
使用门槛 需要注册 token,部分接口付费 免费,pip install 即用
数据质量 较高,经过清洗 依赖源网站,偶有异常
适合场景 生产环境、回测 研究、快速验证

我建议初学者先用 akshare 上手,零成本。等你要做严肃回测了,再切到 tushare 的 pro 版。

核心原则:数据源不是越全越好,而是越稳越好。宁可少几个字段,也别拿到一堆脏数据。

2.1.1 用 akshare 获取日线数据

举个例子,拿贵州茅台的历史日线。代码很简单:

import akshare as ak

# 获取日线数据
df = ak.stock_zh_a_hist(symbol="600519", period="daily", 
                        start_date="20200101", end_date="20231231")
print(df.head())

返回的字段包括:日期、开盘、收盘、最高、最低、成交量、成交额、振幅、涨跌幅、换手率。够用了。

小技巧:akshare 的接口名经常变。我习惯每次用之前先打印一下 ak.__doc__ 看看最新文档,省得踩坑。

2.1.2 用 tushare 获取数据

tushare 需要先注册拿到 token。我个人习惯把 token 存在环境变量里,不写死在代码中。

import tushare as ts

# 初始化
pro = ts.pro_api('你的token')

# 获取日线
df = pro.daily(ts_code='600519.SH', start_date='20200101', 
               end_date='20231231')
print(df.head())

tushare 返回的数据更规整,字段命名也统一。比如复权因子、停牌标记这些,akshare 不一定有。

注意:tushare 的 pro 接口有调用频率限制。基础版每分钟最多 200 次。我曾经写了个循环批量下载,结果被限流了半小时...后来老老实实加了 time.sleep。

2.2 数据清洗与预处理

数据拿到手,别急着用。先看看有没有脏数据。我总结了一套标准流程:

  1. 缺失值处理——检查 NaN,决定填充还是删除
  2. 异常值检测——比如涨跌幅超过 20% 的,大概率是停牌后复牌
  3. 数据类型转换——日期转 datetime,价格转 float
  4. 排序与索引——按日期升序,设置日期为索引
  5. 复权处理——后复权或前复权,统一口径

2.2.1 缺失值处理

股票数据里,缺失值常见于停牌日。比如某只股票停牌一周,那几天的数据就是 NaN。

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

# 方法一:删除缺失行
df_clean = df.dropna()

# 方法二:向前填充(用上一个交易日的数据)
df_filled = df.fillna(method='ffill')

我个人习惯用前向填充。因为停牌期间,价格理论上应该延续停牌前的状态。但要注意,如果连续停牌太久,前向填充会失真。

经验之谈:我曾经处理过一只停牌半年的股票,复牌当天跌了 40%。如果用前向填充,那天的数据会完全偏离真实情况。所以,对于停牌超过 20 天的,我建议直接删除那几行。

2.2.2 异常值检测

涨跌幅超过 10% 的,在 A 股里基本就是异常。要么是新股上市首日,要么是 ST 股。我一般用 3-sigma 法则:

# 计算涨跌幅的均值和标准差
mean = df['pct_chg'].mean()
std = df['pct_chg'].std()

# 标记异常
df['is_outlier'] = (abs(df['pct_chg'] - mean) > 3 * std)

嗯,这里要注意:3-sigma 法则假设数据服从正态分布。但股票涨跌幅其实有尖峰厚尾特征。所以我会结合业务逻辑再判断。比如涨跌幅超过 20% 的,直接标记出来人工审核。

2.2.3 复权处理

复权是个大坑。分红、送股、配股都会导致价格跳空。如果不复权,你的回测结果会严重失真。

tushare 和 akshare 都提供了复权因子。我个人习惯用后复权,因为后复权的价格序列是连续的,适合做技术分析。

# 使用 tushare 获取复权因子
df_adj = pro.adj_factor(ts_code='600519.SH')

# 计算后复权价格
df['close_adj'] = df['close'] * df_adj['adj_factor']

避坑指南:我曾经直接用前复权数据做回测,结果发现策略收益被高估了。原因是前复权会改变历史价格,导致买入信号提前出现。后来我改用后复权,回测结果才合理。

2.3 数据存储:CSV、HDF5 与数据库

数据清洗完了,得找个地方存起来。三种主流方式,各有优劣。

存储方式 优点 缺点 适用场景
CSV 简单、通用、可读 慢、无压缩、不支持索引 小数据量、临时分析
HDF5 快、支持压缩、支持索引 二进制、不易查看 中等数据量、回测
数据库 支持并发、查询灵活 部署复杂、维护成本高 生产环境、团队协作

我个人建议:研究阶段用 HDF5,生产环境用数据库。CSV 只用来做数据交换。

2.3.1 存储为 CSV

df.to_csv('600519_daily.csv', index=False)

简单粗暴。但如果你有 5000 只股票,每只存一个 CSV,管理起来会疯掉。

2.3.2 存储为 HDF5

HDF5 支持分层存储。我习惯按股票代码分组,每个组里存不同频率的数据。

import pandas as pd

# 存储
with pd.HDFStore('stock_data.h5') as store:
    store.put('600519/daily', df, format='table')
    store.put('000001/daily', df2, format='table')

# 读取
with pd.HDFStore('stock_data.h5') as store:
    df = store.get('600519/daily')

性能对比:同样 100 万行数据,CSV 读取需要 3 秒,HDF5 只需要 0.3 秒。回测时,这个差距会放大到分钟级别。

2.3.3 存储到数据库

我用得最多的是 SQLite,轻量级,零配置。团队协作时用 PostgreSQL。

import sqlite3

# 连接数据库
conn = sqlite3.connect('stock.db')

# 写入
df.to_sql('daily_600519', conn, if_exists='replace', index=False)

# 读取
df = pd.read_sql('SELECT * FROM daily_600519 WHERE date > "2023-01-01"', conn)

注意:数据库写入时,一定要设置索引。否则每次查询都是全表扫描,慢得你想哭。我建议用 (date, ts_code) 作为联合索引。

2.4 数据质量检查

数据存好了,不代表就万事大吉。我每次跑回测前,都会做一轮质量检查。这是血的教训换来的。

2.4.1 检查清单

  • 时间连续性:交易日是否完整?有没有跳空?
  • 价格合理性:开盘价、最高价、最低价、收盘价是否满足 OHLC 关系?
  • 成交量非负:成交量不能为负数或零(停牌除外)
  • 涨跌幅范围:A 股正常涨跌幅不超过 ±10%,ST 股不超过 ±5%
  • 复权一致性:复权后的价格序列是否单调?

2.4.2 自动化检查脚本

我写了一个简单的检查函数,每次加载数据后自动跑一遍。

def check_data_quality(df):
    issues = []
    
    # 检查 OHLC 关系
    if not (df['high'] >= df['low']).all():
        issues.append('最高价低于最低价')
    if not (df['high'] >= df['close']).all():
        issues.append('最高价低于收盘价')
    
    # 检查成交量
    if (df['volume'] < 0).any():
        issues.append('存在负成交量')
    
    # 检查涨跌幅
    if (abs(df['pct_chg']) > 10).any():
        issues.append('存在异常涨跌幅')
    
    if issues:
        print('数据质量问题:')
        for issue in issues:
            print(f'  - {issue}')
    else:
        print('数据质量检查通过')
    
    return issues

经验之谈:我曾经用了一个看似完美的数据集,回测年化收益 30%。结果一查,发现数据里混入了未来信息——某天的收盘价被错误地填充了第二天的数据。从那以后,我每次回测前都会做一次未来数据检测:检查是否有任何特征用到了未来的信息。

2.5 本章知识体系

下面这张图,概括了本章的核心逻辑。从数据源到最终可用数据,每一步都有对应的处理手段。

金融数据基础:从获取到可用 数据源 数据源 tushare(稳定、付费) akshare(免费、更新快) 数据清洗与预处理 缺失值处理 异常值检测 复权处理 类型转换 数据存储 CSV(临时) HDF5(回测) 数据库(生产) 数据质量检查

你看,整个流程是环环相扣的。数据源选错了,后面再怎么清洗也救不回来。存储方式选错了,回测效率会大打折扣。质量检查没做好,策略结果可能就是一场幻觉。

我个人习惯把这套流程写成脚本,每天开盘前自动跑一遍。数据有问题,系统会发邮件提醒我。这样我就能安心睡觉,不用半夜爬起来检查数据。

最后说一句:数据是量化投资的基石。花 80% 的时间在数据上,剩下 20% 的时间建模,这是值得的。别像我刚开始那样,拿到数据就急着跑策略,结果被数据坑得体无完肤。

专注资料整理