Skip to content

你的动量策略为什么总是亏?是因为代码里常常有这个坑

作 者:老余捞鱼

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

文中示例仅用于技术讨论,不构成任何操作建议。
量化策略开发应以学习和技术交流为目的。
本号不荐股、不卖课、不承诺收益。
市场有风险,请合法合规投资。

本文约 3800 字 | 预计阅读 7-10 分钟,内有实例代码,建议先收藏。

· · ·

先说一个反直觉的现象

大多数老散户会告诉你:追涨杀跌是亏钱的根源。

这话有一定道理,但不够完整。在某些特定的时间框架内,”追涨杀跌”其实是一种有学术背书的系统化策略,学术界把它叫做时间序列动量(Time Series Momentum,TSMOM)

核心逻辑很朴素:资产过去几个月的表现,在未来一段时间内倾向于延续。涨的继续涨,跌的继续跌。这不是玄学,而是一套有实证数据支撑的交易思路。

2011年,三位学者 Moskowitz、Ooi 和Pedersen在顶刊发表了一篇研究,检验了覆盖股指期货、外汇期货、大宗商品期货和国债期货在内的58种流动性资产。几乎每一种资产的历史收益都能预测自己未来的方向。

这个结论听起来有点反常识,但背后的行为金融学解释很直白:市场对信息存在反应迟滞。好消息来了,先知先觉的人先进场,后知后觉的人跟着追,形成持续的趋势。等趋势走完一程,才可能出现反转。

今天这篇文章,我用 Python 把时间序列动量的完整实现跑一遍,从信号计算到仓位管理到回测评估,全部给你拆开来讲。

一、学术界怎么验证这个规律

上面说到的论文(链接附在文末)里用了相当全面的数据:

▲ 跨资产类别的动量效应:58种期货的历史数据验证了趋势跟踪的可行性(来源:Moskowitz, Ooi & Pedersen, 2011)
  • 58种期货合约,覆盖四大资产类别:股指、外汇、大宗商品、国债。
  • 检验周期从上世纪七八十年代到2009年,时间跨度足够长。
  • 结果显示:几乎每一种资产都能从自己的历史收益中预测未来方向。

更关键的是,当把这58个品种的动量信号做成一个组合时,产生了相当可观的风险调整后收益,而这个收益和传统的股票市场因子、价值因子之间的相关性很低。

翻译成人话就是:这个策略赚的钱,不是靠”大盘涨了我跟着涨”来的,它是独立的。不过,这里有个重要的背景需要知道:趋势跟踪策略的利润,一部分来自为他人提供保险。 做趋势跟踪的人,在市场平稳时不断小额亏损,但当极端行情来临时赚大钱。反过来,那些想要对冲风险的人(套保者),为此付出了保险费。资金流向是:套保者付钱,趋势跟踪者收钱。

二、基础动量信号怎么算

说完了理论,来看代码实现。

动量信号的核心只有一步:比较当前价格和某个历史窗口的价格,看方向。

看下面这个函数:

import yfinance as yf
import pandas as pd
import numpy as np

def calculate_tsmom_signal(ticker, lookback=20,
                           start="2010-01-01",
                           end="2025-12-31"):
    data = yf.download(ticker, start=start, end=end,
                       progress=False)

    # 提取收盘价
    prices = data["Close"]
    if isinstance(prices, pd.DataFrame):
        prices = prices.squeeze()
    prices = prices.dropna()

    # 简单动量收益:用当前价格除以 lookback 天前的价格 - 1
    momentum = prices / prices.shift(lookback) - 1

    # 信号:收益为正则多头,为负则空头
    signal = np.sign(momentum)

    # 信号要提前一天对齐:收盘时产生信号,次日开盘执行
    signal = signal.shift(1)

    return prices, signal

逐步解释一下:

第一步:用 yfinance 下载历史收盘价,数据来源是 Yahoo Finance,A股也可以换成 akshare。

第二步:计算窗口期收益。比如 lookback=20,就是用今天的价格除以20个交易日前的价格,再减1。这个数值可能是正的,也可能是负的。

第三步:取方向。收益为正则信号为+1(做多),收益为负则信号为-1(做空)。简单粗暴,但有效。

