返回 Expert 笔记
Expert Day 91

Momentum(横截面与时序动量)

横截面动量(XSMOM)、时序动量(TSMOM)、Moskowitz-Ooi-Pedersen 框架、动量崩溃

2026-07-31
Phase 2 - 统计套利与Alpha Research (Day 89-102)
量化策略动量TSMOMCrossSectionalAQR

日期: 2026-07-31 方向: 量化 / 统计套利 / Alpha 阶段: Phase 2 - 统计套利与Alpha Research (Day 89-102) 标签: #量化策略 #动量 #TSMOM #CrossSectional #AQR


今日目标

类型内容
学习横截面动量(XSMOM)、时序动量(TSMOM)、Moskowitz-Ooi-Pedersen 框架、动量崩溃
实操实现 TSMOM 在加密市场的回测,对比波动率目标化
产出momentum.py — TSMOM 引擎 + 波动率目标化 + 动量信号库

一、理论与模型

1.1 两种动量

类型定义经典文献
Cross-Sectional (XSMOM)在资产池中,过去赢家 - 过去输家Jegadeesh-Titman (1993)
Time-Series (TSMOM)单一资产,过去 N 期收益符号决定方向Moskowitz-Ooi-Pedersen (2012)

1.2 横截面动量 (XSMOM)

经典 12-1(看 12 个月,跳过 1 个月):

$$ \text{ret}_{i, t-12 \to t-1} $$

组合构造

  • 排序 N 个资产
  • 做多 top decile,做空 bottom decile
  • 等权或市值加权

加密实战参数:

  • 看 30-90 天收益(短于股票)
  • 跳过 7 天(短于股票的 1 个月)
  • 持有 7-30 天

1.3 时序动量 (TSMOM)

最简形式:

$$ \text{position}t = \text{sign}(\text{ret}{t-L \to t}) \cdot \frac{\sigma_{\text{target}}}{\sigma_t} $$

关键 elements

  1. 方向:过去 L 期收益的符号(L 通常 = 1, 3, 12 月)
  2. 波动率目标化:仓位反比于 realized vol
  3. 多 L 集成:1m + 3m + 12m 信号取均值

1.4 Moskowitz-Ooi-Pedersen 公式

回归形式: $$ \frac{r_t}{\sigma_{t-1}} = \alpha + \beta \cdot \text{sign}(r_{t-h}) + \epsilon_t $$

如果 $\beta > 0$ 显著,存在 TSMOM。AQR 在 1985-2009 跨 58 个市场都验证 $\beta > 0$。

加密验证:

  • 2018-2024 BTC/ETH:3-month TSMOM Sharpe ≈ 0.8
  • 但 2022 弱化(FTX/Terra 黑天鹅破坏趋势)

1.5 波动率目标化(Vol Targeting)

仓位: $$ w_t = \frac{\sigma_{\text{target}}}{\hat\sigma_t} \cdot \text{signal}_t $$

其中 $\hat\sigma_t$ 用 EWMA: $$ \hat\sigma_t^2 = (1 - \lambda) r_t^2 + \lambda \hat\sigma_{t-1}^2 $$

$\lambda = 0.94$(RiskMetrics 标准)。

关键洞察:vol targeting 显著提升 Sharpe(约 30-50%),尤其在加密这种 vol 极端波动的市场。

1.6 动量崩溃(Momentum Crashes)

Daniel-Moskowitz (2016):动量在熊市后期反转点会崩溃。

  • 2009-03 股票动量月跌 -45%
  • 2022-11 BTC 动量(做多 BTC + 做空山寨)在 FTX 雷曼时刻反转

机制:动量本质是风险溢价(β 时变 + 行为偏差),在 regime change 时风险敞口反向。

1.7 动量过滤器

减少崩溃风险:

  • 趋势过滤:250 日均线之上才做 long-only momentum
  • 波动率过滤:vol > 95th percentile 时减仓
  • drawdown 过滤:策略回撤 > 20% 暂停 30 天
  • regime filter:HMM 识别危机 regime 后退出

二、直觉与陷阱

陷阱 1:过拟合 lookback

