第四章:特征工程(上)——技术指标构建、波动率特征、动量因子、均值回复特征

各位同学,欢迎来到特征工程部分。说实话,在量化交易这个行当里,我见过太多人把精力花在模型调参上,却忽略了最基础的东西——特征。你想想看,模型再牛,喂进去的是垃圾,出来的只能是更精致的垃圾。今天我们就来聊聊特征工程的上半场,重点讲四个核心模块:技术指标、波动率、动量因子和均值回复。

4.1 技术指标构建:别只会用TA-Lib

很多人一上来就import talib,然后噼里啪啦算一堆指标。我个人习惯是,先想清楚这个指标到底在表达什么市场行为。举个例子,RSI(相对强弱指标)——它本质上是在度量过去N天内上涨幅度与总波动的比值。你理解了这一点,就能自己手写,还能做定制化改造。

核心思路:技术指标不是越多越好,关键是每个指标都要有明确的交易逻辑支撑。

我在项目中遇到过这样一个场景:用TA-Lib算出来的布林带,在A股市场经常出现频繁的假突破。后来我改成了用EWMA(指数加权移动平均)替代SMA,效果好了不少。为什么?因为EWMA对近期价格更敏感,能更快捕捉趋势变化。

这里给出一段我常用的布林带自定义实现:

import numpy as np
import pandas as pd

def custom_bollinger(price, window=20, num_std=2, use_ewma=True):
    if use_ewma:
        mid = price.ewm(span=window, adjust=False).mean()
    else:
        mid = price.rolling(window).mean()
    std = price.rolling(window).std()
    upper = mid + num_std * std
    lower = mid - num_std * std
    return pd.DataFrame({'mid': mid, 'upper': upper, 'lower': lower})

嗯,这里要注意:用EWMA时,窗口参数的含义和SMA略有不同。EWMA的span参数相当于让权重衰减到约86.5%时的周期长度,不是简单的平均周期。

4.2 波动率特征:市场的"心跳"

波动率这东西,说白了就是市场的心跳。心跳太快(高波动)容易猝死,心跳太慢(低波动)可能休克。我个人认为,波动率特征是所有特征里最被低估的一个。

常见的波动率指标有几种:

  • 历史波动率(HV):过去N天收益率的标准差,最基础但最稳定
  • 已实现波动率(RV):日内高频数据的波动率,需要tick级数据
  • Parkinson波动率:利用最高价和最低价估算,比HV更高效
  • GARCH波动率:带自回归的波动率模型,适合有波动聚集的市场

我曾经在商品期货上踩过一个坑:直接用日线算HV,结果发现夜盘跳空导致波动率被严重高估。后来改用"隔夜调整"后的收益率,才得到真实的日内波动率。具体做法是:

def adjusted_returns(close, open_price):
    # 隔夜收益率 = (开盘 - 前收盘) / 前收盘
    overnight_ret = (open_price - close.shift(1)) / close.shift(1)
    # 日内收益率 = (收盘 - 开盘) / 开盘
    intraday_ret = (close - open_price) / open_price
    # 总收益率 = (1 + 隔夜) * (1 + 日内) - 1
    total_ret = (1 + overnight_ret) * (1 + intraday_ret) - 1
    return total_ret

小技巧:波动率特征最好做标准化处理,比如除以近期中位数。这样在不同品种、不同时间区间之间才有可比性。

4.3 动量因子:趋势是你的朋友

动量因子,量化圈里最经典的因子之一。你想想看,如果一只股票连续涨了5天,它明天继续涨的概率大不大?答案是:不一定。动量因子有很多种变体,每种都有不同的适用场景。

我习惯把动量因子分为三类:

类型计算方式适用场景
价格动量(close - close_lag_N) / close_lag_N趋势明显的市场
成交量动量volume / volume_ma_N确认趋势强度
复合动量价格动量 × 成交量动量过滤假突破

这里有个坑,我曾经吃过亏:直接用收益率作为动量因子,结果在震荡市里被来回打脸。后来我加入了"动量持续性"的判断——只有当动量连续N期保持同向时,才认为是有效信号。

