Skip to content

动量策略:趋势是你的朋友

在A股市场中,你是否注意过这样一种现象:过去一段时间涨得好的股票,未来一段时间似乎还能继续涨;而跌得惨的股票,往往还要继续跌。这不是错觉,而是被大量学术研究反复验证的规律——动量效应。基于这一效应构建的交易方法,就是动量策略。

Jegadeesh 和 Titman 在 1993 年发表的论文中首次系统性地证明了动量效应的存在:买入过去表现最好的股票组合,卖空过去表现最差的股票组合,能够获得显著的超额收益。此后三十年,动量策略被广泛研究并在全球多个市场得到验证,成为量化投资领域最经典、最持久的策略之一。

核心逻辑:强者恒强

动量策略的底层逻辑可以概括为四个字——强者恒强。其核心假设是:价格趋势在短期内具有延续性。当一只股票受到资金持续关注、基本面持续改善或市场情绪持续升温时,这种推动力往往不会在一夜之间消失,而是会延续一段时间。

从行为金融学角度,动量效应的存在有几种解释:

  • 反应不足:投资者对新信息的消化需要时间。当一家公司发布利好消息时,价格不会一步到位,而是分阶段上涨。在这个过程中,早期买入者获利,吸引更多跟风资金,推动价格继续上行。
  • 羊群效应:投资者倾向于追随市场主流方向。当某只股票持续上涨时,会吸引更多买盘,形成正反馈循环。
  • 处置效应:投资者倾向于过早卖出盈利的股票,而持有亏损的股票。这种心理导致上涨股票的抛压被分散释放,趋势得以延续。

理解这些底层机制很重要,因为它们决定了动量策略在什么环境下有效、什么环境下可能失效。

动量因子的计算

动量因子本质上衡量的是一段时间内价格变化的大小。常见的计算方法有以下几种:

简单收益率动量

最直接的方法是计算过去N天的累计收益率:

python
import pandas as pd
import numpy as np

def calc_momentum(close: pd.Series, period: int = 20) -> pd.Series:
    """计算简单收益率动量

    Args:
        close: 收盘价序列
        period: 回望周期(交易日)

    Returns:
        动量因子值序列
    """
    momentum = close / close.shift(period) - 1
    return momentum

对数收益率动量

对数收益率在统计性质上更优(可加性),适合后续进行因子分析和回测:

python
def calc_log_momentum(close: pd.Series, period: int = 20) -> pd.Series:
    """计算对数收益率动量"""
    log_return = np.log(close / close.shift(period))
    return log_return

风险调整动量

简单收益率没有考虑波动率的影响。两只股票可能过去都涨了20%,但一只平稳上涨,另一只大起大落。风险调整动量通过除以波动率来标准化:

python
def calc_risk_adj_momentum(close: pd.Series, period: int = 60) -> pd.Series:
    """计算风险调整动量(类似夏普比率)"""
    daily_returns = close.pct_change()
    momentum = close / close.shift(period) - 1
    volatility = daily_returns.rolling(period).std() * np.sqrt(252)
    risk_adj_mom = momentum / volatility
    return risk_adj_mom

跳跃动量(Skip-month Momentum)

为了避免短期反转效应的干扰,学术界常用"跳过最近一个月"的方法计算动量。即用过去12个月的收益率,减去最近1个月的收益率,取中间11个月的表现:

python
def calc_skip_momentum(close: pd.Series, long_period: int = 252, skip_period: int = 21) -> pd.Series:
    """跳跃动量:跳过最近skip_period天"""
    momentum = close.shift(skip_period) / close.shift(long_period) - 1
    return momentum

策略实现步骤

下面以A股全市场为股票池,展示一个完整的动量策略实现流程。

第一步:获取数据

python
import pandas as pd
import numpy as np

# 假设已有日线数据,包含列:date, code, close, open, high, low, volume
# 实际项目中可通过 akshare、tushare 等接口获取
# df = get_stock_data(start_date='2020-01-01', end_date='2024-12-31')

第二步:计算动量因子

python
def compute_momentum_factor(df: pd.DataFrame, lookback: int = 60) -> pd.DataFrame:
    """为每只股票计算动量因子"""
    df = df.sort_values(['code', 'date'])
    df['momentum'] = df.groupby('code')['close'].transform(
        lambda x: x / x.shift(lookback) - 1
    )
    return df

第三步:截面排序与分组