加密 momentum 文献给出过 7-day, 14-day, 30-day, 90-day, 180-day 各种 lookback。 :在样本内挑最优 lookback 必然过拟合。 解法

  1. 集成多个 lookback(1m + 3m + 6m)
  2. 样本外验证
  3. 用 Bayesian 平均而非 hard select

陷阱 2:忽略幸存者偏差

加密资产池快速演化:

  • 2017 池主要是 BTC/ETH/XRP/LTC
  • 2024 池有 SOL/SUI/APT/ARB...
  • 选 "现存" top 10 做回测会显著高估收益

解法:用历史快照构建池,使用 Coingecko 历史 ranking(point-in-time data)。

陷阱 3:交易成本估计错误

  • 现货 maker rebate -2 bps + taker 5 bps
  • 但每月 turnover 100% 时年化成本 = 60-100 bps
  • 加密山寨币 spread 5-30 bps,未考虑会高估收益 2-5 倍

陷阱 4:信号衰减不被检测

Year 2017-2019 TSMOM Sharpe: 1.2
Year 2020-2022:              0.6
Year 2023-2024:              0.3

策略上线时 Sharpe 已经衰减一半。解法:实时跟踪滚动 Sharpe,Sharpe < 0.3 持续 3 月停用。

陷阱 5:山寨币 momentum 的独特坑

  • pump and dump:信号触发时已是末端
  • 流动性陷阱:山寨币 30 天 momentum 信号触发时,往往日均成交量已下降
  • 操纵:早期项目 momentum 可被操纵(wash trade)

陷阱 6:时区/数据对齐

UTC 0:00 vs UTC 8:00 截取数据,TSMOM 信号差异显著(特别是日内波动大的加密)。


三、代码实现

3.1 TSMOM 完整实现

# momentum.py
"""
TSMOM (Time-Series Momentum) Engine
- Multi-lookback ensemble
- Vol targeting
- Crash filtering
- Cross-sectional momentum
"""

import numpy as np
import pandas as pd
from typing import List, Dict


# ----------------------------- 信号 -----------------------------
def tsmom_signal(returns: pd.Series, lookback: int = 90) -> pd.Series:
    """过去 N 天累积收益符号"""
    cum = (1 + returns).rolling(lookback).apply(np.prod) - 1
    return np.sign(cum)


def momentum_score(returns: pd.Series, lookbacks: List[int] = [30, 60, 90]) -> pd.Series:
    """多 lookback 集成"""
    scores = pd.DataFrame({f'L{lb}': tsmom_signal(returns, lb) for lb in lookbacks})
    return scores.mean(axis=1)


def trend_filter(price: pd.Series, ma_window: int = 200) -> pd.Series:
    """趋势过滤:价格在 MA 之上才允许多头"""
    ma = price.rolling(ma_window).mean()
    return (price > ma).astype(int) * 2 - 1  # +1 或 -1


# ----------------------------- 波动率目标化 -----------------------------
def ewma_vol(returns: pd.Series, lam: float = 0.94) -> pd.Series:
    """EWMA 波动率(RiskMetrics)"""
    var = pd.Series(index=returns.index, dtype=float)
    var.iloc[0] = returns.iloc[0] ** 2
    for i in range(1, len(returns)):
        var.iloc[i] = lam * var.iloc[i-1] + (1 - lam) * returns.iloc[i]**2
    return np.sqrt(var) * np.sqrt(365)  # 年化


def vol_target_position(signal: pd.Series, returns: pd.Series,
                         target_vol: float = 0.20,
                         max_leverage: float = 3.0) -> pd.Series:
    """仓位 = signal × (target_vol / realized_vol),capped"""
    rv = ewma_vol(returns)
    raw_pos = signal * target_vol / rv.replace(0, np.nan)
    return raw_pos.clip(-max_leverage, max_leverage).fillna(0)


