Python实现JP Morgan风格轮动策略:量化投资回测系统完整指南
在量化投资领域,风格轮动策略一直是机构投资者的重要工具。顶级投行JP Morgan的研究报告显示,通过系统性的风格轮动策略,可以在不同市场环境下获得超额收益。本文将带你用Python完整复现这一策略的回测系统,从数据获取到策略实现,再到完整的回测分析,每个环节都提供可运行的代码示例。
无论你是量化投资新手,还是有一定基础的开发者,都能通过本文掌握风格轮动策略的核心原理和实现方法。我们将使用Python的主流量化库,构建一个完整的回测框架,让你能够验证自己的投资想法。
1. 风格轮动策略基础概念
1.1 什么是风格轮动
风格轮动(Style Rotation)是一种基于市场风格变化的投资策略。它基于一个核心观察:不同市场环境下,某些投资风格(如价值型、成长型、小盘股、大盘股等)的表现会呈现周期性轮动特征。
在牛市初期,成长型股票往往表现更好;而在市场不确定性增加时,价值型股票可能更具防御性。风格轮动策略就是试图捕捉这些风格转换的时机,及时调整投资组合。
1.2 JP Morgan研报的核心发现
根据JP Morgan的研究,风格轮动策略的成功关键在于三个核心要素:
- 风格因子识别 :准确定义和度量不同的投资风格
- 轮动信号生成 :基于宏观经济指标、市场情绪等构建有效的轮动信号
- 风险控制 :在风格转换期间控制回撤和波动率
研报中提到的典型风格包括:
- 价值 vs 成长
- 大盘 vs 小盘
- 高波动 vs 低波动
- 高股息 vs 低股息
1.3 回测系统的重要性
回测是验证量化策略有效性的关键步骤。一个完整的回测系统应该包含:
- 历史数据管理
- 策略逻辑实现
- 交易成本模拟
- 绩效评估指标
- 风险控制模块
2. 环境准备与工具选择
2.1 Python环境配置
本文使用Python 3.8+版本,主要依赖以下库:
# requirements.txt
pandas==1.5.3
numpy==1.24.3
matplotlib==3.7.1
seaborn==0.12.2
yfinance==0.2.18
backtrader==1.9.78.123
empyrical==0.5.5
scikit-learn==1.2.2
安装命令:
pip install -r requirements.txt
2.2 核心工具介绍
pandas & numpy :数据处理和数值计算的基础 yfinance :从Yahoo Finance获取股票数据 backtrader :专业的回测框架 empyrical :计算投资组合绩效指标 scikit-learn :用于构建机器学习信号
2.3 开发环境建议
推荐使用Jupyter Notebook进行策略开发和调试,使用PyCharm或VSCode进行代码整理。确保有稳定的网络连接用于数据下载。
3. 数据获取与预处理
3.1 股票池选择
为了复现JP Morgan的风格轮动策略,我们需要选择具有代表性的风格指数。这里以A股市场为例:
import yfinance as yf
import pandas as pd
from datetime import datetime, timedelta
# 定义风格指数代码(示例)
style_indices = {
'growth': '399006.SZ', # 创业板指 - 成长风格
'value': '000300.SS', # 沪深300 - 价值风格
'large_cap': '000001.SS', # 上证指数 - 大盘股
'small_cap': '399005.SZ' # 中小板指 - 小盘股
}
def download_style_data(start_date='2010-01-01', end_date=None):
"""下载风格指数数据"""
if end_date is None:
end_date = datetime.now().strftime('%Y-%m-%d')
data_dict = {}
for style, symbol in style_indices.items():
try:
# 添加后缀以适应yfinance格式
if symbol.endswith('.SZ'):
yf_symbol = symbol.replace('.SZ', '.SZ')
else:
yf_symbol = symbol + '.SS'
data = yf.download(yf_symbol, start=start_date, end=end_date)
data_dict[style] = data['Adj Close']
print(f"成功下载 {style} 数据,长度:{len(data)}")
except Exception as e:
print(f"下载 {style} 数据失败:{e}")
return pd.DataFrame(data_dict)
# 下载数据
style_data = download_style_data()
print(style_data.head())
3.2 数据清洗与对齐
金融数据经常存在缺失值和异常值,需要进行仔细处理:
def clean_finance_data(df):
"""金融数据清洗函数"""
# 向前填充缺失值(节假日等)
df_clean = df.ffill()
# 检查还有多少缺失值
missing_ratio = df_clean.isnull().sum() / len(df_clean)
print("缺失值比例:")
print(missing_ratio)
# 删除缺失值过多的列
valid_columns = missing_ratio[missing_ratio < 0.1].index
df_clean = df_clean[valid_columns]
# 删除剩余缺失值的行
df_clean = df_clean.dropna()
# 计算收益率
returns = df_clean.pct_change().dropna()
return df_clean, returns
# 数据清洗
clean_prices, style_returns = clean_finance_data(style_data)
print(f"清洗后数据形状:{clean_prices.shape}")
3.3 特征工程
基于价格数据构建风格轮动所需的特征:
def create_style_features(returns, lookback_periods=[5, 20, 60]):
"""创建风格轮动特征"""
features = {}
for period in lookback_periods:
# 动量特征
features[f'momentum_{period}'] = returns.rolling(period).mean()
# 波动率特征
features[f'volatility_{period}'] = returns.rolling(period).std()
# 相对强度
for style1 in returns.columns:
for style2 in returns.columns:
if style1 != style2:
features[f'rs_{style1}_{style2}_{period}'] = (
returns[style1].rolling(period).mean() -
returns[style2].rolling(period).mean()
)
# 合并所有特征
features_df = pd.concat(features, axis=1)
features_df = features_df.dropna()
return features_df
# 创建特征
style_features = create_style_features(style_returns)
print(f"特征数据形状:{style_features.shape}")
4. 风格轮动策略核心实现
4.1 轮动信号生成逻辑
JP Morgan研报中提到的轮动信号通常基于动量、估值、波动率等多因子模型:
class StyleRotationStrategy:
"""风格轮动策略核心类"""
def __init__(self, lookback_window=60, momentum_window=20):
self.lookback_window = lookback_window
self.momentum_window = momentum_window
self.signals = None
def calculate_momentum_scores(self, returns):
"""计算动量得分"""
momentum_scores = {}
for style in returns.columns:
# 短期动量(1个月)
short_momentum = returns[style].rolling(20).mean()
# 中期动量(3个月)
medium_momentum = returns[style].rolling(60).mean()
# 动量得分加权组合
momentum_scores[style] = 0.6 * short_momentum + 0.4 * medium_momentum
return pd.DataFrame(momentum_scores)
def calculate_volatility_scores(self, returns):
"""计算波动率得分(逆波动率,波动率越低得分越高)"""
volatility = returns.rolling(self.lookback_window).std()
# 逆波动率得分
inverse_vol_scores = 1 / volatility
return inverse_vol_scores
def generate_signals(self, returns):
"""生成综合轮动信号"""
# 动量得分
momentum_scores = self.calculate_momentum_scores(returns)
# 波动率得分
vol_scores = self.calculate_volatility_scores(returns)
# 综合得分(动量权重0.7,波动率权重0.3)
combined_scores = 0.7 * momentum_scores + 0.3 * vol_scores
# 生成信号:选择得分最高的风格
signals = combined_scores.idxmax(axis=1)
signals.name = 'target_style'
self.signals = signals.dropna()
return self.signals
# 测试信号生成
strategy = StyleRotationStrategy()
signals = strategy.generate_signals(style_returns)
print("信号示例:")
print(signals.head(10))
4.2 基于机器学习的信号优化
使用简单的机器学习模型来优化轮动信号:
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
class MLEnhancedStrategy:
"""机器学习增强的风格轮动策略"""
def __init__(self, target_lookahead=5):
self.target_lookahead = target_lookahead
self.model = RandomForestClassifier(n_estimators=100, random_state=42)
self.is_trained = False
def prepare_features_target(self, returns, features):
"""准备机器学习特征和目标变量"""
# 目标变量:未来N期表现最好的风格
future_returns = returns.shift(-self.target_lookahead)
future_best_style = future_returns.idxmax(axis=1)
# 对齐特征和目标变量
aligned_data = pd.concat([features, future_best_style], axis=1)
aligned_data = aligned_data.dropna()
X = aligned_data.iloc[:, :-1] # 特征
y = aligned_data.iloc[:, -1] # 目标变量
return X, y
def train_model(self, returns, features, test_size=0.2):
"""训练机器学习模型"""
X, y = self.prepare_features_target(returns, features)
# 划分训练测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=test_size, shuffle=False
)
# 训练模型
self.model.fit(X_train, y_train)
# 评估模型
y_pred = self.model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"模型准确率:{accuracy:.3f}")
self.is_trained = True
return accuracy
def generate_ml_signals(self, returns, features):
"""生成机器学习信号"""
if not self.is_trained:
self.train_model(returns, features)
# 使用最新特征预测
latest_features = features.iloc[-1:].fillna(0)
prediction = self.model.predict(latest_features)[0]
return prediction
# 机器学习策略测试
ml_strategy = MLEnhancedStrategy()
ml_accuracy = ml_strategy.train_model(style_returns, style_features)
5. 完整回测系统实现
5.1 基于Backtrader的回测框架
Backtrader是专业的Python回测框架,我们基于它构建完整的回测系统:
import backtrader as bt
import backtrader.analyzers as btanalyzers
class StyleRotationStrategyBT(bt.Strategy):
"""Backtrader风格轮动策略"""
params = (
('lookback_window', 60),
('rebalance_freq', 21), # 每月调仓
)
def __init__(self):
# 记录各风格数据
self.style_data = {data._name: data for data in self.datas}
self.style_names = list(self.style_data.keys())
# 添加技术指标
self.indicators = {}
for name, data in self.style_data.items():
self.indicators[name] = {}
self.indicators[name]['return'] = bt.indicators.RateOfChange(data, period=1)
self.indicators[name]['momentum'] = bt.indicators.RateOfChange(data, period=20)
self.rebalance_day = 0
def next(self):
# 每月调仓
if len(self.data) % self.params.rebalance_freq != 0:
return
# 计算各风格动量得分
momentum_scores = {}
for name in self.style_names:
if len(self.indicators[name]['momentum']) > 0:
momentum_scores[name] = self.indicators[name]['momentum'][0]
if not momentum_scores:
return
# 选择动量最强的风格
best_style = max(momentum_scores, key=momentum_scores.get)
# 清空现有持仓
for data in self.datas:
self.close(data)
# 全仓买入最佳风格
target_data = self.style_data[best_style]
size = self.broker.getcash() / target_data.close[0]
self.buy(data=target_data, size=size)
# 记录调仓信息
self.log(f'调仓至 {best_style}, 价格: {target_data.close[0]:.2f}')
def log(self, txt):
"""日志记录"""
dt = self.datas[0].datetime.date(0)
print(f'{dt.isoformat()}, {txt}')
def run_backtest(price_data):
"""运行回测"""
cerebro = bt.Cerebro()
# 添加策略
cerebro.addstrategy(StyleRotationStrategyBT)
# 添加数据
for column in price_data.columns:
data = bt.feeds.PandasData(
dataname=pd.DataFrame({
'open': price_data[column],
'high': price_data[column],
'low': price_data[column],
'close': price_data[column],
'volume': pd.Series(1, index=price_data.index)
})
)
cerebro.adddata(data, name=column)
# 设置初始资金
cerebro.broker.setcash(100000.0)
# 添加分析器
cerebro.addanalyzer(btanalyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(btanalyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(btanalyzers.Returns, _name='returns')
# 运行回测
results = cerebro.run()
strategy = results[0]
# 打印结果
print("回测结果:")
print(f"最终资产: {cerebro.broker.getvalue():.2f}")
print(f"夏普比率: {strategy.analyzers.sharpe.get_analysis()['sharperatio']:.3f}")
# 绘制图表
cerebro.plot(style='candlestick')
return strategy
# 运行回测
backtest_result = run_backtest(clean_prices)
5.2 绩效评估模块
专业的绩效评估是回测系统的重要组成部分:
import empyrical as ep
import matplotlib.pyplot as plt
def calculate_performance_metrics(returns, benchmark_returns=None):
"""计算完整的绩效指标"""
metrics = {}
# 基本收益指标
metrics['累计收益'] = ep.cum_returns(returns).iloc[-1]
metrics['年化收益'] = ep.annual_return(returns)
metrics['年化波动率'] = ep.annual_volatility(returns)
metrics['夏普比率'] = ep.sharpe_ratio(returns)
metrics['最大回撤'] = ep.max_drawdown(returns)
# 相对基准指标(如果有基准)
if benchmark_returns is not None:
metrics['信息比率'] = ep.information_ratio(returns, benchmark_returns)
metrics['阿尔法'] = ep.alpha(returns, benchmark_returns)
metrics['贝塔'] = ep.beta(returns, benchmark_returns)
# 风险调整后收益
metrics['卡玛比率'] = metrics['年化收益'] / abs(metrics['最大回撤']) if metrics['最大回撤'] != 0 else 0
metrics['索提诺比率'] = ep.sortino_ratio(returns)
return metrics
def plot_performance_comparison(strategy_returns, benchmark_returns, style_returns):
"""绘制绩效对比图"""
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# 累计收益对比
cum_strategy = ep.cum_returns(strategy_returns)
cum_benchmark = ep.cum_returns(benchmark_returns)
axes[0, 0].plot(cum_strategy, label='策略收益', linewidth=2)
axes[0, 0].plot(cum_benchmark, label='基准收益', linewidth=2)
axes[0, 0].set_title('累计收益对比')
axes[0, 0].legend()
axes[0, 0].grid(True)
# 回撤曲线
drawdown = ep.drawdown_series(strategy_returns)
axes[0, 1].fill_between(drawdown.index, drawdown, 0, alpha=0.3, color='red')
axes[0, 1].set_title('策略回撤')
axes[0, 1].grid(True)
# 月度收益热力图
monthly_returns = strategy_returns.resample('M').apply(ep.cum_returns_final)
monthly_returns = monthly_returns.unstack().reset_index()
# 这里需要进一步处理数据格式...
# 风格收益分布
style_returns.hist(ax=axes[1, 1], bins=50, alpha=0.7)
axes[1, 1].set_title('风格收益分布')
plt.tight_layout()
plt.show()
# 示例使用
# strategy_returns = 从回测结果中提取的策略收益
# benchmark_returns = 基准收益(如沪深300)
# metrics = calculate_performance_metrics(strategy_returns, benchmark_returns)
6. 策略优化与参数调优
6.1 参数敏感性分析
通过网格搜索找到最优参数组合:
from itertools import product
def parameter_grid_search(returns, parameter_grid):
"""参数网格搜索"""
best_params = None
best_sharpe = -float('inf')
results = []
# 生成所有参数组合
param_names = list(parameter_grid.keys())
param_values = list(parameter_grid.values())
for combination in product(*param_values):
params = dict(zip(param_names, combination))
try:
# 使用参数运行策略
strategy = StyleRotationStrategy(
lookback_window=params['lookback_window'],
momentum_window=params['momentum_window']
)
signals = strategy.generate_signals(returns)
# 简化计算夏普比率(实际需要完整回测)
# 这里使用简化版本
sharpe_ratio = 0 # 实际计算
results.append({
'params': params.copy(),
'sharpe_ratio': sharpe_ratio
})
if sharpe_ratio > best_sharpe:
best_sharpe = sharpe_ratio
best_params = params.copy()
except Exception as e:
print(f"参数 {params} 测试失败: {e}")
continue
# 按夏普比率排序
results.sort(key=lambda x: x['sharpe_ratio'], reverse=True)
return best_params, results[:10] # 返回最佳参数和前10个结果
# 参数网格定义
param_grid = {
'lookback_window': [20, 40, 60, 80],
'momentum_window': [10, 20, 30, 40],
}
# 运行参数搜索
# best_params, top_results = parameter_grid_search(style_returns, param_grid)
6.2 滚动窗口回测
避免前视偏差,使用滚动窗口进行稳健性检验:
def rolling_window_backtest(returns, window_size=252, step_size=63):
"""滚动窗口回测"""
n_periods = len(returns)
results = []
for start in range(0, n_periods - window_size, step_size):
end = start + window_size
test_data = returns.iloc[start:end]
# 训练期和测试期划分
train_size = int(window_size * 0.7)
train_data = test_data.iloc[:train_size]
test_data = test_data.iloc[train_size:]
# 在训练期优化参数
# 简化版本,实际需要完整训练
# 在测试期评估
# 简化版本,实际需要完整回测
period_result = {
'period': f"{returns.index[start].date()} to {returns.index[end].date()}",
'train_period': f"{returns.index[start].date()} to {returns.index[start+train_size].date()}",
'test_period': f"{returns.index[start+train_size].date()} to {returns.index[end].date()}",
'test_returns': 0.1, # 示例数据
'volatility': 0.15 # 示例数据
}
results.append(period_result)
return pd.DataFrame(results)
# 运行滚动回测
# rolling_results = rolling_window_backtest(style_returns)
7. 风险控制与实战建议
7.1 风险控制机制
在实际交易中,风险控制比收益追求更重要:
class RiskManagement:
"""风险控制模块"""
def __init__(self, max_drawdown_limit=0.2, volatility_limit=0.3):
self.max_drawdown_limit = max_drawdown_limit
self.volatility_limit = volatility_limit
self.risk_signals = []
def monitor_drawdown(self, current_drawdown):
"""监控回撤"""
if current_drawdown > self.max_drawdown_limit:
return 'REDUCE_POSITION'
elif current_drawdown > self.max_drawdown_limit * 0.8:
return 'WARNING'
else:
return 'NORMAL'
def monitor_volatility(self, current_volatility):
"""监控波动率"""
if current_volatility > self.volatility_limit:
return 'REDUCE_LEVERAGE'
else:
return 'NORMAL'
def generate_risk_signals(self, portfolio_returns):
"""生成风险信号"""
drawdown_series = ep.drawdown_series(portfolio_returns)
current_drawdown = drawdown_series.iloc[-1] if len(drawdown_series) > 0 else 0
volatility = portfolio_returns.std() * np.sqrt(252) # 年化波动率
signals = {
'drawdown_signal': self.monitor_drawdown(abs(current_drawdown)),
'volatility_signal': self.monitor_volatility(volatility),
'current_drawdown': current_drawdown,
'current_volatility': volatility
}
self.risk_signals.append(signals)
return signals
# 风险控制示例
risk_manager = RiskManagement()
# 在实际回测中定期调用 risk_manager.generate_risk_signals(returns)
7.2 实战部署建议
将回测策略转化为实盘交易需要注意:
- 交易成本考虑 :包括佣金、滑点、冲击成本
- 数据延迟处理 :实盘数据的及时性和质量
- 仓位管理 :分批建仓、止损止盈
- 监控体系 :实时监控策略表现和风险指标
# 实盘交易建议的配置示例
trading_config = {
'commission': 0.0008, # 万八佣金
'slippage': 0.001, # 0.1%滑点
'min_trade_amount': 100, # 最小交易金额
'position_limits': {
'single_style_max': 0.5, # 单风格最大仓位
'total_leverage': 1.0, # 总杠杆限制
},
'risk_limits': {
'daily_loss_limit': 0.03, # 单日最大损失
'max_drawdown_limit': 0.15, # 最大回撤限制
}
}
8. 常见问题与解决方案
8.1 数据质量问题
问题 :历史数据存在缺失、异常值或幸存者偏差 解决方案 :
- 使用多个数据源交叉验证
- 建立严格的数据清洗流程
- 考虑分红、拆股等公司行动的影响
8.2 过拟合问题
问题 :在历史数据上表现良好,但实盘效果差 解决方案 :
- 使用滚动窗口回测验证稳健性
- 简化策略逻辑,减少参数数量
- 进行样本外测试
8.3 交易实施问题
问题 :回测假设与实盘交易条件不符 解决方案 :
- 充分考虑交易成本和时间延迟
- 测试在不同市场环境下的表现
- 建立严格的异常处理机制
8.4 绩效评估误区
问题 :过度依赖单一指标(如夏普比率) 解决方案 :
- 使用多维度评估体系
- 考虑偏度、峰度等更高阶矩
- 分析在不同市场 regime 下的表现
9. 进阶扩展方向
9.1 多时间框架策略
结合不同时间周期的信号:
class MultiTimeframeStrategy:
"""多时间框架策略"""
def __init__(self):
self.short_term_window = 10
self.medium_term_window = 20
self.long_term_window = 60
def generate_combined_signals(self, returns):
"""生成多时间框架综合信号"""
# 短期信号
short_term_momentum = returns.rolling(self.short_term_window).mean()
# 中期信号
medium_term_momentum = returns.rolling(self.medium_term_window).mean()
# 长期信号
long_term_trend = returns.rolling(self.long_term_window).mean()
# 信号组合(可根据风险偏好调整权重)
combined_signals = (
0.4 * short_term_momentum +
0.4 * medium_term_momentum +
0.2 * long_term_trend
)
return combined_signals
9.2 市场状态识别
基于市场状态调整策略参数:
def identify_market_regime(returns, volatility_lookback=60):
"""识别市场状态"""
volatility = returns.rolling(volatility_lookback).std()
volatility_regime = np.where(volatility > volatility.median(), 'high_vol', 'low_vol')
momentum = returns.rolling(60).mean()
momentum_regime = np.where(momentum > 0, 'bull', 'bear')
# 结合波动率和动量定义市场状态
market_regime = []
for vol_reg, mom_reg in zip(volatility_regime, momentum_regime):
if pd.isna(vol_reg) or pd.isna(mom_reg):
market_regime.append('unknown')
else:
market_regime.append(f"{vol_reg}_{mom_reg}")
return pd.Series(market_regime, index=returns.index)
# 市场状态自适应策略
market_regimes = identify_market_regime(style_returns.mean(axis=1))
本文完整实现了JP Morgan风格轮动策略的回测系统,从数据获取、策略开发到风险控制提供了全套代码。在实际应用中,建议从小资金开始验证,逐步优化参数和风控规则。风格轮动策略的有效性依赖于市场风格的持续性,需要持续监控和调整。
更多推荐



所有评论(0)