欢迎加入专注于财经数据与量化投研的【数据科学实战】知识星球!在这里,您将获取持续更新的《财经数据宝典》和《量化投研宝典》,这两部宝典相辅相成,为您在量化投研道路上提供明确指引。 我们提供了精选的国内外量化投研的 220+ 篇高质量文章,并每日更新最新研究成果,涵盖策略开发、因子分析、风险管理等核心领域。 无论您是量化投资新手还是经验丰富的研究者,星球社区都能帮您少走弯路,事半功倍,共同探索数据驱动的投资世界!
引言
你是否想过,为什么有些交易策略能够持续跑赢市场?时间序列动量(Time Series Momentum,简称 TSMOM)就是量化金融领域最稳健的市场异象之一。本文将带你深入理解 TSMOM 策略的核心原理,并通过 Python 代码实现一个完整的回测框架。
无论你是量化交易的初学者,还是希望提升策略开发能力的 Python 开发者,这篇文章都会给你带来实质性的收获。
什么是时间序列动量策略
TSMOM 策略的核心思想非常简洁:
与传统的横截面动量策略不同,TSMOM 只关注资产自身的历史表现,而不与其他资产进行比较。
策略架构设计
一个专业的 TSMOM 策略需要遵循严格的时间顺序,以避免前视偏差(Look-Ahead Bias):
- 在第 t 天收盘时,使用截至 t-1 天的数据生成信号
- 收益率计算公式为:R_t = Open[t+2] / Open[t+1] - 1
三种动量指标实现
1. 价格动量
最简单直接的动量计算方式:
class MomentumIndicators:
"""动量指标计算类"""
@staticmethod
def calculate_price_momentum(close_prices: pd.Series, window: int) -> pd.Series:
"""
计算价格动量信号
参数:
close_prices: 收盘价序列
window: 回看窗口期(天数)
返回:
动量信号序列(+1 做多,-1 做空,0 观望)
"""
# 计算窗口期内的收益率,shift(1) 避免前视偏差
momentum_return = close_prices.pct_change(window).shift(1)
# 生成信号:正收益做多,负收益做空
signal = np.sign(momentum_return)
return signal
2. 均线交叉策略
通过快慢均线的交叉来判断趋势方向:
@staticmethod
def calculate_sma_crossover(close_prices: pd.Series,
fast_window: int,
slow_window: int) -> pd.Series:
"""
计算均线交叉信号
信号逻辑:
- 快线上穿慢线(金叉):做多 (+1)
- 快线下穿慢线(死叉):做空 (-1)
参数:
close_prices: 收盘价序列
fast_window: 快速均线窗口
slow_window: 慢速均线窗口
返回:
交易信号序列
"""
# 计算快慢均线
fast_sma = close_prices.rolling(window=fast_window).mean().shift(1)
slow_sma = close_prices.rolling(window=slow_window).mean().shift(1)
# 生成交叉信号
signal = pd.Series(0, index=close_prices.index)
signal[fast_sma > slow_sma] = 1 # 金叉做多
signal[fast_sma < slow_sma] = -1 # 死叉做空
return signal
3. 均线斜率策略
根据均线的变化率来判断趋势强度:
@staticmethod
def calculate_sma_slope(close_prices: pd.Series,
sma_window: int,
slope_window: int = 5) -> pd.Series:
"""
计算基于均线斜率的交易信号
信号逻辑:
- 均线向上倾斜:做多 (+1)
- 均线向下倾斜:做空 (-1)
参数:
close_prices: 收盘价序列
sma_window: 均线计算窗口
slope_window: 斜率计算窗口
返回:
交易信号序列
"""
# 计算均线
sma = close_prices.rolling(window=sma_window).mean().shift(1)
# 计算斜率(变化率)
sma_slope = sma.diff(slope_window) / slope_window
# 根据斜率方向生成信号
signal = np.sign(sma_slope)
return signal
四种高级波动率估计器
波动率估计是 TSMOM 策略的核心组件之一。本文实现了四种不同的估计方法。
1. 滚动波动率(Rolling Volatility)
最经典的波动率计算方式:
class VolatilityEstimators:
"""高级波动率估计器类"""
@staticmethod
def calculate_rolling_volatility(close_prices: pd.Series, window: int) -> pd.Series:
"""
经典滚动波动率计算
使用对数收益率的标准差,并年化处理
参数:
close_prices: 收盘价序列
window: 滚动窗口期
返回:
年化波动率序列
"""
# 计算对数收益率
log_returns = np.log(close_prices / close_prices.shift(1))
# 计算滚动标准差,shift(1) 避免前视偏差
rolling_std = log_returns.rolling(window=window).std().shift(1)
# 年化波动率(假设一年 252 个交易日)
annualized_vol = rolling_std * np.sqrt(252)
return annualized_vol
2. Parkinson 波动率
利用日内最高最低价来估计波动率,效率是收盘价方法的 5.2 倍:
@staticmethod
def calculate_parkinson_volatility(high_prices: pd.Series,
low_prices: pd.Series,
window: int) -> pd.Series:
"""
Parkinson 波动率估计器(1980)
利用日内高低价范围估计波动率,比收盘价方法效率高 5 倍
参数:
high_prices: 最高价序列
low_prices: 最低价序列
window: 滚动窗口期
返回:
年化 Parkinson 波动率序列
"""
# 计算 log(最高价/最低价) 的比值
hl_ratio = np.log(high_prices / low_prices)
# Parkinson 方差公式:(1/(4*ln(2))) * mean(ln(H/L)^2)
parkinson_var = (1.0 / (4.0 * np.log(2))) * (hl_ratio ** 2)
# 滚动均值方差
rolling_var = parkinson_var.rolling(window=window).mean().shift(1)
# 转换为波动率并年化
parkinson_vol = np.sqrt(rolling_var) * np.sqrt(252)
return parkinson_vol
3. Garman-Klass 波动率
综合使用 OHLC 四个价格,效率达到收盘价方法的 7.4 倍:
@staticmethod
def calculate_garman_klass_volatility(open_prices: pd.Series,
high_prices: pd.Series,
low_prices: pd.Series,
close_prices: pd.Series,
window: int) -> pd.Series:
"""
Garman-Klass 波动率估计器(1980)
使用 OHLC 四价数据,效率是收盘价方法的 7.4 倍
参数:
open_prices: 开盘价序列
high_prices: 最高价序列
low_prices: 最低价序列
close_prices: 收盘价序列
window: 滚动窗口期
返回:
年化 Garman-Klass 波动率序列
"""
# 计算对数比值
hl_ratio = np.log(high_prices / low_prices)
co_ratio = np.log(close_prices / open_prices)
# Garman-Klass 方差公式
gk_var = 0.5 * (hl_ratio ** 2) - (2 * np.log(2) - 1) * (co_ratio ** 2)
# 滚动均值方差
rolling_var = gk_var.rolling(window=window).mean().shift(1)
# 转换为波动率并年化
gk_vol = np.sqrt(rolling_var) * np.sqrt(252)
return gk_vol
4. Yang-Zhang 波动率
最先进的估计器,效率达到收盘价方法的 14 倍,能够捕捉隔夜跳空:
@staticmethod
def calculate_yang_zhang_volatility(open_prices: pd.Series,
high_prices: pd.Series,
low_prices: pd.Series,
close_prices: pd.Series,
window: int) -> pd.Series:
"""
Yang-Zhang 波动率估计器(2000)
最先进的 OHLC 波动率估计器,综合了:
- 隔夜波动率(收盘到开盘)
- 开盘跳空波动率
- 日内漂移无关波动率(Rogers-Satchell)
效率是收盘价方法的 14 倍
参数:
open_prices: 开盘价序列
high_prices: 最高价序列
low_prices: 最低价序列
close_prices: 收盘价序列
window: 滚动窗口期
返回:
年化 Yang-Zhang 波动率序列
"""
# 隔夜收益率:昨日收盘到今日开盘
overnight_ret = np.log(open_prices / close_prices.shift(1))
# 开盘到收盘收益率
oc_ret = np.log(close_prices / open_prices)
# Rogers-Satchell 波动率组件
rs1 = np.log(high_prices / close_prices) * np.log(high_prices / open_prices)
rs2 = np.log(low_prices / close_prices) * np.log(low_prices / open_prices)
# 计算各方差组件
overnight_var = overnight_ret.rolling(window=window).var()
open_var = oc_ret.rolling(window=window).var()
rs_var = (rs1 + rs2).rolling(window=window).mean()
# 计算权重因子 k
k = 0.34 / (1.34 + (window + 1) / (window - 1))
# Yang-Zhang 方差
yz_var = overnight_var + k * open_var + (1 - k) * rs_var
yz_var = yz_var.shift(1)
# 转换为波动率并年化
yz_vol = np.sqrt(yz_var) * np.sqrt(252)
return yz_vol
仓位管理:波动率目标化
所有策略都采用波动率目标化方法来维持恒定的风险敞口(本例中为 15% 年化波动率):
def calculate_position_size(self, signal: pd.Series, realized_vol: pd.Series) -> pd.Series:
"""
基于波动率目标化计算仓位大小
仓位 = 信号 × (目标波动率 / 实现波动率)
这种方法会在高波动时期减少敞口,在低波动时期增加敞口
参数:
signal: 交易信号(+1、-1 或 0)
realized_vol: 已实现波动率
返回:
仓位大小序列(资本的比例)
"""
# 避免除零错误
realized_vol_safe = realized_vol.replace(0, np.nan)
# 计算目标仓位
position_size = signal * (self.target_volatility / realized_vol_safe)
# 限制最大仓位在 ±100% 以内
position_size = position_size.clip(-1.0, 1.0)
return position_size
完整的策略回测框架
下面是整合所有组件的策略类:
class TSMOMStrategy:
"""
时间序列动量策略类
策略逻辑:
1. 使用选定的动量指标计算交易信号
2. 使用选定的波动率估计器计算已实现波动率
3. 在 Close[t] 时生成信号
4. 在 Open[t+1] 执行交易
5. 在 Open[t+1] 对组合估值
6. 收益率 = Open[t+2]/Open[t+1] - 1
"""
def __init__(self,
momentum_type: str,
momentum_params: Dict,
volatility_type: str,
volatility_window: int,
target_volatility: float,
commission_rate: float):
"""
初始化策略
参数:
momentum_type: 动量指标类型
momentum_params: 动量参数字典
volatility_type: 波动率估计器类型
volatility_window: 波动率计算窗口
target_volatility: 目标波动率
commission_rate: 交易佣金率
"""
self.momentum_type = momentum_type
self.momentum_params = momentum_params
self.volatility_type = volatility_type
self.volatility_window = volatility_window
self.target_volatility = target_volatility
self.commission_rate = commission_rate
def
simulate_trading(self, ohlc_data: pd.DataFrame) -> pd.DataFrame:
"""
模拟交易,采用真实的执行逻辑
参数:
ohlc_data: 包含 OHLC 数据的 DataFrame
返回:
包含信号、仓位和收益的 DataFrame
"""
results = pd.DataFrame(index=ohlc_data.index)
# 第一步:计算动量信号
results["momentum_signal"] = self.calculate_momentum_signal(ohlc_data)
# 第二步:计算已实现波动率
results["realized_volatility"] = self.calculate_realized_volatility(ohlc_data)
# 第三步:计算基于波动率目标的仓位
results["position_size"] = self.calculate_position_size(
results["momentum_signal"],
results["realized_volatility"]
)
# 第四步:计算执行价格和收益
results["open"] = ohlc_data["open"]
results["close"] = ohlc_data["close"]
# 执行:Close[t] 决定的仓位在 Open[t+1] 执行
results["executed_position"] = results["position_size"].shift(1)
# 持仓期收益:Open[t+1] 到 Open[t+2]
results["open_to_open_return"] = ohlc_data["open"].pct_change()
# 策略毛收益
results["gross_return"] = results["executed_position"] * results["open_to_open_return"]
# 计算佣金(仓位变化时收取)
results["position_change"] = results["executed_position"].diff().abs()
results["commission_cost"] = results["position_change"] * self.commission_rate
# 净收益
results["net_return"] = results["gross_return"] - results["commission_cost"]
return results
实证结果
在 SPY(2020-2024)上测试 144 种不同配置后,我们发现:
表现最佳的策略组合包括:
- 动量指标:20 日滚动窗口(均线交叉 20/200)
- 波动率估计器:Yang-Zhang 和 Garman-Klass(40 日窗口)
关键发现
估计器效率差异显著
基于价格范围的估计器(Parkinson、Garman-Klass、Yang-Zhang)能够捕捉日内信息,表现出理论上的效率优势。Yang-Zhang 在存在明显跳空的市场中表现尤为突出。
最优参数组合
短期动量窗口(20 日)配合中期波动率窗口(40 日)能够在响应速度和稳定性之间取得最佳平衡。
波动率目标化的重要性
波动率缩放是 TSMOM 成功的基础,能够在危机期间减少敞口,在平静期增加敞口。
交易成本的影响
本框架纳入了每笔交易 0.1% 的佣金。长周期均线交叉策略能够最大程度减少换仓频率和交易成本。
实践建议
避免前视偏差
所有信号计算都使用 shift(1) 确保 t 时刻的信号只使用 t-1 及之前的数据。
真实的执行逻辑
策略在信号生成后的下一个开盘价执行,收益按开盘价到开盘价计算。
风险管理
仓位上限控制在资本的 ±100%,并妥善处理零波动率的除法问题。
总结
本文详细介绍了 TSMOM 策略的完整实现,包括三种动量指标和四种波动率估计器的组合。通过 144 种配置的回测,我们验证了以下结论:
- 波动率估计器很重要:基于价格范围的估计器能提升信号质量
- 实现细节至关重要:真实执行逻辑、佣金和偏差消除是基础
对于想要进入量化交易领域的 Python 开发者,TSMOM 代表了一个稳健、透明的策略框架。推荐的最优配置是均线交叉(20/200)配合 40 日 Rolling 或 Yang-Zhang 波动率估计器。
参考文章
加入专注于财经数据与量化投研的知识星球【数据科学实战】,获取本文完整研究解析、代码实现细节。财经数据与量化投研知识社区
核心权益如下:
- 赠送《财经数据宝典》完整文档,汇集多年财经数据维护经验
- 赠送《量化投研宝典》完整文档,汇集多年量化投研领域经验
- 赠送《PyBroker-入门及实战》视频课程,手把手学习量化策略开发
- 每日分享高质量量化投研文章(已更新 180+篇)、代码和相关资料
星球已有丰富内容积累,包括量化投研论文、财经高频数据、 PyBroker 视频教程、定期直播、数据分享和答疑解难。适合对量化投研和财经数据分析有兴趣的学习者及从业者。欢迎加入我们!