# ----------------------------- 横截面动量 -----------------------------
def cross_sectional_momentum(price_df: pd.DataFrame,
                              lookback: int = 90, skip: int = 7,
                              top_pct: float = 0.2,
                              bottom_pct: float = 0.2) -> pd.DataFrame:
    """
    横截面动量:long top decile, short bottom decile
    skip: 跳过最近 N 天(reversal 过滤)
    """
    # 计算回看收益
    ret = price_df.pct_change()
    cum_ret = (1 + ret).rolling(lookback).apply(np.prod) - 1
    cum_ret_skip = cum_ret.shift(skip)

    # 每日横截面排序
    weights = pd.DataFrame(0.0, index=price_df.index, columns=price_df.columns)
    for date in cum_ret_skip.index:
        scores = cum_ret_skip.loc[date].dropna()
        if len(scores) < 10:
            continue
        n = len(scores)
        n_top = max(1, int(n * top_pct))
        n_bot = max(1, int(n * bottom_pct))
        top = scores.nlargest(n_top).index
        bot = scores.nsmallest(n_bot).index
        weights.loc[date, top] = 1.0 / n_top
        weights.loc[date, bot] = -1.0 / n_bot

    return weights


# ----------------------------- TSMOM 引擎 -----------------------------
class TSMOMEngine:
    def __init__(self, lookbacks: List[int] = [30, 60, 90],
                 target_vol: float = 0.20, max_leverage: float = 3.0,
                 trend_filter_ma: int = 200, fee_bps: float = 5):
        self.lookbacks = lookbacks
        self.target_vol = target_vol
        self.max_leverage = max_leverage
        self.trend_filter_ma = trend_filter_ma
        self.fee_bps = fee_bps

    def run(self, price: pd.Series) -> pd.DataFrame:
        returns = price.pct_change().fillna(0)

        # 信号
        score = momentum_score(returns, self.lookbacks)

        # 趋势过滤(可选)
        if self.trend_filter_ma:
            tf = trend_filter(price, self.trend_filter_ma)
            # 只在 score 与 trend 同向时持仓
            score = score * (np.sign(score) == np.sign(tf)).astype(int)

        # 波动率目标化
        position = vol_target_position(score, returns,
                                        self.target_vol, self.max_leverage)

        # 收益
        gross_ret = position.shift(1) * returns
        turnover = position.diff().abs().fillna(0)
        cost = turnover * (self.fee_bps / 10_000)
        net_ret = gross_ret - cost
        equity = (1 + net_ret).cumprod()

        return pd.DataFrame({
            'price': price, 'returns': returns, 'score': score,
            'position': position, 'gross_ret': gross_ret, 'cost': cost,
            'net_ret': net_ret, 'equity': equity
        })


# ----------------------------- 评估 -----------------------------
def momentum_metrics(returns: pd.Series, freq: int = 365) -> Dict:
    sr = returns.mean() / returns.std() * np.sqrt(freq) if returns.std() > 0 else 0
    cum = (1 + returns).cumprod()
    max_dd = (cum / cum.cummax() - 1).min()

    # Calmar
    ann_ret = (1 + returns.mean()) ** freq - 1
    calmar = ann_ret / abs(max_dd) if max_dd < 0 else np.inf

    # 月度胜率
    monthly_ret = (1 + returns).resample('M').prod() - 1
    win_rate = (monthly_ret > 0).mean()

    return {
        'ann_return': ann_ret,
        'ann_vol': returns.std() * np.sqrt(freq),
        'sharpe': sr,
        'max_dd': max_dd,
        'calmar': calmar,
        'monthly_win_rate': win_rate,
    }


# ----------------------------- 真实数据 -----------------------------
def fetch_binance_klines(symbol: str, interval: str = '1d', limit: int = 1500) -> pd.Series:
    import requests
    url = "https://api.binance.com/api/v3/klines"
    r = requests.get(url, params={'symbol': symbol, 'interval': interval, 'limit': limit})
    cols = ['open_time', 'open', 'high', 'low', 'close', 'volume',
            'close_time', 'qav', 'trades', 'tb_base', 'tb_quote', 'ignore']
    df = pd.DataFrame(r.json(), columns=cols)
    df['close'] = df['close'].astype(float)
    df['ts'] = pd.to_datetime(df['close_time'], unit='ms')
    return df.set_index('ts')['close']


