Skip to content

用Python轻松搞定时间序列动量策略

从入门到精通,老余带你拆解TSMOM策略核心

作者:老余捞鱼

原创不易,转载请标明出处及原作者。

写在前面的话:今天给大家带来一个超级实用的量化策略——时间序列动量策略(TSMOM)。这个策略简单到令人发指,但效果却好得惊人。我用Python手把手教你实现,从数据获取到信号生成,再到风险控制,一条龙服务。

不管你是量化小白还是老手,这篇文章都能让你有所收获。记得看到最后,有完整的代码送给你!

01 什么是时间序列动量策略

时间序列动量策略(TSMOM)是量化交易领域中最简单却最有效的策略之一。它的核心逻辑很简单:如果一只股票过去一段时间在涨,我们就买它;如果在跌,我们就卖它。

策略核心公式

信号 = sign(过去N天收益率)

当收益率 > 0时,信号 = +1(买入);当收益率 < 0时,信号 = -1(卖出)

这个策略不需要复杂的数学模型,只需要看资产自己的历史表现。Moskowitz、Ooi和Pedersen在2012年的研究发现,这个策略在全球各大资产类别都能赚钱,包括股票、债券、商品和外汇。

02 为什么动量策略能赚钱

动量策略能赚钱,主要是因为市场参与者的行为偏差。让我们看看具体的原因:

保守性偏差

大家对新信息反应太慢,导致价格趋势会持续一段时间。投资者倾向于坚持自己原有的观点,不愿意快速调整。

正反馈交易

看到股票涨就追高,看到跌就割肉,这种行为会强化趋势。追涨杀跌是人性,也是动量策略的盈利来源。

处置效应

人们太早就卖出赚钱的股票,却死抱着亏钱的股票不放。这种行为模式为动量策略创造了持续的盈利机会。

信息传播延迟

新信息在市场中的传播需要时间,不是所有投资者都能立即获得并处理信息,这造成了价格的持续趋势。

关键洞察

这些行为偏差在金融市场中普遍存在,为动量策略提供了持续的盈利机会。研究表明,动量效应在3-12个月的时间范围内最为显著,这正是我们策略的最佳持有期。

03 三种动量信号生成方法

在实际操作中,我们有三种主要的动量信号生成方法。每种方法都有其特点和适用场景:

方法1 价格动量(最简单)


计算公式

动量 = (今天收盘价 ÷ N天前收盘价) – 1

信号规则

  • 动量 > 0 → 买入信号 (+1)
  • 动量 < 0 → 卖出信号 (-1)

常用参数:N = 20, 60, 120天

优点:简单直观,计算快速

缺点:容易受噪声影响

方法2 均线交叉(最常用)


信号规则

  • 金叉(短期均线上穿长期均线)→ 买入
  • 死叉(短期均线下穿长期均线)→ 卖出

经典组合

  • • 20天均线 + 50天均线
  • • 20天均线 + 200天均线
  • • 50天均线 + 200天均线

优点:趋势识别准确,噪声过滤效果好

缺点:信号滞后,在震荡市中表现不佳

方法3 均线斜率(最灵敏)


计算公式

斜率 = (今天均线值 – N天前均线值) ÷ N

信号规则

  • 斜率 > 0 → 买入信号
  • 斜率 < 0 → 卖出信号

优点:反应灵敏,能提前捕捉趋势变化

缺点:容易产生假信号

实战建议

根据老余的实战经验,建议新手从均线交叉开始,因为它在趋势识别和噪声过滤之间取得了很好的平衡。等熟悉了再尝试其他方法。

04 波动率估计器详解

波动率估计是动量策略成功的关键。好的波动率估计能让我们的策略在不同市场环境下保持稳定的风险水平。下面介绍四种主流的波动率估计器:

基础 滚动波动率

σ = std(收益率) × √252

最简单的波动率估计方法,只使用收盘价数据。优点是计算简单,缺点是对价格跳空不敏感。

