跳至正文
返回文库全部文档

构建波动率缩放的动量与趋势特征

代码 《交易机器学习》

总结

本特征工程模块为系统化宏观投资组合模型准备日价格数据。它创建按估计波动率缩放的不同期限收益、多种尺度的MACD信号,以及对数价格的滚动Z分数。收益期限可使用设定的一组值,也可使用密集范围来构建原始动量特征集。事前波动率通过日收益的指数加权标准差估计;模块还为学习目标计算下一步原始收益和波动率缩放收益。

为了限制极端特征值,每个特征都会根据滚动中位数和中位数绝对偏差进行截尾,然后将缺失特征值填为零。模块使用向前填充的价格计算变化,同时通过可用性检查屏蔽跨越缺失观测值的收益,并记录每项资产是否存在。生成的对齐面板包含日期、资产、特征、目标、波动率估计和掩码。这些只是数据变换,并非经过验证的交易策略:模型表现、交易成本和投资组合构建均不在本模块范围内,用户必须确保滚动计算和缺失数据处理约定符合预期的评估设计。

核心观点

  • 将不同设定期限的收益按估计波动率和期限长度进行标准化。
  • 特征集可包含多种尺度的MACD信号和对数价格滚动Z分数。
  • EWMA波动率既用于缩放估计,也用于调整下一步目标值。
  • 使用滚动中位数和中位数绝对偏差进行稳健的特征截尾。
  • 可用性掩码用于区分有观测数据的资产和缺失价格,而特征缺口则填为零。

标签

全文
# features.py