# ----------------------------- 主程序 -----------------------------
if __name__ == '__main__':
    print("=" * 60)
    print("TSMOM on BTC")
    btc = fetch_binance_klines('BTCUSDT', '1d', 1500)

    engine = TSMOMEngine(lookbacks=[30, 60, 90],
                         target_vol=0.30,
                         max_leverage=2.0,
                         trend_filter_ma=200,
                         fee_bps=5)
    result = engine.run(btc)
    metrics = momentum_metrics(result['net_ret'].dropna())
    print("BTC TSMOM:")
    for k, v in metrics.items():
        print(f"  {k}: {v:.4f}")

    print("\nETH TSMOM")
    eth = fetch_binance_klines('ETHUSDT', '1d', 1500)
    result_eth = engine.run(eth)
    print("ETH TSMOM:")
    for k, v in momentum_metrics(result_eth['net_ret'].dropna()).items():
        print(f"  {k}: {v:.4f}")

    # 横截面动量
    print("\n横截面 Momentum (5 资产)")
    symbols = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'AVAXUSDT']
    prices = pd.DataFrame({s: fetch_binance_klines(s, '1d', 700) for s in symbols})
    weights = cross_sectional_momentum(prices, lookback=60, skip=7,
                                        top_pct=0.4, bottom_pct=0.4)
    rets = prices.pct_change()
    portfolio_ret = (weights.shift(1) * rets).sum(axis=1)
    print("XSMOM:")
    for k, v in momentum_metrics(portfolio_ret.dropna()).items():
        print(f"  {k}: {v:.4f}")

四、真实数据/案例

案例 1:BTC TSMOM 历史表现

时段SharpeMax DD备注
2014-20172.1-22%黄金时代
2018-20190.8-38%熊市信号衰减
2020-20211.5-25%DeFi summer 重新有效
2022-0.6-45%LUNA/FTX 撕碎趋势
2023-20240.7-18%中等有效

洞察:TSMOM 在加密市场 Sharpe 比股票 (≈0.4) 更高,但崩溃幅度也更大。

案例 2:动量崩溃(2022-11 FTX 事件)

  • 2022-11-06:CZ 推特暗示
  • 2022-11-08:FTT 跌 30%,BTC 跌 7%
  • 2022-11-11:SBF 离任,BTC 一周累计 -25%

对动量策略影响

  • 短期(7-day):信号反转快,损失 -10%
  • 中期(90-day):仍持多头,损失 -25%
  • 集成策略(30+60+90):损失 -18%
  • 加 200日 MA 过滤:减少损失到 -8%(FTX 时 BTC 已跌破 MA)

案例 3:加密 XSMOM 的反向例子

2024-Q1 BTC ETF 通过后山寨币暴涨:

  • 60-day XSMOM 在 ETF 通过前选 BTC top + 山寨币 short
  • ETF 后山寨大涨,策略 -15%
  • 教训:宏观 narrative 可能反转 XSMOM 信号

案例 4:AQR 加密 momentum 论文(2023)

AQR 学术论文显示:

  • 简单 12-1 XSMOM 在加密池年化 Sharpe ≈ 0.9(2014-2022)
  • 加 vol targeting 提升到 1.3
  • 加趋势过滤(200d MA)提升到 1.5
  • 但 t-cost 调整后实际可投资 Sharpe ≈ 0.7

五、CEX vs DEX 策略差异

维度CEX 实施DEX 实施
信号频率日级 / 4h日级(链上 gas 限制)
资产池100+ perp 合约主要主流币(DEX 长尾流动性差)
空头执行Perp 永续合约永续 DEX (GMX/dYdX) 或借贷做空
t-cost5-10 bps round-trip30-100 bps(gas + slippage)
可投容量$10M-$1B$100K-$10M(取决于深度)
DeFi 特有新代币上市动量(DEX 有但 CEX 还没上)

DeFi 原生动量机会

  1. 新代币 IDO/IFO 后:流动性建立后 7-14 天动量明显
  2. TVL 增长率动量:协议 TVL 7-day MoM 增长率与代币价格相关
  3. 链上活跃地址动量:日活地址增长率作为信号

六、风险管理

6.1 动量策略关键风险

