在量化投资领域,风格轮动策略一直是机构投资者的重要工具。顶级投行JP Morgan的研究报告显示,通过系统性的风格轮动策略,可以在不同市场环境下获得超额收益。本文将带你用Python完整复现这一策略的回测系统,从数据获取到策略实现,再到完整的回测分析,每个环节都提供可运行的代码示例。

无论你是量化投资新手,还是有一定基础的开发者,都能通过本文掌握风格轮动策略的核心原理和实现方法。我们将使用Python的主流量化库,构建一个完整的回测框架,让你能够验证自己的投资想法。

1. 风格轮动策略基础概念

1.1 什么是风格轮动

风格轮动(Style Rotation)是一种基于市场风格变化的投资策略。它基于一个核心观察:不同市场环境下,某些投资风格(如价值型、成长型、小盘股、大盘股等)的表现会呈现周期性轮动特征。

在牛市初期,成长型股票往往表现更好;而在市场不确定性增加时,价值型股票可能更具防御性。风格轮动策略就是试图捕捉这些风格转换的时机,及时调整投资组合。

1.2 JP Morgan研报的核心发现

根据JP Morgan的研究,风格轮动策略的成功关键在于三个核心要素:

  1. 风格因子识别 :准确定义和度量不同的投资风格
  2. 轮动信号生成 :基于宏观经济指标、市场情绪等构建有效的轮动信号
  3. 风险控制 :在风格转换期间控制回撤和波动率

研报中提到的典型风格包括:

  • 价值 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 实战部署建议

将回测策略转化为实盘交易需要注意:

  1. 交易成本考虑 :包括佣金、滑点、冲击成本
  2. 数据延迟处理 :实盘数据的及时性和质量
  3. 仓位管理 :分批建仓、止损止盈
  4. 监控体系 :实时监控策略表现和风险指标
# 实盘交易建议的配置示例
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风格轮动策略的回测系统,从数据获取、策略开发到风险控制提供了全套代码。在实际应用中,建议从小资金开始验证,逐步优化参数和风控规则。风格轮动策略的有效性依赖于市场风格的持续性,需要持续监控和调整。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