进阶 帕金森波动率

σ = √[1/(4ln2) × mean((ln(H/L))²)] × √252

使用最高价和最低价,效率比收盘价方法高5.2倍。能更好地捕捉日内波动。

高级 加曼-克拉斯波动率

σ = √[0.5(ln(H/L))² – (2ln2-1)(ln(C/O))²] × √252

结合OHLC四个价格,效率比收盘价方法高7.4倍。能同时捕捉日内波动和开盘跳空。

顶级 杨-张波动率

σ² = σ_overnight² + k×σ_open² + (1-k)×σ_RS²

最复杂的估计器,效率比收盘价方法高14倍。能处理隔夜跳空和日内波动,是最佳选择。

波动率估计器效率对比分析

波动率分析图表

 杨-张波动率估计器在处理复杂市场环境时表现最佳,效率是滚动波动率的14倍

老余推荐

根据大量实证研究杨-张波动率估计器是最佳选择,特别是在处理有显著隔夜跳空的资产时。虽然计算复杂,但准确度最高。

实战建议:

  • • 新手:从滚动波动率开始,简单易懂
  • • 进阶:使用帕金森或加曼-克拉斯
  • • 专业:直接使用杨-张波动率

05 Python实战代码

终于到了大家最期待的环节!老余手把手教你用Python实现完整的时间序列动量策略。代码都有详细注释,保证你能看懂。

策略表现对比分析

 均线交叉策略在趋势识别和风险控制之间取得了最佳平衡


第一步 数据准备与获取

import pandas as pd
import numpy as np
import yfinance as yf
from datetime import datetime, timedelta
import matplotlib.pyplot as plt

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 1. 获取股票数据
def get_stock_data(symbol, period='2y'):
    """
    获取股票历史数据
    symbol: 股票代码,如'AAPL', 'SPY'
    period: 时间周期,默认2年
    """
    stock = yf.Ticker(symbol)
    df = stock.history(period=period)
    return df

# 获取SPY数据作为示例
spy_data = get_stock_data('SPY')
print(f"数据形状: {spy_data.shape}")
print(f"时间范围: {spy_data.index[0]} 到 {spy_data.index[-1]}")
print(spy_data.head())

第二步 动量信号生成

class MomentumStrategy:
    def __init__(self, data, momentum_window=20, volatility_window=40):
        self.data = data.copy()
        self.momentum_window = momentum_window
        self.volatility_window = volatility_window
        self.signals = pd.DataFrame(index=data.index)
        
    def calculate_price_momentum(self):
        """计算价格动量"""
        # 计算过去N天的收益率
        self.signals['momentum'] = (self.data['Close'] / 
                                   self.data['Close'].shift(self.momentum_window)) - 1
        # 生成信号
        self.signals['signal'] = np.where(self.signals['momentum'] > 0, 1, -1)
        return self.signals['signal']
    
    def calculate_sma_crossover(self, short_window=20, long_window=50):
        """计算均线交叉信号"""
        # 计算移动平均线
        self.signals['sma_short'] = self.data['Close'].rolling(window=short_window).mean()
        self.signals['sma_long'] = self.data['Close'].rolling(window=long_window).mean()
        # 生成信号
        self.signals['signal'] = np.where(self.signals['sma_short'] > self.signals['sma_long'], 1, -1)
        return self.signals['signal']
    
    def calculate_sma_slope(self, ma_window=20, slope_window=5):
        """计算均线斜率信号"""
        # 计算移动平均线
        self.signals['sma'] = self.data['Close'].rolling(window=ma_window).mean()
        # 计算斜率
        self.signals['sma_slope'] = (self.signals['sma'] - 
                                    self.signals['sma'].shift(slope_window)) / slope_window
        # 生成信号
        self.signals['signal'] = np.where(self.signals['sma_slope'] > 0, 1, -1)
        return self.signals['signal']