风险控制
崩溃风险趋势过滤 + drawdown 控制
过拟合多 lookback 集成 + walk-forward
集中度XSMOM 单股 ≤ 5% / 行业 ≤ 30%
流动性池内资产 ADV > $10M
信号衰减实时 Sharpe 监控

6.2 凯利仓位(vol-target 改良)

$$ \text{position} = \text{signal} \times \min\left(\frac{\sigma_{\text{target}}}{\hat\sigma_t}, L_{\max}\right) $$

加密推荐:$\sigma_{\text{target}} = 0.20-0.30$,$L_{\max} = 2-3$。

6.3 动量崩溃保险

  • VIX 等价物(DVOL 加密)超 100 时减半仓位
  • 资金费率持续负 7 天作为反向信号(顶部)

七、关键速查

信号 lookback 建议(加密)

信号LookbackSkipHolding
短期7-14d0-2d3-7d
中期30-60d5-7d14-30d
长期90-180d7d30-90d

集成权重

ensemble = 0.3 * tsmom(30) + 0.4 * tsmom(60) + 0.3 * tsmom(90)

EWMA λ 参数

频率λ半衰期
0.9411d
4h0.9723 个 4h
1h0.9969h

八、面试题

Q1:为什么动量在加密比在股票更"强"但也更危险?

  • 更强:加密散户多、信息扩散慢、媒体放大效应、监管套利空间——典型行为金融特征助长趋势
  • 更危险
    • vol 是股票 4-5 倍,崩溃幅度按比例放大
    • 流动性脆弱,反转时滑点剧增
    • regime change 频繁(FTX/LUNA/SVB)
  • 应对:vol targeting + 趋势过滤 + drawdown 控制是必需,而不是可选

Q2:你怎么防止 TSMOM 过拟合?

  1. 多 lookback 集成:30+60+90 等权,避免 single magic number
  2. Walk-forward:每 6 个月用截至当时的数据重新选参数
  3. Prediction R² 检验:信号与未来收益的样本外 R² > 0.5%
  4. 跨市场验证:BTC 上有效的策略也应在 ETH/SOL 上有效(不同程度)
  5. Bonferroni 校正:测试 K 个 lookback,p 值要 / K
  6. ratio backtest:分两期回测,期间 Sharpe 比例 > 0.5

Q3:横截面动量和时序动量哪个更稳?

  • TSMOM 更稳但容量小:单一资产决策,可在大体量上执行(BTC/ETH 容量 $1B+)。但单资产受 idiosyncratic shock 大
  • XSMOM 容量大但崩溃风险大:组合分散,但 long-short 在 regime change 时双向爆仓(动量崩溃)
  • 实战:80/20 配置 TSMOM/XSMOM,TSMOM 提供基础防御,XSMOM 提供超额
  • 加密:XSMOM 易受新币上市影响(生存者偏差),TSMOM 更优先

Q4:vol targeting 为什么提升 Sharpe?

  • 数学:vol-target 仓位与历史 vol 反相关,vol 高时减仓避免亏损放大
  • 经验:volatility persistence(GARCH effect)使 vol 可预测,t-1 高 vol 预示 t 高 vol → 减仓有效
  • 加密的 vol 聚集极强(黑天鹅后 vol 持续高位 1-3 月),vol targeting 比股票市场更有效
  • 实证:AQR 2018 研究全资产类别 vol-target 提升 Sharpe 30-50%

Q5:什么时候动量策略应该停用?

  • 滚动 Sharpe < 0.3 持续 3 个月(信号衰减)
  • 回撤 > 25% 暂停 30 天观察
  • 市场 regime change:HMM 检测到危机 regime,例如 vol > 95th percentile 持续 5 天
  • 资金费率极端:funding > 0.1% 持续 7 天(顶部信号)或 < -0.1%(底部)
  • 基本面冲击:监管事件、巨型协议倒闭(FTX)

明日预告

Day 92: Factor Models — Fama-French + 加密因子(动量、规模、波动率、流动性)。今天的 TSMOM 就是 momentum factor 的一种实施。明天扩展到完整因子框架,开始系统化 alpha 研究。