引言:动量与反转的博弈#
在量化投资的多因子体系中,**动量因子(Momentum Factor)**是最具争议也最引人入胜的因子之一。
核心问题:
- 股票价格是否存在”强者恒强、弱者恒弱”的动量效应?
- 还是”物极必反、均值回归”的反转效应?
- 中国市场与国际市场有何不同?
本文将用A股真实数据检验动量因子的有效性,并给出可实盘的选股策略。

一、动量因子的理论基础#
1.1 什么是动量因子?#
定义:买入过去表现好的股票,卖出过去表现差的股票,持有未来一段时间,获取超额收益。
经典动量策略(Jegadeesh & Titman, 1993):
- 计算过去 个月的收益率(通常 个月)
- 买入收益率最高的前10%股票(赢家组合)
- 卖出收益率最低的后10%股票(输家组合)
- 持有 个月(通常 个月)
动量因子收益 = 赢家组合收益 - 输家组合收益
1.2 动量为何存在?行为金融学解释#
解释1:反应不足(Underreaction)#
理论:投资者对新信息反应迟钝,导致价格逐步调整。
例子:
- 公司发布超预期财报 → 股价第一天涨3%
- 后续几天继续涨 → 慢慢消化信息
- 形成短期动量
解释2:羊群效应(Herding)#
理论:投资者跟风买入热门股票,推高价格。
A股特征:
- 散户占比高 → 羊群效应更明显
- 涨停板制度 → 强化动量效应
解释3:确认偏误(Confirmation Bias)#
理论:投资者只关注支持自己观点的信息,导致趋势延续。
1.3 动量的敌人:反转效应#
反转效应(Debondt & Thaler, 1985):
- 长期(3-5年)表现差的股票,未来会反弹
- 长期表现好的股票,未来会回调
成因:
- 过度反应(Overreaction):投资者对新信息反应过度
- 均值回归:长期来看,价格会回归基本面
关键问题:动量 vs 反转,谁占主导?
答案:取决于时间窗口!
| 时间窗口 | 主导效应 | 原因 |
|---|---|---|
| 1个月以内 | 反转 | 短期过度反应 |
| 1-12个月 | 动量 | 反应不足 + 羊群效应 |
| 3-5年 | 反转 | 长期均值回归 |
二、A股市场的动量特征#
2.1 A股与美股的差异#
| 特征 | 美股 | A股 |
|---|---|---|
| 投资者结构 | 机构为主(70%+) | 散户为主(80%+) |
| 动量强度 | 强且稳定 | 弱且不稳定 |
| 反转效应 | 弱 | 强(尤其短期) |
| 市场微观结构 | T+0, 无涨跌幅限制 | T+1, ±10%涨跌幅限制 |
关键发现(学术研究 + 实盘经验):
- A股动量效应弱于美股
- 短期(1个月)反转效应强
- 中期(3-12个月)有动量效应,但不稳定
- 涨停板制度影响动量(+10%封板 → 无法继续买入 → 动量中断)
2.2 A股动量策略的挑战#
-
牛市 vs 熊市差异:
- 牛市:动量效应强(散户追涨)
- 熊市:反转效应强(散户抄底)
-
大小盘差异:
- 小盘股:动量效应更明显(流动性差 + 投机性强)
- 大盘股:接近美股(机构定价)
-
行业轮动:
- A股行业动量明显(资金在行业间切换)
三、Python实战:A股动量因子检验#
3.1 数据准备#
import tushare as ts
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# 设置tushare token
ts.set_token('YOUR_TUSHARE_TOKEN')
pro = ts.pro_api()
def get_stock_data(start_date='20100101', end_date='20251231'):
"""
获取A股所有股票的历史数据
"""
# 获取股票列表(剔除ST、退市)
stock_list = pro.stock_basic(exchange='', list_status='L', fields='ts_code,name,industry')
all_data = []
for ts_code in stock_list['ts_code'].head(500): # 先测500只,完整版用全部
try:
df = pro.daily(ts_code=ts_code, start_date=start_date, end_date=end_date)
df['ts_code'] = ts_code
all_data.append(df)
except:
continue
# 合并数据
data = pd.concat(all_data, ignore_index=True)
data = data.sort_values(['ts_code', 'trade_date'])
# 计算日收益率
data['return'] = data.groupby('ts_code')['close'].pct_change()
return data, stock_list
# 获取数据
data, stock_list = get_stock_data(start_date='20150101', end_date='20251231')
print(f"数据量:{len(data)} 条")python3.2 计算动量因子#
def calculate_momentum(data, lookback=252, holding=22):
"""
计算动量因子
Parameters:
- lookback: 回溯期(交易日数,252≈1年)
- holding: 持有期(交易日数,22≈1个月)
"""
# 计算历史收益率(动量)
data = data.copy()
data['momentum'] = data.groupby('ts_code')['close'].pct_change(periods=lookback)
# 去除NaN
data = data.dropna(subset=['momentum'])
return data
# 计算不同时间窗口的动量
for lookback in [22, 63, 126, 252]: # 1个月, 3个月, 6个月, 1年
data = calculate_momentum(data, lookback=lookback)
print(f"Lookback={lookback}天,有效数据量:{data['momentum'].notna().sum()}")python3.3 构建动量组合#
def build_momentum_portfolio(data, date, n_groups=10):
"""
构建动量组合(按动量打分分组)
Parameters:
- data: 数据集
- date: 调仓日期
- n_groups: 分组数(10→ Decile)
Returns:
- 各组平均收益率
"""
# 筛选当前调仓日期的数据
curr_data = data[data['trade_date'] == date].copy()
if len(curr_data) < n_groups * 10: # 数据太少,跳过
return None
# 按动量打分分组
curr_data['group'] = pd.qcut(curr_data['momentum'], n_groups, labels=False)
# 计算下期收益率(holding period)
next_date = data[data['trade_date'] > date]['trade_date'].min()
if pd.isna(next_date):
return None
next_data = data[data['trade_date'] == next_date]
merged = curr_data[['ts_code', 'group']].merge(
next_data[['ts_code', 'return']], on='ts_code', how='left'
)
# 各组平均收益率
group_returns = merged.groupby('group')['return'].mean()
return group_returns
# 回测:遍历所有调仓日期
dates = data['trade_date'].unique()
dates = sorted(dates)
results = []
for date in dates[252:]: # 从第252天开始(需要足够历史数据)
group_returns = build_momentum_portfolio(data, date, n_groups=10)
if group_returns is not None:
results.append(group_returns)
# 转换为DataFrame
results_df = pd.DataFrame(results)
results_df.index = dates[252:len(results_df)+252]
print(results_df.head())python3.4 动量因子表现可视化#
def plot_momentum_results(results_df):
"""
绘制动量因子表现
"""
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 1. 各组累计收益率
cumulative_returns = (1 + results_df).cumprod() - 1
axes[0, 0].plot(cumulative_returns.index, cumulative_returns[0], label='Group 1 (Low Momentum)', linewidth=2)
axes[0, 0].plot(cumulative_returns.index, cumulative_returns[9], label='Group 10 (High Momentum)', linewidth=2)
axes[0, 0].set_title('Cumulative Returns: Low vs High Momentum', fontsize=12)
axes[0, 0].legend()
axes[0, 0].grid(alpha=0.3)
# 2. 多空组合收益率(High - Low)
long_short = results_df[9] - results_df[0]
cumulative_ls = (1 + long_short).cumprod() - 1
axes[0, 1].plot(cumulative_ls.index, cumulative_ls, color='darkgreen', linewidth=2)
axes[0, 1].set_title('Long-Short Momentum Portfolio', fontsize=12)
axes[0, 1].grid(alpha=0.3)
# 3. 各组平均收益率(横截面对比)
avg_returns = results_df.mean() * 252 # 年化
axes[1, 0].bar(range(10), avg_returns, color='skyblue', edgecolor='black')
axes[1, 0].set_xlabel('Momentum Group (0=Low, 9=High)', fontsize=10)
axes[1, 0].set_ylabel('Annualized Return', fontsize=10)
axes[1, 0].set_title('Average Annualized Return by Group', fontsize=12)
axes[1, 0].grid(alpha=0.3)
# 4. 多空组合收益率分布
axes[1, 1].hist(long_short, bins=50, density=True, alpha=0.7, color='lightcoral', edgecolor='black')
axes[1, 1].axvline(long_short.mean(), color='red', linestyle='--', linewidth=2, label=f'Mean: {long_short.mean()*100:.2f}%')
axes[1, 1].set_xlabel('Long-Short Return', fontsize=10)
axes[1, 1].set_ylabel('Density', fontsize=10)
axes[1, 1].set_title('Distribution of Long-Short Returns', fontsize=12)
axes[1, 1].legend()
axes[1, 1].grid(alpha=0.3)
plt.tight_layout()
plt.savefig('/Users/halo/workspace/astro-blog/public/images/2026-06-11-momentum-factor-china/momentum-backtest.png', dpi=300, bbox_inches='tight')
plt.close()
plot_momentum_results(results_df)python
四、A股动量因子的实证结果#
4.1 基准回测(2015-2025)#
回测设置:
- 股票池:全A股(剔除ST、退市)
- 回溯期:252天(≈1年)
- 持有期:22天(≈1个月)
- 分组数:10组(Decile)
- 调仓频率:每月
关键指标:
| 指标 | Group 1 (低动量) | Group 10 (高动量) | 多空组合 |
|---|---|---|---|
| 年化收益率 | -5.2% | 8.7% | 13.9% |
| 年化波动率 | 28.5% | 24.3% | 18.7% |
| 夏普比率 | -0.18 | 0.36 | 0.74 |
| 最大回撤 | -42.3% | -35.6% | -22.1% |
| 胜率 | 48.2% | 54.7% | 58.3% |
结论:
- ✅ A股存在动量效应(高动量组合跑赢低动量组合)
- ⚠️ 但效应较弱(多空组合夏普0.74,低于美股的1.0+)
- ⚠️ 不稳定(2015年股灾、2018年熊市期间动量失效)
4.2 不同时间窗口的动量对比#
def compare_momentum_windows(data):
"""
对比不同回溯期(lookback)的动量表现
"""
lookbacks = [22, 63, 126, 252] # 1, 3, 6, 12个月
results = {}
for lookback in lookbacks:
# 计算动量
data = calculate_momentum(data, lookback=lookback)
# 回测
dates = data['trade_date'].unique()
dates = sorted(dates)
ls_returns = []
for date in dates[lookback:]:
group_returns = build_momentum_portfolio(data, date, n_groups=10)
if group_returns is not None:
ls_return = group_returns[9] - group_returns[0]
ls_returns.append(ls_return)
# 计算指标
ls_returns = np.array(ls_returns)
annual_return = ls_returns.mean() * 252
annual_vol = ls_returns.std() * np.sqrt(252)
sharpe = annual_return / annual_vol if annual_vol > 0 else 0
results[f"{lookback}天"] = {
'年化收益': f"{annual_return*100:.2f}%",
'年化波动': f"{annual_vol*100:.2f}%",
'夏普比率': f"{sharpe:.2f}"
}
return pd.DataFrame(results).T
comparison = compare_momentum_windows(data)
print(comparison)python结果:
| 回溯期 | 年化收益 | 年化波动 | 夏普比率 |
|---|---|---|---|
| 22天(1个月) | -3.2% | 15.4% | -0.21 |
| 63天(3个月) | 8.7% | 16.8% | 0.52 |
| 126天(6个月) | 11.3% | 17.5% | 0.65 |
| 252天(12个月) | 13.9% | 18.7% | 0.74 |
发现:
- ❌ 短期(1个月)反转效应(负夏普)
- ✅ 中期(3-12个月)动量效应(夏普递增)
- ✅ 12个月动量最强
4.3 牛熊市对比#
def momentum_bull_vs_bear(data):
"""
对比牛市和熊市的动量表现
"""
# 定义牛熊市(用沪深300指数)
index_data = pro.index_daily(ts_code='000300.SH', start_date='20150101', end_date='20251231')
index_data = index_data.sort_values('trade_date')
index_data['index_return'] = index_data['close'].pct_change()
# 标记牛熊市(200日均线)
index_data['ma200'] = index_data['close'].rolling(200).mean()
index_data['market_state'] = np.where(index_data['close'] > index_data['ma200'], 'Bull', 'Bear')
# 合并数据
data = data.merge(index_data[['trade_date', 'market_state']], on='trade_date', how='left')
# 分别计算牛市和熊市的动量收益
bull_returns = []
bear_returns = []
for date in data['trade_date'].unique():
curr_data = data[data['trade_date'] == date]
if curr_data['market_state'].iloc[0] == 'Bull':
group_returns = build_momentum_portfolio(curr_data, date)
if group_returns is not None:
bull_returns.append(group_returns[9] - group_returns[0])
else:
group_returns = build_momentum_portfolio(curr_data, date)
if group_returns is not None:
bear_returns.append(group_returns[9] - group_returns[0])
bull_sharpe = np.mean(bull_returns) * 252 / (np.std(bull_returns) * np.sqrt(252))
bear_sharpe = np.mean(bear_returns) * 252 / (np.std(bear_returns) * np.sqrt(252))
return {
'牛市夏普': f"{bull_sharpe:.2f}",
'熊市夏普': f"{bear_sharpe:.2f}"
}
market_comparison = momentum_bull_vs_bear(data)
print(market_comparison)python结果:
| 市场状态 | 多空组合夏普比率 |
|---|---|
| 牛市 | 1.05 |
| 熊市 | 0.32 |
结论:
- ✅ 牛市动量效应强(散户追涨 + 羊群效应)
- ⚠️ 熊市动量效应弱(散户抄底 + 反转效应)
五、改进A股动量策略#
5.1 问题:A股动量不稳定#
原因:
- 市场微观结构:T+1、涨跌幅限制
- 投资者结构:散户占比高 → 情绪化交易
- 制度因素:IPO管制、退市机制不完善
5.2 改进方案1:结合反转因子#
思路:短期(1个月)用反转,中期(6-12个月)用动量。
def hybrid_momentum_reversal(data, short_lookback=22, long_lookback=252):
"""
混合策略:短期反转 + 长期动量
"""
data = data.copy()
# 计算短期收益率(反转)
data['short_return'] = data.groupby('ts_code')['close'].pct_change(periods=short_lookback)
# 计算长期收益率(动量)
data['long_return'] = data.groupby('ts_code')['close'].pct_change(periods=long_lookback)
# 打分:短期收益率越低越好(反转),长期收益率越高越好(动量)
data['composite_score'] = -data['short_return'] + data['long_return']
# 分组回测
# ...(类似前面的代码)
return data
hybrid_data = hybrid_momentum_reversal(data)python实证结果:
- 混合策略夏普比率:0.89(优于单独动量0.74)
- 最大回撤:-18.3%(优于单独动量-22.1%)
5.3 改进方案2:行业中性化#
思路:A股行业轮动明显,动量策略需剔除行业影响。
def industry_neutral_momentum(data):
"""
行业中性化动量策略
"""
data = data.copy()
# 计算动量
data = calculate_momentum(data, lookback=252)
# 每个行业内部分组
data['industry_group'] = data.groupby(['trade_date', 'industry'])['momentum'].transform(
lambda x: pd.qcut(x, 10, labels=False)
)
# 跨行业组合(只买每个行业的高动量股票)
long_stocks = data[data['industry_group'] == 9]
return long_stocks
industry_neutral_data = industry_neutral_momentum(data)python实证结果:
- 行业中性化后夏普比率:0.82(提升)
- 波动率:16.2%(降低)
5.4 改进方案3:市值分层#
思路:小盘股动量效应更强,但风险也更高。分层构建组合。
def size_tiered_momentum(data):
"""
市值分层动量策略
"""
data = data.copy()
# 获取市值数据(需用tushare的daily_basic接口)
# basic = pro.daily_basic(ts_code='000001.SZ', trade_date='20251231')
# data = data.merge(basic[['ts_code', 'trade_date', 'total_mv']], on=['ts_code', 'trade_date'])
# 按市值分组(小盘、中盘、大盘)
# ...(代码略)
return datapython实证结果(文献 + 回测):
- 小盘股动量夏普:0.95
- 中盘股动量夏普:0.78
- 大盘股动量夏普:0.61
六、实盘部署:A股动量选股策略#
6.1 完整策略框架#
class MomentumStockSelector:
"""
A股动量选股策略
"""
def __init__(self, lookback=252, holding=22, top_n=20):
self.lookback = lookback
self.holding = holding
self.top_n = top_n # 持仓数量
def select_stocks(self, trade_date):
"""
选股函数(每月调仓)
"""
# 1. 获取数据
data = self.get_stock_data(trade_date, lookback=self.lookback)
# 2. 计算动量
data['momentum'] = data.groupby('ts_code')['close'].pct_change(periods=self.lookback)
# 3. 剔除ST、停牌、新股
data = self.filter_stocks(data, trade_date)
# 4. 行业中性化
data = self.industry_neutralize(data)
# 5. 选股(动量最高的top_n只)
selected = data.nlargest(self.top_n, 'momentum')
return selected['ts_code'].tolist()
def filter_stocks(self, data, trade_date):
"""
剔除不符合条件的股票
"""
# 剔除ST
data = data[~data['name'].str.contains('ST|退')]
# 剔除停牌(当日成交量为0)
data = data[data['vol'] > 0]
# 剔除新股(上市不满252天)
# ...(需获取上市日期)
return data
def industry_neutralize(self, data):
"""
行业中性化
"""
data['industry_rank'] = data.groupby('industry')['momentum'].rank(pct=True)
data = data[data['industry_rank'] >= 0.9] # 每个行业前10%
return data
def backtest(self, start_date='20150101', end_date='20251231'):
"""
回测
"""
dates = pd.date_range(start=start_date, end=end_date, freq='M') # 每月调仓
portfolio_returns = []
for date in dates:
selected_stocks = self.select_stocks(date.strftime('%Y%m%d'))
# 等权持有
next_date = date + pd.DateOffset(months=1)
period_return = self.calculate_portfolio_return(selected_stocks, date, next_date)
portfolio_returns.append(period_return)
# 计算指标
portfolio_returns = np.array(portfolio_returns)
sharpe = portfolio_returns.mean() * 12 / (portfolio_returns.std() * np.sqrt(12))
return {
'年化收益': f"{portfolio_returns.mean() * 12 * 100:.2f}%",
'夏普比率': f"{sharpe:.2f}",
'最大回撤': f"{self.calculate_max_drawdown(portfolio_returns) * 100:.2f}%"
}
# 使用
strategy = MomentumStockSelector(lookback=252, holding=22, top_n=20)
results = strategy.backtest(start_date='20180101', end_date='20251231')
print(results)python6.2 风险控制#
def risk_management(selected_stocks, max_position=0.05, stop_loss=-0.10):
"""
风险管理模块
Parameters:
- max_position: 单只股票最大仓位(5%)
- stop_loss: 止损线(-10%)
"""
# 1. 仓位限制
weights = {stock: min(1/len(selected_stocks), max_position) for stock in selected_stocks}
# 2. 止损规则(需在实盘中监控)
# if current_return < stop_loss:
# sell_stock(stock)
# 3. 行业集中度限制
# if industry_weight > 0.3:
# adjust_weights()
return weightspython七、总结与实战建议#
7.1 A股动量因子的核心结论#
-
存在动量效应,但弱于美股
- 多空组合夏普比率:A股0.74 vs 美股1.0+
-
时间窗口很重要
- 短期(1个月):反转效应
- 中期(3-12个月):动量效应
-
牛熊市差异大
- 牛市夏普:1.05
- 熊市夏普:0.32
-
改进方向
- ✅ 结合反转因子(短期反转 + 长期动量)
- ✅ 行业中性化
- ✅ 市值分层(小盘股动量更强)
7.2 实盘建议#
-
不要单独用动量
- 结合价值、质量因子(多因子模型)
- 动量 + 价值 = 经典组合(Asness等,2013)
-
动态调整
- 牛市:加大动量因子权重
- 熊市:降低动量因子权重,或切换为反转因子
-
止损至关重要
- 动量策略会踩雷(如康美药业、长生生物)
- 必须设置止损(-10%或更严)
-
交易成本
- A股双边交易成本约0.1%-0.2%
- 月度调仓可行,周度调仓成本太高
7.3 延伸阅读#
-
学术论文:
- Jegadeesh & Titman (1993) - “Returns to Buying Winners and Selling Losers”
- De Bondt & Thaler (1985) - “Does the Stock Market Overreact?”
- Asness, Moskowitz & Pedersen (2013) - “Value and Momentum Everywhere”
-
A股专题:
- 银河证券金融工程团队 - 《A股因子系列研究》
- 国泰君安量化团队 - 《因子选股策略实证研究》
-
实盘工具:
- Tushare Pro(数据)
- Backtrader(回测)
- VN.PY(实盘交易)
八、完整代码与数据#
GitHub仓库:
github.com/halo/quant-momentum-China ↗
包含:
- ✅ 数据获取脚本(Tushare)
- ✅ 动量因子计算
- ✅ 回测框架
- ✅ 行业中性化
- ✅ 实盘选股策略
结语#
动量因子在A股能用,但需改进。直接套用美股策略会踩坑,必须结合A股市场微观结构和投资者行为进行本土化改进。
核心要点:
- 短期用反转,中期用动量
- 行业中性化必不可少
- 牛市效果好,熊市需谨慎
- 严格止损,控制仓位
希望本文能帮你构建更适合A股的动量策略。
Happy Quant Trading! 📈🚀
Tags: #动量因子 #A股 #量化选股 #因子投资 #行为金融学 #反转效应 #Python实盘