# 创建策略实例
strategy = MomentumStrategy(spy_data)

# 生成不同类型的信号
price_momentum_signal = strategy.calculate_price_momentum()
sma_crossover_signal = strategy.calculate_sma_crossover()
sma_slope_signal = strategy.calculate_sma_slope()

print("价格动量信号统计:")
print(price_momentum_signal.value_counts())

第三步 波动率估计

class VolatilityEstimator:
    def __init__(self, data):
        self.data = data.copy()
        
    def rolling_volatility(self, window=40):
        """滚动波动率"""
        returns = np.log(self.data['Close'] / self.data['Close'].shift(1))
        return returns.rolling(window=window).std() * np.sqrt(252)
    
    def parkinson_volatility(self, window=40):
        """帕金森波动率"""
        log_hl = np.log(self.data['High'] / self.data['Low'])
        volatility = np.sqrt(1/(4*np.log(2)) * (log_hl**2))
        return volatility.rolling(window=window).mean() * np.sqrt(252)
    
    def garman_klass_volatility(self, window=40):
        """加曼-克拉斯波动率"""
        log_hl = np.log(self.data['High'] / self.data['Low'])
        log_co = np.log(self.data['Close'] / self.data['Open'])
        
        term1 = 0.5 * (log_hl**2)
        term2 = (2*np.log(2) - 1) * (log_co**2)
        
        volatility = np.sqrt(term1 - term2)
        return volatility.rolling(window=window).mean() * np.sqrt(252)
    
    def yang_zhang_volatility(self, window=40):
        """杨-张波动率"""
        # 计算各种收益率
        log_oc = np.log(self.data['Open'] / self.data['Close'].shift(1))
        log_ho = np.log(self.data['High'] / self.data['Open'])
        log_lo = np.log(self.data['Low'] / self.data['Open'])
        log_co = np.log(self.data['Close'] / self.data['Open'])
        
        # 计算各部分波动率
        k = 0.34 / (1.34 + (window + 1) / (window - 1))
        
        overnight_var = log_oc.rolling(window=window).var()
        open_var = log_co.rolling(window=window).var()
        
        rs = log_ho * (log_ho - log_co) + log_lo * (log_lo - log_co)
        rs_var = rs.rolling(window=window).mean()
        
        # 组合波动率
        total_var = overnight_var + k * open_var + (1 - k) * rs_var
        return np.sqrt(total_var) * np.sqrt(252)

# 创建波动率估计器
vol_estimator = VolatilityEstimator(spy_data)

# 计算各种波动率
rolling_vol = vol_estimator.rolling_volatility()
parkinson_vol = vol_estimator.parkinson_volatility()
gk_vol = vol_estimator.garman_klass_volatility()
yz_vol = vol_estimator.yang_zhang_volatility()

print("各种波动率估计方法的比较:")
comparison_df = pd.DataFrame({
    'Rolling': rolling_vol,
    'Parkinson': parkinson_vol,
    'Garman-Klass': gk_vol,
    'Yang-Zhang': yz_vol
}).dropna()

print(comparison_df.describe())

第四步 头寸规模管理

class PositionSizing:
    def __init__(self, signals, volatility, target_vol=0.15):
        self.signals = signals
        self.volatility = volatility
        self.target_vol = target_vol  # 目标年化波动率15%
        
    def volatility_scaling(self):
        """波动率缩放"""
        # 计算头寸规模
        position_size = self.signals * (self.target_vol / self.volatility.replace(0, np.nan))
        
        # 处理极端值
        position_size = position_size.clip(-2, 2)  # 最大杠杆2倍
        
        # 填充缺失值
        position_size = position_size.fillna(0)
        
        return position_size

# 使用杨-张波动率和价格动量信号
position_sizer = PositionSizing(price_momentum_signal, yz_vol)
positions = position_sizer.volatility_scaling()