```py
"""Feature engineering for DeePM-style systematic macro models.

Constructs a compact feature set from daily closes:
1. Volatility-normalized returns for horizons h in {1, 21, 63, 252}
2. Multi-scale MACD signals at (8,24), (16,48), (32,96) plus re-normalization
3. Log-price z-scores over windows l in {21, 252}
4. Robust clipping using rolling median and MAD over 252 days
"""

from __future__ import annotations

from collections.abc import Sequence
from dataclasses import dataclass

import numpy as np
import pandas as pd

from .configs import FeatureConfig


@dataclass(frozen=True, slots=True)
class FeaturePanel:
    """A fully-aligned feature panel for end-to-end portfolio learning."""

    dates: pd.DatetimeIndex
    assets: list[str]

    x: np.ndarray  # (T, N, F)
    r_fwd1: np.ndarray  # (T, N) forward raw returns
    sigma_hat: np.ndarray  # (T, N) ex-ante volatility
    vol_scale: np.ndarray  # (T, N) = 1 / (sigma_hat + eps)
    y_fwd1: np.ndarray  # (T, N) vol-scaled forward return
    mask: np.ndarray  # (T, N) availability mask

    feature_names: list[str]


def _robust_clip(
    df: pd.DataFrame,
    *,
    window: int,
    k: float,
    mad_scale: float,
) -> pd.DataFrame:
    """Robust, no-lookahead clipping using rolling median and MAD."""
    median = df.rolling(window=window, min_periods=window).median()
    mad = (df - median).abs().rolling(window=window, min_periods=window).median()
    bound = k * mad_scale * mad
    return df.clip(lower=median - bound, upper=median + bound)


def compute_ex_ante_volatility(
    daily_returns: pd.DataFrame,
    *,
    span: int,
    min_periods: int | None = None,
) -> pd.DataFrame:
    """EWMA volatility estimate (sigma_hat) from daily returns."""
    mp = span if min_periods is None else min_periods
    return daily_returns.ewm(span=span, adjust=False, min_periods=mp).std(bias=False)


def build_feature_panel(
    prices: pd.DataFrame,
    cfg: FeatureConfig,
) -> FeaturePanel:
    """Compute DeePM-style features, returns, vol scaling, and masks.

    Parameters
    ----------
    prices:
        Wide DataFrame of close prices (dates x assets). NaNs for missing.

    cfg:
        Feature engineering configuration.
    """
    if not isinstance(prices.index, pd.DatetimeIndex):
        raise TypeError("prices index must be a pandas.DatetimeIndex")
    prices = prices.sort_index()
    assets = [str(c) for c in prices.columns]

    existence = prices.notna().astype(np.float32)
    prices_ffill = prices.ffill()

    r_daily = prices_ffill.pct_change()
    valid_r_daily = existence.astype(bool) & existence.shift(1).fillna(False).astype(bool)
    r_daily = r_daily.where(valid_r_daily)

    sigma_hat = compute_ex_ante_volatility(r_daily, span=cfg.vol_span)
    vol_scale = 1.0 / (sigma_hat + cfg.vol_eps)

    r_fwd1 = prices_ffill.pct_change().shift(-1)
    valid_trade = existence.astype(bool) & existence.shift(-1).fillna(False).astype(bool)
    r_fwd1 = r_fwd1.where(valid_trade)

    y_fwd1 = vol_scale * r_fwd1

    # Build feature blocks
    feature_dfs: list[tuple[str, pd.DataFrame]] = []

    if cfg.feature_set == "signal":
        horizons: Sequence[int] = cfg.ret_horizons
    elif cfg.feature_set == "raw_momentum":
        horizons = tuple(range(1, int(cfg.raw_momentum_max_horizon) + 1))
    else:
        horizons = cfg.ret_horizons

    for h in horizons:
        ret_h = prices_ffill / prices_ffill.shift(h) - 1.0
        scaled = ret_h / (sigma_hat * np.sqrt(float(h)) + cfg.vol_eps)
        feature_dfs.append((f"ret_h{h}", scaled))

    if cfg.feature_set == "signal":
        for s, l in cfg.macd_pairs:
            ewm_fast = prices_ffill.ewm(span=s, adjust=False, min_periods=s).mean()
            ewm_slow = prices_ffill.ewm(span=l, adjust=False, min_periods=l).mean()
            macd_raw = (ewm_fast - ewm_slow) / (sigma_hat + cfg.vol_eps)
            macd_std = macd_raw.rolling(
                window=cfg.macd_renorm_window, min_periods=cfg.macd_renorm_window
            ).std()
            macd = macd_raw / (macd_std + cfg.vol_eps)
            feature_dfs.append((f"macd_{s}_{l}", macd))

    log_prices = np.log(prices_ffill)
    for w in cfg.zscore_windows:
        mu = log_prices.rolling(window=w, min_periods=w).mean()
        sd = log_prices.rolling(window=w, min_periods=w).std()
        z = (log_prices - mu) / (sd + cfg.vol_eps)
        feature_dfs.append((f"zscore_{w}", z))

    # Robust clipping
    clipped_features: list[np.ndarray] = []
    feature_names: list[str] = []

    for name, df in feature_dfs:
        df_clipped = _robust_clip(
            df, window=cfg.clip_window, k=cfg.clip_k, mad_scale=cfg.clip_mad_scale
        )
        df_filled = df_clipped.fillna(0.0)
        clipped_features.append(df_filled.to_numpy(dtype=np.float32))
        feature_names.append(name)

    x = (
        np.stack(clipped_features, axis=-1)
        if clipped_features
        else np.zeros((len(prices), len(assets), 0))
    )

    if cfg.include_existence:
        x = np.concatenate([x, existence.to_numpy(dtype=np.float32)[..., None]], axis=-1)
        feature_names.append("existence")

    mask = existence.to_numpy(dtype=np.float32)

    return FeaturePanel(
        dates=prices.index,
        assets=assets,
        x=x,
        r_fwd1=r_fwd1.fillna(0.0).to_numpy(dtype=np.float32),
        sigma_hat=sigma_hat.fillna(0.0).to_numpy(dtype=np.float32),
        vol_scale=vol_scale.fillna(0.0).to_numpy(dtype=np.float32),
        y_fwd1=y_fwd1.fillna(0.0).to_numpy(dtype=np.float32),
        mask=mask,
        feature_names=feature_names,
    )

```

在遵守原作品许可的前提下,附作者信息全文展示。 许可协议: MIT

此摘要由 Stratmill 研究智能体根据原文撰写,并非原文副本。