Py学习  »  Python

用 Python 实现量化交易策略:时间序列动量完全指南

数据科学实战 • 3 周前 • 118 次点击  

欢迎加入专注于财经数据与量化投研的【数据科学实战】知识星球!在这里,您将获取持续更新的《财经数据宝典》和《量化投研宝典》,这两部宝典相辅相成,为您在量化投研道路上提供明确指引。 我们提供了精选的国内外量化投研的 220+ 篇高质量文章,并每日更新最新研究成果,涵盖策略开发、因子分析、风险管理等核心领域。 无论您是量化投资新手还是经验丰富的研究者,星球社区都能帮您少走弯路,事半功倍,共同探索数据驱动的投资世界!

引言

你是否想过,为什么有些交易策略能够持续跑赢市场?时间序列动量(Time Series Momentum,简称 TSMOM)就是量化金融领域最稳健的市场异象之一。本文将带你深入理解 TSMOM 策略的核心原理,并通过 Python 代码实现一个完整的回测框架。

无论你是量化交易的初学者,还是希望提升策略开发能力的 Python 开发者,这篇文章都会给你带来实质性的收获。

什么是时间序列动量策略

TSMOM 策略的核心思想非常简洁:

  • 当资产呈现正向动量时,做多
  • 当资产呈现负向动量时,做空
  • 通过波动率目标化来维持稳定的风险敞口

与传统的横截面动量策略不同,TSMOM 只关注资产自身的历史表现,而不与其他资产进行比较。

策略架构设计

一个专业的 TSMOM 策略需要遵循严格的时间顺序,以避免前视偏差(Look-Ahead Bias):

  1. 在第 t 天收盘时,使用截至 t-1 天的数据生成信号
  2. 在第 t+1 天开盘时执行交易
  3. 在第 t+1 天开盘时对组合进行估值
  4. 收益率计算公式为: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.01.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 日窗口)
  • 夏普比率:最高达 0.965
  • 总收益:约 90%(基准约 60%)

关键发现

估计器效率差异显著

基于价格范围的估计器(Parkinson、Garman-Klass、Yang-Zhang)能够捕捉日内信息,表现出理论上的效率优势。Yang-Zhang 在存在明显跳空的市场中表现尤为突出。

最优参数组合

短期动量窗口(20 日)配合中期波动率窗口(40 日)能够在响应速度和稳定性之间取得最佳平衡。

波动率目标化的重要性

波动率缩放是 TSMOM 成功的基础,能够在危机期间减少敞口,在平静期增加敞口。

交易成本的影响

本框架纳入了每笔交易 0.1% 的佣金。长周期均线交叉策略能够最大程度减少换仓频率和交易成本。

实践建议

避免前视偏差

所有信号计算都使用 shift(1) 确保 t 时刻的信号只使用 t-1 及之前的数据。

真实的执行逻辑

策略在信号生成后的下一个开盘价执行,收益按开盘价到开盘价计算。

风险管理

仓位上限控制在资本的 ±100%,并妥善处理零波动率的除法问题。

总结

本文详细介绍了 TSMOM 策略的完整实现,包括三种动量指标和四种波动率估计器的组合。通过 144 种配置的回测,我们验证了以下结论:

  1. TSMOM 策略有效:各策略持续跑赢买入持有基准
  2. 波动率估计器很重要:基于价格范围的估计器能提升信号质量
  3. 波动率目标化不可或缺:维持恒定风险并改善夏普比率
  4. 短周期参数占优:20-40 日参数组合表现最佳
  5. 实现细节至关重要:真实执行逻辑、佣金和偏差消除是基础

对于想要进入量化交易领域的 Python 开发者,TSMOM 代表了一个稳健、透明的策略框架。推荐的最优配置是均线交叉(20/200)配合 40 日 Rolling 或 Yang-Zhang 波动率估计器。

参考文章

加入专注于财经数据与量化投研的知识星球【数据科学实战】,获取本文完整研究解析、代码实现细节。

财经数据与量化投研知识社区

核心权益如下:

  1. 赠送《财经数据宝典》完整文档,汇集多年财经数据维护经验
  2. 赠送《量化投研宝典》完整文档,汇集多年量化投研领域经验
  3. 赠送《PyBroker-入门及实战》视频课程,手把手学习量化策略开发
  4. 每日分享高质量量化投研文章(已更新 180+篇)、代码和相关资料
  5. 定期更新高频财经数据
  6. 参与年度不少于 10 次专属直播与录播课程
  7. 与核心开发者直接交流,解决实际问题
  8. 获取专业微信群交流机会和课程折扣

星球已有丰富内容积累,包括量化投研论文、财经高频数据、 PyBroker 视频教程、定期直播、数据分享和答疑解难。适合对量化投研和财经数据分析有兴趣的学习者及从业者。欢迎加入我们!

好文推荐

1. 用 Python 打造股票预测系统:Transformer 模型教程(一)

2. 用 Python 打造股票预测系统:Transformer 模型教程(二)

3. 用 Python 打造股票预测系统:Transformer 模型教程(三)

4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)

5. 揭秘隐马尔可夫模型:因子投资的制胜武器

6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用

7. 金融 AI 助手:FinGPT 让你轻松掌握市场分析

8. 量化交易秘籍:为什么专业交易员都在用对数收益率?

9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解

10. 掌握金融波动率模型:完整 Python 实现指南

好书推荐



Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/191141