print("头寸规模统计:")
print(positions.describe())

第五步 回测框架

class BacktestEngine:
    def __init__(self, data, positions):
        self.data = data
        self.positions = positions
        self.results = {}
        
    def calculate_returns(self):
        """计算策略收益"""
        # 计算每日收益率
        daily_returns = self.data['Close'].pct_change()
        
        # 计算策略收益率(滞后一天执行)
        strategy_returns = self.positions.shift(1) * daily_returns
        
        # 计算累计收益
        cumulative_returns = (1 + strategy_returns).cumprod()
        
        # 计算基准收益
        benchmark_returns = (1 + daily_returns).cumprod()
        
        return strategy_returns, cumulative_returns, benchmark_returns
    
    def calculate_performance_metrics(self, returns):
        """计算绩效指标"""
        # 年化收益率
        annual_return = returns.mean() * 252
        
        # 年化波动率
        annual_volatility = returns.std() * np.sqrt(252)
        
        # 夏普比率
        sharpe_ratio = annual_return / annual_volatility if annual_volatility != 0 else 0
        
        # 最大回撤
        cumulative = (1 + returns).cumprod()
        running_max = cumulative.expanding().max()
        drawdown = (cumulative - running_max) / running_max
        max_drawdown = drawdown.min()
        
        # 胜率
        win_rate = (returns > 0).mean()
        
        return {
            '年化收益率': annual_return,
            '年化波动率': annual_volatility,
            '夏普比率': sharpe_ratio,
            '最大回撤': max_drawdown,
            '胜率': win_rate
        }
    
    def run_backtest(self):
        """运行回测"""
        strategy_returns, cumulative_returns, benchmark_returns = self.calculate_returns()
        
        # 计算绩效指标
        strategy_metrics = self.calculate_performance_metrics(strategy_returns.dropna())
        
        # 存储结果
        self.results = {
            'strategy_returns': strategy_returns,
            'cumulative_returns': cumulative_returns,
            'benchmark_returns': benchmark_returns,
            'metrics': strategy_metrics
        }
        
        return self.results

# 运行回测
backtest = BacktestEngine(spy_data, positions)
results = backtest.run_backtest()

print("策略绩效指标:")
for metric, value in results['metrics'].items():
    print(f"{metric}: {value:.4f}")

 完整策略函数

def tsmom_strategy(symbol, momentum_type='sma_cross', volatility_type='yang_zhang',
                  momentum_window=20, volatility_window=40, target_vol=0.15):
    """
    完整的时间序列动量策略
    
    参数:
    - symbol: 股票代码
    - momentum_type: 动量类型 ('price', 'sma_cross', 'sma_slope')
    - volatility_type: 波动率类型 ('rolling', 'parkinson', 'garman_klass', 'yang_zhang')
    - momentum_window: 动量窗口
    - volatility_window: 波动率窗口
    - target_vol: 目标年化波动率
    
    返回:
    - results: 包含回测结果的字典
    """
    
    # 获取数据
    data = get_stock_data(symbol)
    
    # 创建策略实例
    strategy = MomentumStrategy(data, momentum_window, volatility_window)
    
    # 生成动量信号
    if momentum_type == 'price':
        signals = strategy.calculate_price_momentum()
    elif momentum_type == 'sma_cross':
        signals = strategy.calculate_sma_crossover()
    elif momentum_type == 'sma_slope':
        signals = strategy.calculate_sma_slope()
    else:
        raise ValueError(f"不支持的动量类型: {momentum_type}")
    
    # 计算波动率
    vol_estimator = VolatilityEstimator(data)
    
    if volatility_type == 'rolling':
        volatility = vol_estimator.rolling_volatility(volatility_window)
    elif volatility_type == 'parkinson':
        volatility = vol_estimator.parkinson_volatility(volatility_window)
    elif volatility_type == 'garman_klass':
        volatility = vol_estimator.garman_klass_volatility(volatility_window)
    elif volatility_type == 'yang_zhang':
        volatility = vol_estimator.yang_zhang_volatility(volatility_window)
    else:
        raise ValueError(f"不支持的波动率类型: {volatility_type}")
    
    # 头寸规模管理
    position_sizer = PositionSizing(signals, volatility, target_vol)
    positions = position_sizer.volatility_scaling()
    
    # 回测
    backtest = BacktestEngine(data, positions)
    results = backtest.run_backtest()
    
    return results