在每个换仓日,将所有股票按动量因子值从高到低排序,分为若干组:

python
def rank_and_group(df: pd.DataFrame, date: str, n_groups: int = 5) -> pd.DataFrame:
    """在指定日期对股票按动量排序并分组"""
    daily = df[df['date'] == date].copy()
    daily = daily.dropna(subset=['momentum'])
    daily['group'] = pd.qcut(daily['momentum'], n_groups, labels=False) + 1
    # group=5 为动量最强组,group=1 为动量最弱组
    return daily

第四步:构建组合并计算收益

python
def backtest_momentum(df: pd.DataFrame, lookback: int = 60,
                      holding: int = 20, n_groups: int = 5) -> pd.DataFrame:
    """简单动量策略回测框架"""
    # 获取所有换仓日
    dates = sorted(df['date'].unique())
    rebalance_dates = dates[lookback::holding]

    portfolio_returns = []

    for i in range(len(rebalance_dates) - 1):
        rb_date = rebalance_dates[i]
        next_rb_date = rebalance_dates[i + 1]

        # 排序分组
        ranked = rank_and_group(df, rb_date, n_groups)
        top_group = ranked[ranked['group'] == n_groups]  # 动量最强组

        if len(top_group) == 0:
            continue

        # 等权持有多头组合
        stocks = top_group['code'].tolist()
        period_data = df[(df['date'] > rb_date) & (df['date'] <= next_rb_date)]
        period_data = period_data[period_data['code'].isin(stocks)]

        # 计算每日组合收益
        daily_ret = period_data.groupby('date').apply(
            lambda x: x['close'].pct_change().mean()
        ).dropna()

        portfolio_returns.append(daily_ret)

    result = pd.concat(portfolio_returns)
    return result

第五步:评估策略表现

python
def evaluate_performance(returns: pd.Series) -> dict:
    """评估策略表现"""
    cumulative = (1 + returns).cumprod()
    annual_return = (1 + returns).prod() ** (252 / len(returns)) - 1
    annual_vol = returns.std() * np.sqrt(252)
    sharpe = annual_return / annual_vol if annual_vol != 0 else 0
    max_drawdown = (cumulative / cumulative.cummax() - 1).min()

    return {
        '年化收益率': f'{annual_return:.2%}',
        '年化波动率': f'{annual_vol:.2%}',
        '夏普比率': f'{sharpe:.2f}',
        '最大回撤': f'{max_drawdown:.2%}',
    }

周期选择:1个月、3个月、6个月

回望周期是动量策略最关键的参数之一。不同周期的动量效应有显著差异:

1个月(约20个交易日)

短期动量捕捉的是市场的短期趋势延续。在A股市场中,短期动量效应相对不稳定,部分原因是A股T+1交易制度和涨跌停板限制导致短期价格发现效率较高。此外,短期动量容易受到短期反转效应的干扰。

适用场景:日内级别或周级别的短线交易者,需要结合成交量等辅助指标。

3个月(约60个交易日)

3个月周期是学术界和实务中最常用的动量周期之一。它在趋势延续性和噪声之间取得了较好的平衡。3个月动量在A股市场有较为稳定的效应,特别是在趋势明显的市场环境下。

适用场景:中线趋势跟踪,月度换仓。

6个月(约120个交易日)

6个月动量捕捉的是中长线趋势。它的信号更加平滑,换仓频率更低,交易成本更可控。但缺点是对趋势转折的响应较慢,可能在市场反转时遭受较大损失。

适用场景:低频调仓,适合资金量较大的投资者。

12个月(约250个交易日)

12个月动量是经典学术研究中使用的周期。长期来看,12个月动量在多个市场都有统计显著性,但在A股的表现受市场结构性因素影响较大——A股的牛短熊长特征使得长期动量的持续性不如成熟市场。

周期选择的实践建议

python
# 多周期动量合成:综合多个周期信号
def multi_period_momentum(close: pd.Series) -> pd.Series:
    """多周期动量合成"""
    mom_1m = close / close.shift(20) - 1
    mom_3m = close / close.shift(60) - 1
    mom_6m = close / close.shift(120) - 1

    # 简单等权合成
    composite = (mom_1m + mom_3m + mom_6m) / 3
    return composite

实践中,推荐使用多周期合成的方式,而非依赖单一周期。多周期合成可以降低对单一参数的依赖,提高策略的鲁棒性。同时,建议在构建策略时进行参数敏感性分析——如果稍微改变回望周期策略表现就大幅波动,说明策略不够稳健。