第四步:信号偏移一位。这是量化回测里最重要的原则之一,信号在收盘时产生,实际仓位从次日才开始生效,不能偷看未来数据。

三、为什么要做波动率缩放

光有方向信号还不行。如果不加控制,同样的信号在不同资产上产生的风险敞口可能相差十倍,波动大的品种仓位太重,波动小的品种仓位太轻。

解决办法是波动率缩放(Volatility Scaling)

def volatility_scaled_position(prices, signal,
                               target_vol=0.15,
                               vol_lookback=60):
    returns = prices.pct_change()
    # 计算历史实现波动率,年化
    realized_vol = returns.rolling(vol_lookback).std() \
                   * np.sqrt(252)

    # 避免除零
    realized_vol = realized_vol.replace(0, np.nan).ffill()

    # 目标波动率 / 实际波动率,得到缩放系数
    position = signal * (target_vol / realized_vol)

    # 限制极端仓位,最多只允许3倍杠杆
    position = position.clip(-3, 3)

    return position

核心思路:把仓位大小调整成每笔交易对组合的风险贡献大致相等。波动率高的资产少配,波动率低的资产多配。

这里的15%是年化目标波动率,是一个常见的组合风险参数,可以根据你自己的风险偏好调整。

clip(-3, 3) 这一步是风控底线,最多只允许3倍杠杆,不能让单一仓位把整个组合拖下水。

四、单品种回测:我跑了SPY的数据

有了仓位,回测就顺理成章了:

def backtest_tsmom(prices, position, cost_bps=5):
    asset_returns = prices.pct_change()

    # 策略收益 = 前一天仓位 × 当天资产收益
    strategy_returns = position.shift(1) * asset_returns

    # 估算交易成本:仓位变动 × 手续费率
    position_change = position.diff().abs()
    costs = position_change * (cost_bps / 10000)

    net_returns = strategy_returns - costs
    if isinstance(net_returns, pd.DataFrame):
        net_returns = net_returns.squeeze()
    net_returns = net_returns.dropna()

    # 计算关键指标
    cumulative = (1 + net_returns).cumprod()
    total_return = cumulative.iloc[-1] - 1
    ann_return = (1 + total_return) ** (252 / len(net_returns)) - 1
    ann_vol = net_returns.std() * np.sqrt(252)
    sharpe = ann_return / ann_vol if ann_vol > 0 else np.nan

    return {
        "cumulative": cumulative,
        "ann_return": ann_return,
        "ann_vol": ann_vol,
        "sharpe": sharpe,
        "net_returns": net_returns
    }

运行结果(SPY,2010-2025,20日 lookback,15%目标波动率):

指标数值
年化收益率-0.10%
年化波动率16.36%
夏普比率-0.01

这个结果有点扎眼,Sharpe 几乎等于零。 说明在2020-2025年的美股市场,单纯的时间序列动量策略并没有显著的超额收益。

这正好印证了那句老话:策略不是全场景通用,没有哪个策略在所有市场环境下都有效。 趋势跟踪在趋势明显的行情里表现好,在震荡市里会不断被来回打脸。

五、多资产组合:分散化才是关键

单个品种效果一般,那多个品种组合呢?

def multi_asset_tsmom(tickers, lookback=60, target_vol=0.10):
    all_positions = {}
    all_returns = {}

    for ticker in tickers:
        prices, signal = calculate_tsmom_signal(
            ticker, lookback=lookback)
        position = volatility_scaled_position(
            prices, signal, target_vol=target_vol)
        asset_returns = prices.pct_change()

        all_positions[ticker] = position
        all_returns[ticker] = asset_returns

    positions_df = pd.DataFrame(all_positions)
    returns_df = pd.DataFrame(all_returns)

    # 等权平均各资产的仓位
    portfolio_position = positions_df.mean(axis=1)
    portfolio_returns = (positions_df.shift(1) * returns_df).mean(axis=1)

    return portfolio_returns.dropna()

测试组合:SPY(美股)、TLT(长期国债)、GLD(黄金)、UUP(美元指数)。覆盖四类资产:股票、债券、黄金、美元。