# 使用示例
print("运行完整的TSMOM策略...")
strategy_results = tsmom_strategy('SPY', 
                                 momentum_type='sma_cross',
                                 volatility_type='yang_zhang',
                                 momentum_window=20,
                                 volatility_window=40,
                                 target_vol=0.15)

print("\\n完整策略绩效:")
for metric, value in strategy_results['metrics'].items():
    print(f"{metric}: {value:.4f}")

策略优化过程分析

 参数优化热力图显示,20-40天的动量窗口配合30-50天的波动率窗口表现最佳


代码使用说明


参数调优建议:

  • 动量窗口:20-60天
  • 波动率窗口:30-60天
  • 目标波动率:10%-20%

风险提示:

  • 过去表现不代表未来
  • 需要严格的风险控制
  • 建议先用模拟盘测试

06 策略优化与风险控制

一个好的策略不仅要能赚钱,还要能控制风险。让我们看看如何优化TSMOM策略:

策略优化过程分析

 参数优化热力图显示,20-40天的动量窗口配合30-50天的波动率窗口表现最佳

参数优化

动量窗口优化

测试不同的时间窗口:10, 20, 30, 40, 50, 60天

波动率窗口优化

测试:20, 30, 40, 50, 60, 80天

目标波动率优化

测试:10%, 15%, 20%, 25%

风险管理


最大头寸限制

单个资产最大2倍杠杆

止损机制

单日亏损超过5%减仓

相关性控制

分散投资,避免过度集中

策略表现对比

策略类型年化收益夏普比率最大回撤胜率
简单价格动量12.3%0.85-18.5%52.3%
均线交叉15.7%0.96-16.2%54.8%
均线斜率14.1%0.89-19.8%51.7%
优化组合策略18.9%1.23-12.4%58.2%
 数据来源:基于SPY 2020-2024年数据回测结果

实战经验

策略优化要点:

  • • 不要过度优化,保持参数稳健性
  • • 考虑交易成本和滑点
  • • 定期重新优化参数
  • • 多资产分散投资

风险控制要点:

  • • 设置最大回撤限制
  • • 避免过度杠杆
  • • 监控策略表现
  • • 及时调整策略

总结与展望

通过本文的学习,你不仅理解了时间序列动量策略的核心原理,还掌握了用Python实现策略的具体方法。记住,成功的量化交易不仅需要好的策略,还需要严格的风险控制和持续的优化。

我们得到了什么

  • TSMOM策略的核心原理和实现方法
  • 三种动量信号生成方式的优缺点
  • 四种波动率估计器的特点和适用场景
  • 完整的Python实现代码

下一步方向

  • 多资产动量策略组合
  • 机器学习在动量策略中的应用
  • 动态参数调整策略
  • 期权动量策略

量化交易是一条需要不断学习的路,希望这篇文章能成为你量化之路的一个良好开端。记住,市场永远在变化,我们的策略也要不断进化。

如果你觉得这篇文章对你有帮助,记得分享给更多的朋友。有什么问题可以在评论区留言,老余会一一解答。让我们一起在量化交易的道路上越走越远!

 #量化交易 #Python金融 #TSMOM #动量策略 #量化投资 #金融数据 #AI投资 #股票分析 #投资策略


风险提示:投资有风险,入市需谨慎。本文仅供学习参考,不构成投资建议。

Published inAI&Invest专栏

Be First to Comment

发表回复