Momentum(横截面与时序动量)
横截面动量(XSMOM)、时序动量(TSMOM)、Moskowitz-Ooi-Pedersen 框架、动量崩溃
日期: 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:
- 方向:过去 L 期收益的符号(L 通常 = 1, 3, 12 月)
- 波动率目标化:仓位反比于 realized vol
- 多 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 必然过拟合。 解法:
- 集成多个 lookback(1m + 3m + 6m)
- 样本外验证
- 用 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 历史表现
| 时段 | Sharpe | Max DD | 备注 |
|---|---|---|---|
| 2014-2017 | 2.1 | -22% | 黄金时代 |
| 2018-2019 | 0.8 | -38% | 熊市信号衰减 |
| 2020-2021 | 1.5 | -25% | DeFi summer 重新有效 |
| 2022 | -0.6 | -45% | LUNA/FTX 撕碎趋势 |
| 2023-2024 | 0.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-cost | 5-10 bps round-trip | 30-100 bps(gas + slippage) |
| 可投容量 | $10M-$1B | $100K-$10M(取决于深度) |
| DeFi 特有 | — | 新代币上市动量(DEX 有但 CEX 还没上) |
DeFi 原生动量机会:
- 新代币 IDO/IFO 后:流动性建立后 7-14 天动量明显
- TVL 增长率动量:协议 TVL 7-day MoM 增长率与代币价格相关
- 链上活跃地址动量:日活地址增长率作为信号
六、风险管理
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 建议(加密)
| 信号 | Lookback | Skip | Holding |
|---|---|---|---|
| 短期 | 7-14d | 0-2d | 3-7d |
| 中期 | 30-60d | 5-7d | 14-30d |
| 长期 | 90-180d | 7d | 30-90d |
集成权重
ensemble = 0.3 * tsmom(30) + 0.4 * tsmom(60) + 0.3 * tsmom(90)
EWMA λ 参数
| 频率 | λ | 半衰期 |
|---|---|---|
| 日 | 0.94 | 11d |
| 4h | 0.97 | 23 个 4h |
| 1h | 0.99 | 69h |
八、面试题
Q1:为什么动量在加密比在股票更"强"但也更危险?
答:
- 更强:加密散户多、信息扩散慢、媒体放大效应、监管套利空间——典型行为金融特征助长趋势
- 更危险:
- vol 是股票 4-5 倍,崩溃幅度按比例放大
- 流动性脆弱,反转时滑点剧增
- regime change 频繁(FTX/LUNA/SVB)
- 应对:vol targeting + 趋势过滤 + drawdown 控制是必需,而不是可选
Q2:你怎么防止 TSMOM 过拟合?
答:
- 多 lookback 集成:30+60+90 等权,避免 single magic number
- Walk-forward:每 6 个月用截至当时的数据重新选参数
- Prediction R² 检验:信号与未来收益的样本外 R² > 0.5%
- 跨市场验证:BTC 上有效的策略也应在 ETH/SOL 上有效(不同程度)
- Bonferroni 校正:测试 K 个 lookback,p 值要 / K
- 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 研究。