def momentum_signal(price, lookback=20, min_consistency=3):
    ret = price.pct_change(lookback)
    # 计算每日方向
    daily_dir = np.sign(price.diff())
    # 连续同向天数
    consistency = daily_dir.rolling(lookback).apply(
        lambda x: np.sum(x == x.iloc[-1]) if len(x) > 0 else 0
    )
    # 只有动量方向一致且持续性足够时才产生信号
    signal = np.where(
        (np.abs(ret) > 0.02) & (consistency >= min_consistency),
        np.sign(ret), 0
    )
    return signal

警告:动量因子在极端行情下会失效。比如2020年3月的美股熔断,动量策略几乎全部崩溃。记得加上风控条件。

4.4 均值回复特征:物极必反

均值回复,说白了就是"涨多了会跌,跌多了会涨"。这个逻辑在震荡市里特别好用,但在单边趋势里就是灾难。我建议你把均值回复和动量因子结合起来用——用动量判断趋势方向,用均值回复寻找入场时机。

常用的均值回复特征包括:

  • Z-score:(当前价格 - 均值) / 标准差,衡量偏离程度
  • 乖离率:(价格 - 均线) / 均线,简单直观
  • RSI:超买超卖指标,30以下超卖,70以上超买
  • 布林带宽度:(上轨 - 下轨) / 中轨,衡量波动区间

我记得有一次做ETF套利,发现沪深300和中证500的价差经常在±2%之间波动。我构建了一个简单的均值回复策略:当价差超过1.5%时做空价差,低于-1.5%时做多价差。回测效果不错,但实盘时发现一个问题——价差可能长时间不回均值,导致浮亏很大。

后来我加了一个"时间衰减"因子:如果价差偏离超过N天还没回复,就强制平仓。这个改进让夏普比率从0.8提升到了1.4。

def mean_reversion_score(price, window=20, decay_days=10):
    zscore = (price - price.rolling(window).mean()) / price.rolling(window).std()
    # 时间衰减:偏离越久,信心越低
    days_since_cross = np.where(
        np.abs(zscore) > 1.5,
        np.arange(len(zscore)) - np.maximum.accumulate(
            np.where(np.abs(zscore) <= 1.5, np.arange(len(zscore)), 0)
        ),
        0
    )
    decay = np.exp(-days_since_cross / decay_days)
    return zscore * decay

4.5 知识体系总览

说了这么多,我们来画一张图,把今天讲的内容串起来。这张图展示了特征工程的四个核心模块以及它们之间的关系:

特征工程核心模块 技术指标构建 布林带、RSI、MACD 自定义改造(EWMA替代SMA) 逻辑驱动 > 工具依赖 波动率特征 HV、RV、Parkinson、GARCH 隔夜调整、标准化处理 市场的"心跳"监测 动量因子 价格动量、成交量动量 复合动量、持续性判断 趋势识别 + 假突破过滤 均值回复特征 Z-score、乖离率、RSI 布林带宽度、时间衰减 物极必反 + 风控约束 四个模块协同使用,构建多维特征空间

这张图里,四个模块不是孤立的。技术指标是基础,波动率特征提供风险度量,动量因子判断方向,均值回复寻找拐点。我个人习惯是把这四个维度的特征组合成一个特征向量,然后输入到模型中。你想想看,一个同时包含动量方向和均值回复程度的特征,是不是比单一指标更有信息量?

实战建议:刚开始做特征工程时,不要追求数量。先选3-5个核心特征,把它们的计算逻辑、参数敏感性、市场适应性都吃透。等跑通了基础模型,再逐步添加新特征。

好了,特征工程的上半场就到这里。记住一句话:好的特征能让简单的模型跑出惊人的效果,烂的特征能让复杂的模型变成废物。下一节我们会继续聊特征工程的下半场——时序特征、截面特征和特征选择。各位先把今天的内容消化一下,动手写写代码,有问题随时交流。

专注资料整理