数据获取与预处理:量化分析的基石工程

做量化交易这些年,我越来越觉得数据预处理比策略本身更考验功力。你想想看,再好的模型,喂进去一堆脏数据,结果只能是垃圾进垃圾出。今天我们就来聊聊这个看似基础、实则决定成败的环节——数据获取与预处理。

核心观点:数据预处理占整个量化项目60%以上的时间,但决定了80%的最终效果。别急着跑模型,先把数据收拾利索了。

一、数据获取:不同市场的玩法不一样

股票、期货、加密货币,这三个市场的数据获取方式差别挺大。我刚开始做的时候,以为都一样,结果踩了不少坑。

1. 股票数据

A股市场的数据源,我个人习惯用Tushare或AkShare。前者需要注册获取token,后者直接pip安装就能用。港股和美股的话,Yahoo Finance是个不错的选择,但要注意它偶尔会断连。

import akshare as ak
# 获取A股日线数据
stock_data = ak.stock_zh_a_hist(symbol="000001", period="daily", 
                                start_date="20200101", end_date="20231231")
print(stock_data.head())

我的经验:数据源最好准备两个以上。我曾经遇到Tushare某天接口升级,数据拉不下来,幸好有AkShare做备选,才没耽误当天的策略运行。

2. 期货数据

期货数据有个麻烦事——主力合约会切换。你直接拉连续合约,会发现价格有跳空。我建议用Wind或CTP接口获取原始合约数据,然后自己拼接。

# 期货主力合约拼接示例
def get_futures_main_contract(symbol, start_date, end_date):
    # 获取所有合约列表
    contracts = get_all_contracts(symbol)
    # 按持仓量筛选主力合约
    main_contracts = []
    for contract in contracts:
        if is_main_contract(contract):
            main_contracts.append(contract)
    # 拼接数据
    result = pd.concat(main_contracts)
    return result

3. 加密货币数据

加密货币是7×24小时交易的,数据量巨大。我常用CCXT库,它支持上百个交易所。不过要注意,不同交易所的K线起始时间可能不一样,对齐起来有点麻烦。

import ccxt
exchange = ccxt.binance()
# 获取BTC/USDT的1小时K线
ohlcv = exchange.fetch_ohlcv('BTC/USDT', '1h', limit=1000)
# 转换成DataFrame
df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])

二、数据清洗:脏数据的处理艺术

数据拉下来之后,你会发现各种问题。缺失值、异常值、重复值……嗯,这些都是家常便饭。

1. 缺失值处理

缺失值分两种:随机缺失和系统性缺失。随机缺失比如某天交易所网络故障,数据没传过来。系统性缺失比如非交易日,股票没有数据。

缺失类型 处理方法 适用场景
随机缺失 前向填充、插值 日内高频数据
系统性缺失 保留NaN或删除 日线数据(非交易日)
连续缺失 模型预测填充 缺失超过5个连续点
# 缺失值处理示例
# 前向填充(适合金融时间序列)
df.fillna(method='ffill', inplace=True)
# 线性插值
df.interpolate(method='linear', inplace=True)

避坑指南:我曾经用均值填充处理股票数据,结果把一段下跌趋势硬生生填成了横盘。后来策略回测表现很好,实盘却亏得一塌糊涂。记住:金融数据不要用均值填充,它会破坏趋势信息。

2. 异常值检测

异常值怎么找?我常用的方法有三种:

  • 标准差法:超过3倍标准差的数据视为异常
  • IQR法:四分位距法,适合非正态分布
  • 百分比法:比如涨跌幅超过±10%视为异常
# 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
outliers = df[(df['close'] < lower_bound) | (df['close'] > upper_bound)]

三、重采样与对齐:让不同频率的数据说话

实际工作中,你经常会遇到不同频率的数据。比如股票是日线,宏观经济数据是月线,新闻情绪是分钟级。怎么把它们对齐?

1. 降采样(高频转低频)

把分钟数据合成日线,这是最常见的操作。注意OHLC的聚合规则:Open取第一个,High取最大值,Low取最小值,Close取最后一个,Volume求和。

# 分钟数据转日线
daily_data = minute_data.resample('D').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'volume': 'sum'
})

2. 升采样(低频转高频)

这个要小心。把月线数据填充到日线,本质上是在创造信息。我一般只用前向填充,不会用插值——因为未来的数据不能用来预测过去。

3. 多资产对齐

不同股票的交易时间不一样,比如A股和港股的开盘时间就不同。对齐时,我习惯用左对齐(取较早的时间点),这样能保证策略信号不会用到未来数据。

我的习惯:对齐之后,一定要检查一下数据长度。如果发现某只股票的数据比其他股票少很多,那可能是停牌了。这时候要决定是删除这只股票,还是用前值填充。

四、对数收益率:为什么不用简单收益率?

这个问题我经常被问到。简单收益率和对数收益率,到底用哪个?

说白了,对数收益率有两个好处:

  • 可加性:多期对数收益率可以直接相加,简单收益率不行
  • 正态性:对数收益率更接近正态分布,方便建模
# 对数收益率计算
df['log_return'] = np.log(df['close'] / df['close'].shift(1))
# 简单收益率(用于对比)
df['simple_return'] = df['close'].pct_change()

重要提醒:计算收益率时,一定要先排序。按时间升序排列,然后用shift(1)取前一天的收盘价。我见过有人忘记排序,结果收益率算出来全是负的——因为数据是倒序的。

五、OHLCV数据结构:量化分析的通用语言

OHLCV是量化交易的通用数据格式。Open、High、Low、Close、Volume,这五个字段几乎能衍生出所有技术指标。

我整理了一个标准的数据结构模板:

字段 类型 说明
timestamp datetime 时间戳,建议用UTC
open float 开盘价
high float 最高价
low float 最低价
close float 收盘价
volume float 成交量
adj_close float 复权收盘价(股票必填)

为什么强调复权?因为股票有分红送股,不复权的话,价格会出现跳空。我见过有人用不复权的数据做回测,结果策略收益被高估了30%。

# 标准OHLCV数据结构
ohlcv_columns = ['timestamp', 'open', 'high', 'low', 'close', 'volume', 'adj_close']
# 确保数据类型正确
df['timestamp'] = pd.to_datetime(df['timestamp'])
df['open'] = df['open'].astype(float)
# 设置时间索引
df.set_index('timestamp', inplace=True)
# 按时间排序
df.sort_index(inplace=True)

最后提醒:数据预处理没有一劳永逸的方法。每个市场、每个品种都有自己的特点。我建议你每次处理完数据后,都画个图看看——肉眼检查比任何统计指标都管用。

数据获取与预处理核心流程 数据获取 股票/期货/加密货币 数据清洗 缺失值/异常值处理 重采样与对齐 多频率数据统一 收益率计算 对数收益率 vs 简单收益率 OHLCV数据结构 Open / High / Low / Close / Volume / Adj_Close 数据预处理是量化分析的基石,每一步都影响最终策略效果

专注资料整理