第二章:金融数据与预处理

做量化投资,尤其是强化学习训练,第一步就是跟数据打交道。我见过太多人一上来就调模型,结果数据都是脏的,训练出来的策略跟笑话一样。今天我们就聊聊金融数据的获取和清洗,这是整个资产配置模型的基石。

2.1 获取股票/ETF历史数据

数据源的选择,我个人习惯用两个:一是免费的Tushare或AKShare,适合个人研究;二是Wind或Bloomberg,机构用得比较多。这里我以AKShare为例,因为它对国内ETF支持得不错。

import akshare as ak
import pandas as pd
import numpy as np

# 获取沪深300 ETF历史数据
etf_data = ak.fund_etf_hist_em(
    symbol="510300", 
    period="daily", 
    start_date="20200101", 
    end_date="20231231", 
    adjust="qfq"
)
print(etf_data.head())

嗯,这里要注意。前复权(qfq)和后复权(hfq)的选择很关键。我做过一个回测,用后复权数据训练出来的策略,实盘时直接崩了。为什么?因为后复权会改变历史价格,导致强化学习模型学到的是「假趋势」。

避坑指南: 我曾经用后复权数据训练了一个DQN模型,回测曲线漂亮得不行。结果实盘第一天就亏了3%。后来发现是复权方式搞错了。建议统一用前复权,或者干脆用原始价格+分红除权数据自己算。

2.2 处理缺失值与异常值

金融数据里缺失值很常见。停牌、节假日、数据源故障都会导致NaN。我的处理原则很简单:

  • 缺失值少于5%:用前向填充(ffill)或插值法
  • 缺失值在5%-20%:用同类ETF的均值填充
  • 缺失值超过20%:直接删除该时间段
# 前向填充缺失值
etf_data.fillna(method='ffill', inplace=True)

# 或者用线性插值
etf_data.interpolate(method='linear', inplace=True)

异常值处理更有意思。我记得有一次,某ETF突然出现单日涨幅20%的数据点。一看,原来是数据源把分红日当成了价格跳变。这种异常值如果不处理,强化学习模型会认为「哇,原来一天能赚20%」,然后疯狂追涨。

我的经验: 用3倍标准差法或者IQR(四分位距)法检测异常值。对于金融数据,我更推荐IQR法,因为它对极端值不那么敏感。
# IQR法检测异常值
Q1 = etf_data['收盘'].quantile(0.25)
Q3 = etf_data['收盘'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 标记异常值
etf_data['异常标记'] = (etf_data['收盘'] < lower_bound) | (etf_data['收盘'] > upper_bound)

2.3 计算收益率与波动率

收益率计算是核心中的核心。强化学习模型需要的是收益率序列,而不是价格序列。为什么?因为价格是非平稳的,收益率相对平稳。

# 计算对数收益率(推荐)
etf_data['log_return'] = np.log(etf_data['收盘'] / etf_data['收盘'].shift(1))

# 计算简单收益率
etf_data['simple_return'] = etf_data['收盘'].pct_change()

我个人习惯用对数收益率。原因很简单:对数收益率具有可加性,而且更接近正态分布。你想想看,如果某天跌了10%,第二天涨了10%,简单收益率算出来是-1%,但对数收益率算出来是0%。哪个更合理?显然是后者。

波动率计算,我一般用滚动窗口法:

# 20日滚动波动率
window = 20
etf_data['volatility'] = etf_data['log_return'].rolling(window=window).std() * np.sqrt(252)

# 或者用EWMA(指数加权移动平均)
etf_data['ewma_vol'] = etf_data['log_return'].ewm(span=window).std() * np.sqrt(252)
重要: 年化因子用252(交易日),不是365。我见过有人用365算波动率,结果模型训练出来的风险偏好完全不对。

2.4 数据标准化与平稳性检验

强化学习对输入数据的尺度非常敏感。如果价格是几百块,收益率是百分之几,模型会天然地更关注价格特征。所以标准化是必须的。

from sklearn.preprocessing import StandardScaler

# 标准化收益率和波动率
scaler = StandardScaler()
features = ['log_return', 'volatility']
etf_data[features] = scaler.fit_transform(etf_data[features])

平稳性检验,说白了就是检查数据有没有「趋势」或「季节性」。如果数据不平稳,强化学习模型学到的规律可能只是「过去一直在涨」,而不是真正的市场逻辑。

from statsmodels.tsa.stattools import adfuller

# ADF检验
result = adfuller(etf_data['log_return'].dropna())
print(f'ADF统计量: {result[0]}')
print(f'p值: {result[1]}')

if result[1] < 0.05:
    print('数据平稳,可以用于训练')
else:
    print('数据不平稳,需要差分处理')

嗯,这里有个坑。很多初学者以为收益率天然就是平稳的,其实不一定。我遇到过某些ETF在特定时间段内,收益率序列的ADF检验p值高达0.3。后来发现是因为那段时间市场处于极端波动期,收益率序列出现了明显的波动率聚集效应。

建议: 如果ADF检验不通过,可以试试一阶差分。或者用对数收益率的一阶差分。实在不行,就用滚动窗口标准化,让每个时间窗口的数据都服从N(0,1)。

知识体系总览

下面这张图是我自己整理的金融数据预处理流程。每次做新项目,我都会先过一遍这个流程,确保数据质量过关。

金融数据预处理流程 数据获取 缺失值处理 前向填充 插值法 均值填充 删除缺失段 异常值检测 3倍标准差法 IQR法 特征工程 收益率计算 波动率计算 标准化 平稳性检验 训练数据就绪

数据预处理这件事,说白了就是「垃圾进,垃圾出」。你花80%的时间在数据清洗上,一点都不夸张。我见过太多人把时间花在调参上,结果数据本身就有问题。强化学习模型再厉害,也架不住输入数据是错的。

核心要点:
  • 数据获取:前复权优先,注意数据源一致性
  • 缺失值:ffill + 插值法,超过20%直接删
  • 异常值:IQR法比3倍标准差更稳健
  • 收益率:对数收益率优于简单收益率
  • 波动率:滚动窗口法,年化因子用252
  • 标准化:StandardScaler,注意fit_transform的顺序
  • 平稳性:ADF检验,p值小于0.05才放心

好了,数据预处理这块就聊到这儿。记住,数据质量决定了模型的天花板。下一章我们会把这些处理好的数据喂给强化学习模型,看看它们到底能学到什么。


公众号:蓝海资料掘金营,微信deep3321