Appearance
动量策略:趋势是你的朋友
在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 * scaling2. 结合反转因子
在短期动量出现反转信号时,降低或对冲动量暴露:
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 df3. 行业中性化
动量策略容易在特定行业集中。通过行业中性化,可以避免单一行业反转带来的集中风险:
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 df4. 动态止损
为持仓设置合理的止损线,在趋势反转时及时退出:
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股市场同样适用。但策略并非万能——动量崩溃风险是悬在头上的达摩克利斯之剑。理解策略的收益来源、合理选择参数、做好风险管理,是长期运用动量策略的关键。
本文仅为教学目的,介绍动量策略的基本原理和实现方法,不构成任何投资建议。量化策略的历史表现不代表未来收益,实际交易需考虑交易成本、滑点、流动性等现实约束。