特征工程(上):技术指标计算与滚动窗口统计
大家好,我是你们的量化金融工程师。今天咱们聊聊特征工程的上半部分——技术指标计算和滚动窗口统计。说实话,这部分内容我每次做股票分类项目时都会反复打磨。为什么?因为特征的质量直接决定了聚类效果的好坏。你想想看,垃圾数据进去,再牛的算法也白搭。
核心观点:特征工程不是简单的公式套用,而是对市场行为的量化表达。好的特征能帮你捕捉到股票的内在规律。
2.1 移动平均线(MA)——趋势的骨架
移动平均线,说白了就是给价格序列做平滑。我刚开始做量化时,总觉得这东西太简单,不就是算个均值嘛。直到有一次,我用5日均线和60日均线的交叉信号做策略,回测效果出奇的好。嗯,简单的东西往往最有效。
常用的移动平均线有三种:
- 简单移动平均(SMA):等权重平均,计算快,但滞后明显
- 指数移动平均(EMA):近期的数据权重更大,反应更灵敏
- 加权移动平均(WMA):自定义权重,灵活性高
我个人习惯用EMA做短线分析,SMA做长线趋势判断。为什么?EMA对价格突变反应快,适合捕捉短期机会;SMA平滑效果好,能过滤掉一些噪音。
import pandas as pd
import numpy as np
def calculate_ma(df, windows=[5, 10, 20, 60]):
"""
计算多周期移动平均线
"""
for w in windows:
# 简单移动平均
df[f'SMA_{w}'] = df['close'].rolling(window=w).mean()
# 指数移动平均
df[f'EMA_{w}'] = df['close'].ewm(span=w, adjust=False).mean()
return df
# 使用示例
df = calculate_ma(df, windows=[5, 10, 20, 60])
print(df[[f'SMA_{w}' for w in [5, 10, 20, 60]]].head())
避坑指南:我曾经在计算EMA时忘记设置adjust=False,结果算出来的值跟主流交易软件对不上。后来查文档才发现,adjust参数控制的是初始值的处理方式。记住,做量化一定要跟市场标准对齐。
2.2 MACD——趋势的加速器
MACD(指数平滑异同移动平均线)是我最常用的趋势指标之一。它本质上是对两条EMA的差值再做一次平滑。你想想看,这相当于在趋势的基础上再找趋势的变化,是不是很巧妙?
MACD由三部分组成:
- DIF线:快线(通常12日EMA)减去慢线(通常26日EMA)
- DEA线:DIF线的9日EMA
- MACD柱:DIF与DEA的差值,反映动能强弱
def calculate_macd(df, fast=12, slow=26, signal=9):
"""
计算MACD指标
"""
# 计算快慢EMA
ema_fast = df['close'].ewm(span=fast, adjust=False).mean()
ema_slow = df['close'].ewm(span=slow, adjust=False).mean()
# DIF线
df['MACD_DIF'] = ema_fast - ema_slow
# DEA线
df['MACD_DEA'] = df['MACD_DIF'].ewm(span=signal, adjust=False).mean()
# MACD柱
df['MACD_HIST'] = 2 * (df['MACD_DIF'] - df['MACD_DEA'])
return df
df = calculate_macd(df)
我在项目中遇到过一个问题:不同股票的波动率差异很大,直接用MACD的绝对值做特征,效果并不好。后来我改用MACD的标准化值——除以当前股价或者波动率,聚类效果明显提升。这个经验分享给大家。
2.3 RSI——超买超卖的探测器
RSI(相对强弱指标)衡量的是价格变动的速度和幅度。它的核心思想很简单:在一段时间内,上涨的幅度和下跌的幅度做对比。如果涨得太多,就可能回调;跌得太多,就可能反弹。
标准RSI的计算公式:
def calculate_rsi(df, period=14):
"""
计算RSI指标
"""
delta = df['close'].diff()
# 分离上涨和下跌
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0)
# 计算平均涨幅和平均跌幅
avg_gain = gain.rolling(window=period).mean()
avg_loss = loss.rolling(window=period).mean()
# RSI计算
rs = avg_gain / avg_loss
df['RSI'] = 100 - (100 / (1 + rs))
return df
df = calculate_rsi(df, period=14)
注意:RSI在震荡行情中表现很好,但在单边趋势中容易产生假信号。我曾经用RSI<30作为买入信号,结果在熊市中连续被套。后来我加了一个条件——必须配合趋势确认,比如价格在MA20之上才考虑买入。
2.4 布林带——价格的通道
布林带由三条线组成:中轨(通常是20日SMA)、上轨(中轨+2倍标准差)、下轨(中轨-2倍标准差)。它反映的是价格相对于历史波动范围的位置。
我个人觉得布林带最有价值的地方在于:它把波动率纳入了考量。当布林带收窄时,说明市场在蓄势;当布林带扩张时,说明趋势在加速。
def calculate_bollinger(df, period=20, num_std=2):
"""
计算布林带指标
"""
# 中轨
df['BB_MID'] = df['close'].rolling(window=period).mean()
# 标准差
df['BB_STD'] = df['close'].rolling(window=period).std()
# 上下轨
df['BB_UPPER'] = df['BB_MID'] + num_std * df['BB_STD']
df['BB_LOWER'] = df['BB_MID'] - num_std * df['BB_STD']
# 带宽(归一化指标)
df['BB_WIDTH'] = (df['BB_UPPER'] - df['BB_LOWER']) / df['BB_MID']
# 位置指标
df['BB_POSITION'] = (df['close'] - df['BB_LOWER']) / (df['BB_UPPER'] - df['BB_LOWER'])
return df
df = calculate_bollinger(df)
小技巧:布林带的带宽(BB_WIDTH)是一个很好的波动率指标。我在做股票分类时,经常用带宽来区分高波动和低波动的股票。你想想看,银行股和科技股的带宽肯定不一样,这个特征对聚类很有帮助。
2.5 滚动窗口统计特征——挖掘时序的深层信息
除了标准的技术指标,滚动窗口统计特征也是我常用的工具。说白了,就是在一定时间窗口内,计算价格序列的各种统计量。这些特征能反映股票在不同时间尺度上的行为模式。
常用的滚动统计特征:
| 统计量 | 含义 | 应用场景 |
|---|---|---|
| 滚动均值 | 窗口内的平均价格 | 趋势方向判断 |
| 滚动方差 | 窗口内的波动程度 | 风险度量 |
| 滚动斜率 | 窗口内价格的线性趋势 | 趋势强度判断 |
| 滚动偏度 | 价格分布的不对称性 | 尾部风险识别 |
| 滚动峰度 | 价格分布的尖峰程度 | 极端事件概率 |
def calculate_rolling_stats(df, window=20):
"""
计算滚动窗口统计特征
"""
# 滚动均值
df[f'ROLL_MEAN_{window}'] = df['close'].rolling(window=window).mean()
# 滚动方差
df[f'ROLL_VAR_{window}'] = df['close'].rolling(window=window).var()
# 滚动斜率(用线性回归拟合)
def rolling_slope(series):
x = np.arange(len(series))
slope, _ = np.polyfit(x, series, 1)
return slope
df[f'ROLL_SLOPE_{window}'] = df['close'].rolling(window=window).apply(
rolling_slope, raw=True
)
# 滚动偏度
df[f'ROLL_SKEW_{window}'] = df['close'].rolling(window=window).skew()
# 滚动峰度
df[f'ROLL_KURT_{window}'] = df['close'].rolling(window=window).kurt()
return df
df = calculate_rolling_stats(df, window=20)
经验之谈:滚动斜率这个特征,我曾在一次股票分类项目中发挥了关键作用。当时我发现,同样是上涨趋势,有些股票斜率稳定,有些则忽高忽低。通过斜率的标准差,我成功区分出了「稳健型」和「激进型」的股票。聚类结果非常清晰。
2.6 特征相关性分析与筛选
特征造了一大堆,但并不是越多越好。你想想看,如果两个特征高度相关,它们提供的信息其实是重复的。而且,过多的特征会导致维度灾难,聚类效果反而变差。
我常用的特征筛选方法:
- 计算相关性矩阵:找出相关系数大于0.8的特征对
- 保留代表性特征:从高度相关的特征组中,选一个最有解释力的
- 结合业务理解:有些特征虽然相关,但业务含义不同,可以都保留
def feature_selection(df, threshold=0.8):
"""
基于相关性进行特征筛选
"""
# 选择数值型特征
features = df.select_dtypes(include=[np.number]).columns.tolist()
# 计算相关性矩阵
corr_matrix = df[features].corr().abs()
# 找出高度相关的特征对
upper = corr_matrix.where(
np.triu(np.ones(corr_matrix.shape), k=1).astype(bool)
)
# 需要删除的特征
to_drop = [column for column in upper.columns
if any(upper[column] > threshold)]
print(f"建议删除的特征(相关性>{threshold}):{to_drop}")
# 返回筛选后的特征
selected_features = [f for f in features if f not in to_drop]
return selected_features
selected = feature_selection(df, threshold=0.8)
注意:相关性筛选只是第一步。我曾经犯过一个错误——只依赖相关性分析,结果删掉了一些虽然相关但很有价值的特征。比如,SMA_5和SMA_10虽然高度相关,但它们在不同时间尺度上的交叉信号对聚类很有帮助。所以,筛选时一定要结合业务理解。
2.7 本章知识体系
下面我用一张SVG图来总结本章的核心内容。这张图展示了从原始数据到最终特征集的完整流程。
这张图清晰地展示了本章的脉络:从原始价格数据出发,先计算四大技术指标,再提取滚动窗口统计特征,最后通过相关性分析进行特征筛选。每一步都有它的意义,缺一不可。
好了,特征工程的上半部分就讲到这里。我个人觉得,技术指标的计算虽然基础,但真正用好它们需要大量的实践和思考。下一章我们会继续讨论特征工程的下半部分——如何构造更复杂的衍生特征,以及如何处理缺失值和异常值。到时候见。