动量崩溃风险

动量策略最大的风险不是日常波动,而是动量崩溃(Momentum Crash)。

什么是动量崩溃

动量崩溃通常发生在市场剧烈反转的时刻。在熊市末期或恐慌性反弹中,之前跌幅最大的股票(动量最弱组)往往反弹幅度最大,而之前涨幅最大的股票(动量最强组)反而表现平平甚至下跌。这时,做多强势股、做空弱势股的动量策略会遭遇双杀,产生极端亏损。

历史上最著名的动量崩溃发生在2009年3月。当时金融危机触底反弹,此前跌幅最大的金融股反弹幅度远超市场平均水平,导致动量策略遭受巨大损失。在A股市场,2015年股灾后的反弹、2020年疫情后的V型反转都曾对动量策略造成严重冲击。

动量崩溃的特征

  • 集中发生:不是均匀分布,而是集中在少数极端市场环境下
  • 幅度巨大:单月亏损可能超过策略年度收益的数倍
  • 难以预测:崩溃发生的时点很难提前判断

应对策略

1. 波动率择时

当市场波动率急剧上升时,降低动量策略的仓位暴露:

python
def vol_targeting(position: pd.Series, returns: pd.Series,
                  target_vol: float = 0.15) -> pd.Series:
    """波动率目标仓位管理"""
    realized_vol = returns.rolling(60).std() * np.sqrt(252)
    scaling = target_vol / realized_vol
    scaling = scaling.clip(0.2, 1.5)  # 限制杠杆范围
    return position * scaling

2. 结合反转因子

在短期动量出现反转信号时,降低或对冲动量暴露:

python
def momentum_with_reversal_filter(df: pd.DataFrame) -> pd.DataFrame:
    """动量 + 短期反转过滤"""
    df['long_mom'] = df.groupby('code')['close'].transform(
        lambda x: x / x.shift(120) - 1
    )
    df['short_mom'] = df.groupby('code')['close'].transform(
        lambda x: x / x.shift(10) - 1
    )
    # 排除短期极端上涨的股票(可能面临回调)
    df = df[df['short_mom'] < df['short_mom'].quantile(0.95)]
    return df

3. 行业中性化

动量策略容易在特定行业集中。通过行业中性化,可以避免单一行业反转带来的集中风险:

python
def industry_neutralize(df: pd.DataFrame, factor_col: str = 'momentum') -> pd.DataFrame:
    """行业中性化:减去行业均值"""
    df[f'{factor_col}_neutral'] = df.groupby('industry')[factor_col].transform(
        lambda x: x - x.mean()
    )
    return df

4. 动态止损

为持仓设置合理的止损线,在趋势反转时及时退出:

python
def apply_stop_loss(prices: pd.Series, stop_loss: float = -0.08) -> pd.Series:
    """应用动态止损"""
    cummax = prices.cummax()
    drawdown = prices / cummax - 1
    # 触发止损后标记为0仓位
    stopped = drawdown < stop_loss
    return (~stopped).astype(float)

A股市场的特殊考量

在A股市场应用动量策略时,有几个本地化的因素需要特别注意:

  • 涨跌停限制:A股10%/20%的涨跌停板限制会影响动量因子的计算和策略执行。涨停的股票可能无法买入,跌停的股票可能无法卖出。
  • T+1交易:当日买入次日才能卖出,限制了日内动量策略的可行性。
  • 散户占比高:A股市场散户参与度高,羊群效应更强,这可能增强动量效应,但也增加了波动。
  • 行业轮动明显:A股行业轮动速度快,不做行业中性化的纯动量策略可能承受较大的行业集中风险。
  • 壳价值与小票效应:小盘股的壳价值会影响动量因子的有效性,建议在构建因子时控制市值暴露。

总结

动量策略是量化投资中经久不衰的经典策略,其核心逻辑"强者恒强"在A股市场同样适用。但策略并非万能——动量崩溃风险是悬在头上的达摩克利斯之剑。理解策略的收益来源、合理选择参数、做好风险管理,是长期运用动量策略的关键。

本文仅为教学目的,介绍动量策略的基本原理和实现方法,不构成任何投资建议。量化策略的历史表现不代表未来收益,实际交易需考虑交易成本、滑点、流动性等现实约束。

仅供学习交流,不构成任何投资建议