组合逻辑不复杂:每个品种分别跑动量信号,做完波动率缩放之后,等权平均。 这么做的好处是,当某一类资产进入震荡时,其他资产的趋势信号可能还在生效,组合层面可以平滑收益。这也是为什么学术研究发现趋势跟踪在极端市场环境下反而表现最强,危机来临时,各类资产的相关性飙升,方向性趋势更明显,趋势跟踪策略的信号成功率大幅提升。

六、对个人投资者有什么启发

说完代码,来点实在的。个体投资者没有机构那么复杂的系统,但完全可以借鉴这个思路。

第一,用主流ETF就够了。 不需要搞期货,不需要杠杆,你手上的 QDII ETF、黄金ETF、国债ETF、货币基金,就是天然的资产类别覆盖。

第二,lookback 窗口不要乱设。 学术界常用 1 个月到 12 个月,我的经验是从 60 日(3个月)起步做测试,太短了噪声多,太长了信号滞后严重。

第三,波动率缩放一定要做。 这是策略能不能长期跑下去的核心。你不一定要手写代码,但现在主流的智能定投工具和FOF组合,背后多少都有类似思路。

第四,成本比你想象的更伤。 策略越频繁换仓,交易成本越高。机构的手续费极低,散户如果用高佣金券商,成本可以把所有超额收益吃掉。

第五,这个策略也有弱点。 趋势跟踪在市场急转的时候会有明显滞后,追进去的时候可能趋势已经快结束了。震荡行情里信号来回切换,会反复亏损。仓位管理和组合分散是应对这些弱点的主要手段。

七、进阶优化:六个可以提升的方向

如果你想把这套框架继续完善,下面几个方向值得研究:

一、拉长 lookback 窗口

学术研究显示,6-12个月的动量信号比短周期信号更稳定,噪声更少。短周期(20日以内)更适合短线趋势,但震荡市的亏损也更频繁。

二、多周期叠加

用一个 lookback 信号容易错过其他时间尺度的趋势。实践中,把 60 日、120 日、252 日三个周期的信号取平均,得到的复合信号更稳健。

三、资产间相关性调整

当多个市场同时向同一方向运行时,等权组合会产生过度集中。通过计算资产间的滚动相关性,降低同向信号的仓位权重,可以控制组合层面的极端风险。

四、机器学习辅助

ML可以捕捉收益预测中的非线性关系,比如用随机森林或XGBoost结合多个技术指标来预测未来收益。不过,简单线性规则在很多市场里依然有竞争力,ML的优势在于特征组合,不在于替代逻辑。

五、风控层叠加

  • 单品种最大仓位上限
  • 组合层面年化波动率目标
  • 止损线或时间止损
  • 最大回撤控制

这些是机构标准配置,散户用ETF实现起来也不复杂。

六、关注策略拥挤度

趋势跟踪策略近年越来越受欢迎,大量资金流入同一个类型的策略,会压缩未来收益空间。关注CTA管理规模和趋势类对冲基金的业绩基准,是判断策略拥挤度的间接方法。

八、观点总结

时间序列动量不是一个”稳赚密码”,它有自己的适用场景和局限。

它的优势在于:逻辑清晰、有学术支撑、在极端行情里有超额表现、能分散传统股债组合的风险

它的劣势在于:震荡市里反复亏损、短期表现可能跑输指数、对交易成本敏感、需要足够长的回测周期才能看到效果

对普通投资者来说,把它作为自己股债组合里的一块卫星配置(5%-20%仓位),可能是更务实的用法。用核心仓位做资产配置,用卫星仓位做趋势增强,两者结合,既能享受分散化红利,又不至于完全错过趋势行情。

本文用到的完整代码逻辑都可以直接跑,有Python基础的朋友可以自己动手试试。数据源换成akshare的A股接口,同样的框架也能用来测试国内期货和ETF的动量效应。

📚 参考资料

• Time Series Momentum (Moskowitz, Ooi, Pedersen, 2011) :https://www.sciencedirect.com/science/article/pii/S0304405X11002613

•本文策略代码基于 Python + yfinance 实现;A股数据可替换为 akshare等。


#时间序列动量 #量化策略 #趋势跟踪 #Python量化 #资产配置 #波动率缩放 #动量效应 #多资产组合 #Sharpe比率 #风险管理 #Futures动量 #系统化交易

Published inAI&Invest专栏

Be First to Comment

    发表回复