第1章 数据准备:获取行情数据(OHLCV)、数据清洗与对齐
做因子挖掘,第一步不是写模型,而是搞定数据。我见过太多人一上来就调包跑策略,结果回测漂亮得一塌糊涂,实盘一跑就崩——十有八九是数据出了问题。说白了,数据是因子挖掘的「地基」,地基不稳,上面盖什么都是危楼。
这一章,我们就来聊聊怎么把行情数据(OHLCV)拿到手,怎么洗干净,怎么对齐。嗯,这里说的「对齐」不只是时间对齐,还包括复权、除权、停牌这些坑。我当年刚入行时,就因为没处理好复权,白白亏了两个月的时间。
1.1 行情数据长什么样?
OHLCV 是四个价格加一个成交量的缩写:
- O(Open):开盘价
- H(High):最高价
- L(Low):最低价
- C(Close):收盘价
- V(Volume):成交量
这是最基础的数据结构。你想想看,几乎所有技术指标、因子计算,都是从这五个字段衍生出来的。比如最简单的动量因子,就是拿今天的收盘价和 N 天前的收盘价比一下。
我个人习惯把数据存成 DataFrame 格式,索引是时间,列是股票代码。举个例子:
import pandas as pd
import numpy as np
# 模拟一个简单的 OHLCV 数据
dates = pd.date_range('2024-01-01', periods=5, freq='D')
data = {
'open': [100, 102, 101, 103, 105],
'high': [103, 104, 103, 106, 107],
'low': [99, 101, 100, 102, 104],
'close':[102, 101, 103, 105, 106],
'volume':[10000, 12000, 11000, 13000, 15000]
}
df = pd.DataFrame(data, index=dates)
print(df)
输出结果:
open high low close volume
2024-01-01 100 103 99 102 10000
2024-01-02 102 104 101 101 12000
2024-01-03 101 103 100 103 11000
2024-01-04 103 106 102 105 13000
2024-01-05 105 107 104 106 15000
你看,结构就这么简单。但实际项目中,数据源往往不会这么规整。有的接口返回的是 JSON,有的是 CSV,有的甚至直接给你一个 Excel 文件。我建议你统一转成 DataFrame,后面处理起来会顺手很多。
1.2 数据获取:从哪里拿?
获取行情数据的渠道很多,我列几个常用的:
| 数据源 | 特点 | 适用场景 |
|---|---|---|
| Tushare | 免费,数据全,但有限流 | 个人研究、小规模回测 |
| Wind | 收费,数据质量高 | 机构、专业量化团队 |
| 聚宽/米筐 | 平台自带数据,方便回测 | 策略开发、快速验证 |
| Yahoo Finance | 免费,覆盖全球市场 | 海外股票、加密货币 |
我个人偏好用 Tushare 做快速验证,毕竟免费又方便。但要注意,免费接口通常有频率限制,比如每分钟只能请求 200 次。如果你要拉全 A 股的历史数据,建议写个循环加 sleep,别把人家服务器搞崩了。
举个例子,用 Tushare 获取某只股票的历史日线数据:
import tushare as ts
# 设置 token(需要去官网注册)
ts.set_token('your_token_here')
pro = ts.pro_api()
# 获取平安银行(000001.SZ)2024年1月日线数据
df = pro.daily(ts_code='000001.SZ', start_date='20240101', end_date='20240131')
print(df.head())
输出结果:
ts_code trade_date open high low close vol
0 000001.SZ 20240131 10.2 10.5 10.1 10.3 123456
1 000001.SZ 20240130 10.1 10.4 10.0 10.2 110000
2 000001.SZ 20240129 10.3 10.6 10.2 10.4 130000
...
嗯,这里要注意:Tushare 返回的 trade_date 是字符串格式,建议转成 datetime 类型,方便后面做时间序列操作。
1.3 数据清洗:脏数据怎么处理?
数据拿到手,别急着用。先检查一下有没有脏数据。我在项目中遇到过最离谱的情况——某只股票某天的收盘价是 0,成交量是负数。你想想看,这要是直接拿去算因子,结果能看吗?
常见的脏数据包括:
- 缺失值:某天没有交易数据(停牌、节假日)
- 异常值:价格突变、成交量异常大或异常小
- 重复值:同一时间戳出现多条记录
- 格式错误:日期格式不统一、数字带逗号等
我的处理流程一般是这样的:
def clean_ohlcv(df):
# 1. 删除重复行
df = df.drop_duplicates()
# 2. 处理缺失值(向前填充,或直接删除)
df = df.fillna(method='ffill')
# 3. 过滤异常值(比如价格不能为0或负数)
df = df[(df['close'] > 0) & (df['volume'] >= 0)]
# 4. 排序(按时间升序)
df = df.sort_index()
return df
这里有个坑:向前填充(ffill)虽然方便,但如果你连续停牌好几天,填充出来的数据其实是有问题的。我曾经就因为没注意这个,导致因子在停牌期间「凭空」产生了交易信号,回测结果虚高。后来我加了一个判断:如果连续缺失超过 5 天,就直接删除这些数据。
1.4 数据对齐:时间、复权、除权
数据对齐是因子挖掘里最容易被忽视的环节。说白了,就是确保不同股票的数据在同一个时间点上「对齐」。比如你要算沪深 300 所有成分股的动量因子,如果 A 股和 B 股的数据日期不一致,算出来的因子值就是错的。
对齐主要分三步:
- 时间对齐:把所有股票的数据统一到同一个交易日历上
- 复权处理:消除分红、送股、配股对价格的影响
- 停牌处理:停牌期间的数据怎么填充或删除
先看时间对齐。A 股的交易日历不是连续的,周末和法定节假日都不交易。我建议你维护一个「交易日历表」,然后做左连接:
# 假设 trade_cal 是交易日历,df 是某只股票的数据
trade_cal = pd.DataFrame({'date': pd.date_range('2024-01-01', '2024-01-31', freq='B')})
df_aligned = trade_cal.merge(df, on='date', how='left')
这样,非交易日的数据就是 NaN,后面再填充或删除。
再来看复权。复权分为前复权和后复权。我个人习惯用前复权,因为它的价格序列是连续的,不会因为除权出现跳空。举个例子:
# 假设 df 包含复权因子(adj_factor)
df['close_adj'] = df['close'] * df['adj_factor']
这里 adj_factor 是复权因子,一般数据源会直接提供。如果没有,你可以自己算:
# 简单前复权计算(不考虑分红再投资)
def calc_adj_factor(df):
# 以最新价格为基准
base_price = df['close'].iloc[-1]
df['adj_factor'] = base_price / df['close']
return df
嗯,这个算法比较粗糙,实际项目中建议直接用数据源提供的复权因子。
最后是停牌处理。停牌期间没有交易数据,价格和成交量都是 NaN。我的做法是:
- 如果停牌时间短(≤5天),用前一天的收盘价填充
- 如果停牌时间长(>5天),直接删除这些数据,或者标记为「不可交易」
我曾经踩过一个坑:某只股票停牌半年,复牌后连续涨停。我用前复权填充了停牌期间的数据,结果因子计算时把停牌期间的「假价格」也算进去了,导致信号完全失真。后来我加了一个停牌标记列,在因子计算前先过滤掉停牌期间的数据。
1.5 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张流程图:
这张图把数据准备的流程串起来了。从获取原始数据开始,经过清洗、对齐,最终得到干净可用的 OHLCV 数据。每一步都有坑,每一步都需要你仔细处理。
1.6 避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路:
不同数据源的复权因子可能不同。我曾经用 Tushare 和 Wind 拉同一只股票的数据,算出来的因子值差了 0.5%。后来发现是复权算法不一样。建议你固定一个数据源,不要混用。
我习惯给每批数据打上时间戳和版本号。比如 "ohlcv_202401_v1.parquet"。这样万一发现数据有问题,可以快速回滚到之前的版本。
前面说过,停牌期间用前复权填充价格,会导致因子计算失真。我的建议是:在因子计算前,先过滤掉停牌期间的数据。具体做法是维护一个「停牌标记列」,停牌日标记为 1,正常交易日标记为 0。
如果你同时交易 A 股和港股,注意它们的交易日历不一样。A 股春节休市 7 天,港股可能只休 3 天。我建议你分别维护两个市场的交易日历,不要混在一起。
好了,数据准备这部分就讲到这里。记住一句话:数据质量决定了因子挖掘的上限。花 80% 的时间在数据上,剩下的 20% 用来写模型,这才是量化工程师该有的态度。