第三章 特征工程入门:技术指标计算与截面处理
各位同学,欢迎来到特征工程这一章。
说实话,在量化这个行当里摸爬滚打这些年,我越来越觉得:特征工程做得好,模型就成功了一半。深度学习再厉害,喂进去的是垃圾,出来的还是垃圾。今天我们就来聊聊,怎么把原始行情数据,加工成模型能吃的「好料」。
3.1 技术指标计算:均线、RSI、MACD
技术指标这东西,很多人觉得老掉牙。但我个人习惯是,永远不要轻视这些经典指标。它们背后是几代交易员用真金白银验证过的市场规律。
3.1.1 移动平均线(MA)
均线是最基础的特征。说白了,就是过去N天的平均收盘价。但你别小看它,不同周期的均线组合,能反映趋势的强弱。
import pandas as pd
import numpy as np
# 假设df是包含'close'列的DataFrame
df['ma5'] = df['close'].rolling(window=5).mean()
df['ma20'] = df['close'].rolling(window=20).mean()
df['ma60'] = df['close'].rolling(window=60).mean()
# 我个人喜欢加一个均线斜率特征
df['ma5_slope'] = (df['ma5'] - df['ma5'].shift(5)) / df['ma5'].shift(5)
3.1.2 相对强弱指标(RSI)
RSI衡量的是价格变动的速度和幅度。公式不复杂,但理解它的含义更重要:RSI超过70算超买,低于30算超卖。不过在实际中,这个阈值得根据品种调整。
def compute_rsi(series, period=14):
delta = series.diff()
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
return rsi
df['rsi_14'] = compute_rsi(df['close'], 14)
3.1.3 指数平滑异同移动平均线(MACD)
MACD其实是均线的衍生品。它由三部分组成:DIF线(快线)、DEA线(慢线)、柱状图。核心逻辑是:当快线上穿慢线时,是买入信号;下穿时,是卖出信号。
# 计算MACD
exp12 = df['close'].ewm(span=12, adjust=False).mean()
exp26 = df['close'].ewm(span=26, adjust=False).mean()
df['macd_dif'] = exp12 - exp26
df['macd_dea'] = df['macd_dif'].ewm(span=9, adjust=False).mean()
df['macd_bar'] = 2 * (df['macd_dif'] - df['macd_dea'])
嗯,这里要注意:MACD的滞后性比较强。我建议把它当作趋势确认工具,而不是入场触发器。
3.2 滚动统计特征
技术指标是「有名字」的特征。但很多时候,我们需要一些「无名」的统计特征。滚动统计就是这类特征的典型代表。
说白了,就是在一个滑动窗口内,计算各种统计量。比如:
- 滚动均值:反映近期平均水平
- 滚动标准差:反映波动率
- 滚动偏度:反映分布不对称性
- 滚动峰度:反映尾部风险
- 滚动最大值/最小值:反映支撑阻力位
window = 20
df['roll_mean'] = df['close'].rolling(window).mean()
df['roll_std'] = df['close'].rolling(window).std()
df['roll_skew'] = df['close'].rolling(window).skew()
df['roll_kurt'] = df['close'].rolling(window).kurt()
df['roll_max'] = df['close'].rolling(window).max()
df['roll_min'] = df['close'].rolling(window).min()
# 我个人还喜欢加一个「位置特征」
df['position_in_range'] = (df['close'] - df['roll_min']) / (df['roll_max'] - df['roll_min'])
3.3 时序滞后特征
金融市场是有记忆的。今天的价格,往往和昨天的价格有关。时序滞后特征,就是把这个「记忆」显式地表达出来。
最简单的做法:
# 生成滞后特征
for lag in [1, 2, 3, 5, 10, 20]:
df[f'close_lag_{lag}'] = df['close'].shift(lag)
df[f'volume_lag_{lag}'] = df['volume'].shift(lag)
但这里有个坑:滞后太多期,特征会变得非常稀疏。我建议最多滞后到20期,再多的话,信息增益就微乎其微了。
另外,我习惯把滞后特征和差分特征结合起来用:
# 差分特征:今天相对于N天前的涨跌幅
for lag in [1, 5, 10]:
df[f'return_{lag}d'] = (df['close'] - df['close'].shift(lag)) / df['close'].shift(lag)
3.4 截面标准化与市值中性化
前面讲的特征,都是针对单只股票的「时序特征」。但深度学习模型往往需要处理多只股票。这时候,截面标准化就派上用场了。
3.4.1 截面标准化
截面标准化的目的,是消除不同股票之间的量纲差异。比如A股价格100元,B股价格5元,如果不做标准化,模型会天然地偏向高价股。
# 假设df是包含多只股票的面板数据,index是日期,columns是股票代码
# 截面标准化:在每个时间截面上,对特征进行Z-score标准化
def cross_sectional_normalize(df_feature):
mean = df_feature.mean(axis=1)
std = df_feature.std(axis=1)
df_normalized = (df_feature.subtract(mean, axis=0)).divide(std, axis=0)
return df_normalized
# 对收益率特征做截面标准化
df['return_normalized'] = cross_sectional_normalize(df['return_1d'])
3.4.2 市值中性化
市值中性化,说白了就是剔除市值因子对特征的影响。为什么要这么做?因为市值大的股票和市值小的股票,在很多特征上天然不同。如果不做中性化,模型学到的可能只是「大小盘风格」,而不是真正的Alpha信号。
具体做法:
- 对每个特征,用市值做回归
- 取回归的残差作为中性化后的特征
import statsmodels.api as sm
def market_cap_neutralize(feature, market_cap):
"""
对特征做市值中性化
feature: 某时间截面上所有股票的特征值
market_cap: 对应时间截面上所有股票的市值
"""
X = sm.add_constant(np.log(market_cap)) # 市值取对数,更符合线性关系
y = feature.values
model = sm.OLS(y, X).fit()
residual = y - model.predict(X)
return residual
# 应用:对每个时间截面做中性化
# 这里假设df_feature和df_market_cap都是面板数据
df_neutralized = df_feature.copy()
for date in df_feature.index:
df_neutralized.loc[date] = market_cap_neutralize(
df_feature.loc[date],
df_market_cap.loc[date]
)
3.5 本章知识体系
说了这么多,我们来梳理一下本章的核心逻辑。下面这张图,是我自己画的知识框架:
这张图把本章的内容串起来了。你看,从原始行情数据出发,我们做了四类特征:技术指标、滚动统计、时序滞后、截面处理。前三类属于「时序特征」,最后一类属于「截面特征」。两者结合,才能给模型提供全面的信息。
公众号:蓝海资料掘金营,微信deep3321