那天下午,我盯着屏幕上的券商研报,心里冒出一个念头:这些报告写得天花乱坠,但真正能跑起来的代码在哪里?特别是那个“ETF双因子轮动策略”,听起来很美好——动量因子抓趋势,质量因子控风险,理论上能跑赢市场。但当我真的想复现时,发现报告里只有理念框架,缺少可落地的完整流程。
这不仅仅是写几行Python的问题。从数据获取、因子计算、信号生成,到仓位管理、回测验证、实盘衔接,每个环节都有细节决定成败。更重要的是,很多策略在回测里表现完美,一到实盘就出问题,往往是因为忽略了交易成本、滑点、或者因子计算的时间对齐问题。
所以,我决定把这次复现写成一篇能直接操作的指南。不只是给你代码,更重要的是解释每个步骤为什么要这样设计,哪里容易踩坑,以及如何把一次性的研究代码变成可重复使用的策略框架。
1. 先搞清楚双因子轮动到底在解决什么问题
很多人一看到“双因子”就觉得是高端操作,其实它的核心思想很朴素:用动量因子找到近期表现好的ETF,再用质量因子过滤掉波动过大或基本面不稳的品种,避免追高踩雷。
1.1 动量因子不是简单看涨跌幅
动量因子的常见误解是直接按过去N天的收益率排序。但这样做有两个问题:一是短期噪音大,二是没有考虑不同市场状态下的动量有效性。
更稳妥的做法是结合绝对动量与相对动量:
- 绝对动量:ETF自身在过去20日、60日的收益率
- 相对动量:与基准指数(如沪深300)的相对强弱
- 动量持续性:动量是否在多个时间维度一致
# 示例:动量因子计算框架
def calculate_momentum_factor(etf_data, benchmark_data, windows=[20, 60]):
momentum_scores = {}
for etf_code in etf_data.keys():
# 绝对动量
abs_momentum = []
for window in windows:
returns = etf_data[etf_code]['close'].pct_change(window).iloc[-1]
abs_momentum.append(returns)
# 相对动量(相对于基准)
rel_momentum = []
for window in windows:
etf_return = etf_data[etf_code]['close'].pct_change(window).iloc[-1]
bench_return = benchmark_data.pct_change(window).iloc[-1]
rel_momentum.append(etf_return - bench_return)
# 综合动量得分(可根据需要调整权重)
total_score = (sum(abs_momentum) * 0.6 + sum(rel_momentum) * 0.4) / len(windows)
momentum_scores[etf_code] = total_score
return momentum_scores
这个框架的好处是,你可以根据不同的市场环境调整权重。比如在牛市中,绝对动量权重可以高一些;在震荡市中,相对动量更重要。
1.2 质量因子真正过滤的是不确定性
质量因子常被简化为"低波动",但其实应该包含更多维度:
- 波动率:过去60日年化波动率,过滤异常波动的品种
- 流动性:日均成交金额,避免无法正常买卖
- 规模:ETF资产规模,过小的有清盘风险
- 跟踪误差:与标的指数的偏离程度
def calculate_quality_factor(etf_data, windows=60):
quality_scores = {}
for etf_code in etf_data.keys():
returns = etf_data[etf_code]['close'].pct_change()
# 年化波动率(负向指标,波动越小得分越高)
volatility = returns.std() * np.sqrt(252)
volatility_score = 1 / (1 + volatility) # 波动率倒数作为得分
# 流动性(日均成交额)
avg_volume = etf_data[etf_code]['volume'].mean()
liquidity_score = min(avg_volume / 1e8, 1) # 归一化处理
# 综合质量得分
quality_scores[etf_code] = volatility_score * 0.7 + liquidity_score * 0.3
return quality_scores
质量因子的关键不是追求绝对的低波动,而是在动量因子选出的候选标的中,剔除风险过高的品种。
1.3 双因子结合的关键是权重动态调整
单纯把两个因子得分加权平均是最简单的做法,但更好的方式是根据市场状态动态调整因子权重:
def dynamic_factor_weighting(market_status):
"""根据市场状态调整因子权重"""
if market_status == 'bull':
# 牛市中动量因子权重更高
return {'momentum': 0.7, 'quality': 0.3}
elif market_status == 'bear':
# 熊市中质量因子权重更高
return {'momentum': 0.3, 'quality': 0.7}
else:
# 震荡市均衡配置
return {'momentum': 0.5, 'quality': 0.5}
市场状态的判断可以用简单的均线系统,比如沪深300指数在20日均线上方为牛市,下方为熊市,震荡区间为震荡市。
2. 数据获取与处理的工程化细节
策略失效的常见原因不是逻辑问题,而是数据问题。特别是时间对齐、复权处理、异常值过滤这些细节。
2.1 构建稳定的数据获取管道
直接从交易软件获取数据虽然方便,但长期运行不稳定。建议使用专业的量化数据接口,并建立本地缓存机制:
import pandas as pd
import os
from datetime import datetime, timedelta
class ETFDataPipeline:
def __init__(self, data_dir='./etf_data'):
self.data_dir = data_dir
os.makedirs(data_dir, exist_ok=True)
def get_etf_list(self):
"""获取ETF基础信息列表"""
# 这里可以维护一个核心ETF池
etf_pool = {
'510300': '沪深300ETF',
'510500': '中证500ETF',
'512100': '中证1000ETF',
'512880': '证券ETF',
'515000': '科技ETF',
# ... 其他ETF
}
return etf_pool
def download_daily_data(self, etf_code, start_date, end_date):
"""下载日线数据并缓存"""
cache_file = f"{self.data_dir}/{etf_code}_{start_date}_{end_date}.csv"
if os.path.exists(cache_file):
# 从缓存读取
data = pd.read_csv(cache_file, index_col='date', parse_dates=True)
return data
# 实际数据获取逻辑(需要接入具体数据源)
data = self._fetch_from_api(etf_code, start_date, end_date)
# 保存到缓存
data.to_csv(cache_file)
return data
def validate_data_quality(self, data):
"""验证数据质量"""
checks = []
# 检查缺失值
missing_ratio = data.isnull().sum() / len(data)
checks.append(missing_ratio.max() < 0.05) # 缺失率低于5%
# 检查价格连续性
price_changes = data['close'].pct_change().abs()
extreme_moves = (price_changes > 0.2).sum() # 单日涨跌幅超过20%的次数
checks.append(extreme_moves < len(data) * 0.01) # 异常波动少于1%
return all(checks)
注意:数据获取一定要考虑复权处理。ETF会有分红再投资,使用前复权数据能更准确反映真实收益。
2.2 处理时间对齐和幸存者偏差
回测中常见的问题是使用未来数据。确保每个时间点只能使用当时已经存在的数据:
def avoid_lookahead_bias(etf_data, current_date):
"""避免使用未来数据"""
valid_etfs = {}
for etf_code, data in etf_data.items():
# 只使用在当前日期之前已经上市且有足够历史数据的ETF
if data.index[0] < current_date - timedelta(days=100): # 至少100天历史
historical_data = data[data.index <= current_date]
if len(historical_data) >= 60: # 至少有60天数据用于计算因子
valid_etfs[etf_code] = historical_data
return valid_etfs
2.3 数据异常值处理
市场数据中常有异常值,需要稳健的处理方法:
def robust_data_cleaning(data, method='iqr'):
"""稳健的数据清洗"""
cleaned_data = data.copy()
if method == 'iqr':
# IQR方法处理异常值
Q1 = data.quantile(0.25)
Q3 = data.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 将异常值限制在边界内
cleaned_data = data.clip(lower_bound, upper_bound)
return cleaned_data
3. 策略回测的完整实现
回测不是简单的信号模拟,要包含完整的交易闭环。
3.1 构建事件驱动的回测框架
class BacktestEngine:
def __init__(self, initial_capital=1000000, transaction_cost=0.001):
self.initial_capital = initial_capital
self.transaction_cost = transaction_cost # 交易成本(千分之一)
self.positions = {} # 当前持仓
self.cash = initial_capital # 现金
self.portfolio_value = [] # 组合净值记录
self.trade_log = [] # 交易记录
def run_backtest(self, signals, price_data):
"""运行回测"""
dates = sorted(signals.keys())
for date in dates:
current_prices = {}
for etf in signals[date].keys():
if etf in price_data and date in price_data[etf].index:
current_prices[etf] = price_data[etf].loc[date]['close']
if not current_prices:
continue
# 执行调仓逻辑
self.rebalance_portfolio(signals[date], current_prices, date)
# 记录组合净值
self.record_portfolio_value(current_prices, date)
def rebalance_portfolio(self, target_weights, current_prices, date):
"""组合再平衡"""
# 计算当前持仓价值
current_value = self.cash
for etf, shares in self.positions.items():
if etf in current_prices:
current_value += shares * current_prices[etf]
# 计算目标持仓
target_shares = {}
for etf, weight in target_weights.items():
if etf in current_prices:
target_value = current_value * weight
target_shares[etf] = target_value // current_prices[etf]
# 执行交易
self.execute_trades(target_shares, current_prices, date)
def execute_trades(self, target_shares, current_prices, date):
"""执行交易"""
# 先卖出需要减仓的品种
for etf, current_shares in self.positions.items():
target_shares_count = target_shares.get(etf, 0)
if current_shares > target_shares_count:
sell_shares = current_shares - target_shares_count
sell_value = sell_shares * current_prices[etf]
self.cash += sell_value * (1 - self.transaction_cost)
self.log_trade(date, etf, 'SELL', sell_shares, current_prices[etf])
# 再买入需要加仓的品种
for etf, target_shares_count in target_shares.items():
current_shares = self.positions.get(etf, 0)
if target_shares_count > current_shares:
buy_shares = target_shares_count - current_shares
buy_value = buy_shares * current_prices[etf]
if self.cash >= buy_value * (1 + self.transaction_cost):
self.cash -= buy_value * (1 + self.transaction_cost)
self.log_trade(date, etf, 'BUY', buy_shares, current_prices[etf])
# 更新持仓
self.positions = {k: v for k, v in target_shares.items() if v > 0}
3.2 关键性能指标计算
回测结果不能只看收益率,要全面评估风险调整后收益:
def calculate_performance_metrics(portfolio_values, benchmark_returns, risk_free_rate=0.03):
"""计算关键性能指标"""
returns = portfolio_values.pct_change().dropna()
metrics = {}
# 年化收益率
total_return = (portfolio_values.iloc[-1] / portfolio_values.iloc[0] - 1)
annual_return = (1 + total_return) ** (252/len(returns)) - 1
metrics['年化收益率'] = annual_return
# 年化波动率
annual_volatility = returns.std() * np.sqrt(252)
metrics['年化波动率'] = annual_volatility
# 夏普比率
sharpe_ratio = (annual_return - risk_free_rate) / annual_volatility
metrics['夏普比率'] = sharpe_ratio
# 最大回撤
cumulative_returns = (1 + returns).cumprod()
peak = cumulative_returns.expanding().max()
drawdown = (cumulative_returns - peak) / peak
max_drawdown = drawdown.min()
metrics['最大回撤'] = max_drawdown
# 相对于基准的Alpha、Beta
if len(benchmark_returns) == len(returns):
covariance = returns.cov(benchmark_returns)
benchmark_variance = benchmark_returns.var()
beta = covariance / benchmark_variance
alpha = annual_return - (risk_free_rate + beta * (benchmark_returns.mean()*252 - risk_free_rate))
metrics['Alpha'] = alpha
metrics['Beta'] = beta
return metrics
3.3 回测验证的常见陷阱
很多策略在回测中表现完美,实盘却失败,常见原因包括:
- 过拟合 :在有限数据上过度优化参数
def avoid_overfitting(returns, n_splits=5):
"""使用时间序列交叉验证避免过拟合"""
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=n_splits)
performance_scores = []
for train_idx, test_idx in tscv.split(returns):
train_returns = returns.iloc[train_idx]
test_returns = returns.iloc[test_idx]
# 在训练集上优化参数
best_params = optimize_parameters(train_returns)
# 在测试集上验证
test_performance = evaluate_strategy(test_returns, best_params)
performance_scores.append(test_performance)
return np.mean(performance_scores)
- 忽略交易成本 :频繁调仓的成本会侵蚀收益
- 幸存者偏差 :只使用现在还存在ETF,忽略了已退市品种
4. 从回测到实盘的工程化改造
研究代码和实盘代码是两回事。实盘需要额外的稳健性设计。
4.1 构建容错机制
class ProductionStrategy:
def __init__(self, fallback_strategy=None):
self.fallback_strategy = fallback_strategy # 备用策略
def generate_signals(self, market_data):
try:
# 主策略逻辑
signals = self.main_strategy(market_data)
# 信号验证
if self.validate_signals(signals):
return signals
else:
return self.fallback_strategy.generate_signals(market_data)
except Exception as e:
logging.error(f"策略执行错误: {e}")
# 返回保守信号或保持现有持仓
return self.get_conservative_signals()
def validate_signals(self, signals):
"""验证信号合理性"""
if not signals:
return False
# 检查权重和为1
total_weight = sum(signals.values())
if abs(total_weight - 1.0) > 0.01: # 允许1%的误差
return False
# 检查单个权重不超过50%(避免过度集中)
if max(signals.values()) > 0.5:
return False
return True
4.2 实盘风控设计
实盘必须包含多层次风控:
class RiskManager:
def __init__(self, max_position_size=0.3, max_daily_loss=0.05):
self.max_position_size = max_position_size # 单品种最大仓位
self.max_daily_loss = max_daily_loss # 单日最大损失
def check_position_risk(self, positions, prices):
"""检查持仓风险"""
total_value = sum(shares * prices[etf] for etf, shares in positions.items())
for etf, shares in positions.items():
position_value = shares * prices[etf]
position_size = position_value / total_value
if position_size > self.max_position_size:
return False, f"{etf}仓位超过限制"
return True, "风险检查通过"
def check_market_condition(self, market_data):
"""检查市场状态"""
# 监控市场波动率
recent_volatility = market_data['close'].pct_change().std() * np.sqrt(252)
if recent_volatility > 0.5: # 年化波动率超过50%
return "high_volatility", "市场波动过大,建议降低仓位"
return "normal", "市场状态正常"
4.3 监控与日志系统
实盘策略需要完善的监控:
import logging
from datetime import datetime
class StrategyMonitor:
def __init__(self, strategy_name):
self.strategy_name = strategy_name
self.setup_logging()
def setup_logging(self):
logging.basicConfig(
filename=f'{self.strategy_name}_{datetime.now().strftime("%Y%m%d")}.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def log_signal_generation(self, signals, timestamp):
logging.info(f"信号生成: {signals}")
def log_trade_execution(self, trades, execution_prices):
for trade in trades:
logging.info(f"交易执行: {trade} @ {execution_prices[trade['etf']]}")
def log_performance(self, portfolio_value, benchmark_value):
relative_perf = portfolio_value / benchmark_value - 1
logging.info(f"相对收益: {relative_perf:.2%}")
5. 策略优化与持续改进
一个好的策略不是一成不变的,需要建立持续优化的框架。
5.1 参数敏感度分析
def parameter_sensitivity_analysis(strategy_func, base_params, param_ranges, historical_data):
"""分析参数敏感度"""
results = []
for param_name, range_values in param_ranges.items():
param_results = []
for value in range_values:
test_params = base_params.copy()
test_params[param_name] = value
# 运行回测
performance = strategy_func(historical_data, test_params)
param_results.append({
'param_value': value,
'performance': performance
})
# 分析该参数对性能的影响
sensitivity = analyze_parameter_sensitivity(param_results)
results.append({
'parameter': param_name,
'sensitivity': sensitivity,
'optimal_range': find_optimal_range(param_results)
})
return results
5.2 市场适应性测试
策略在不同市场环境下的表现:
def market_regime_testing(strategy, market_data):
"""测试策略在不同市场环境下的表现"""
regimes = identify_market_regimes(market_data)
regime_performance = {}
for regime_name, regime_periods in regimes.items():
regime_returns = []
for start_date, end_date in regime_periods:
regime_data = market_data[start_date:end_date]
returns = strategy.backtest(regime_data)
regime_returns.append(returns)
regime_performance[regime_name] = {
'平均收益': np.mean(regime_returns),
'胜率': len([r for r in regime_returns if r > 0]) / len(regime_returns),
'收益波动率': np.std(regime_returns)
}
return regime_performance
5.3 建立策略评估矩阵
用多个维度评估策略质量:
| 评估维度 | 指标 | 目标值 | 实际值 |
|---|---|---|---|
| 收益能力 | 年化收益率 | >15% | |
| 风险控制 | 最大回撤 | <20% | |
| 风险调整收益 | 夏普比率 | >1.0 | |
| 稳定性 | 月胜率 | >60% | |
| 容量 | 最大资金容量 | >1000万 | |
| 适应性 | 不同市场环境表现 | 均衡 |
这个评估框架帮你避免只看收益的片面评价,全面衡量策略的实战价值。
6. 完整代码框架与使用指南
最后给出可以直接使用的代码框架:
# etf_double_factor.py
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
class ETFDoubleFactorStrategy:
def __init__(self, momentum_windows=[20, 60], quality_windows=60):
self.momentum_windows = momentum_windows
self.quality_windows = quality_windows
self.data_pipeline = ETFDataPipeline()
def prepare_data(self, start_date, end_date):
"""准备数据"""
etf_list = self.data_pipeline.get_etf_list()
market_data = {}
for etf_code in etf_list.keys():
data = self.data_pipeline.download_daily_data(etf_code, start_date, end_date)
if self.data_pipeline.validate_data_quality(data):
market_data[etf_code] = data
return market_data
def calculate_factors(self, market_data, current_date):
"""计算双因子"""
# 动量因子
momentum_scores = calculate_momentum_factor(market_data, current_date)
# 质量因子
quality_scores = calculate_quality_factor(market_data, current_date)
return momentum_scores, quality_scores
def generate_signals(self, momentum_scores, quality_scores, top_n=5):
"""生成交易信号"""
# 综合得分
composite_scores = {}
for etf in momentum_scores.keys():
if etf in quality_scores:
composite_scores[etf] = (
momentum_scores[etf] * 0.6 + quality_scores[etf] * 0.4
)
# 选择得分最高的top_n个ETF
selected_etfs = sorted(composite_scores.items(),
key=lambda x: x[1], reverse=True)[:top_n]
# 等权重配置
signals = {etf: 1.0/len(selected_etfs) for etf, score in selected_etfs}
return signals
def run_strategy(self, start_date, end_date, initial_capital=1000000):
"""运行完整策略"""
# 准备数据
market_data = self.prepare_data(start_date, end_date)
# 初始化回测引擎
backtest_engine = BacktestEngine(initial_capital)
# 逐日运行
current_date = start_date
all_signals = {}
while current_date <= end_date:
# 避免使用未来数据
valid_data = avoid_lookahead_bias(market_data, current_date)
if len(valid_data) >= 10: # 至少有10个可选ETF
momentum_scores, quality_scores = self.calculate_factors(valid_data, current_date)
signals = self.generate_signals(momentum_scores, quality_scores)
all_signals[current_date] = signals
current_date += timedelta(days=1)
# 运行回测
backtest_engine.run_backtest(all_signals, market_data)
return backtest_engine.portfolio_value, backtest_engine.trade_log
# 使用示例
if __name__ == "__main__":
strategy = ETFDoubleFactorStrategy()
# 运行回测
portfolio_values, trade_log = strategy.run_strategy(
start_date=datetime(2020, 1, 1),
end_date=datetime(2023, 12, 31)
)
# 计算性能指标
metrics = calculate_performance_metrics(portfolio_values)
print("策略性能指标:", metrics)
这个框架的价值不在于提供一个"圣杯"策略,而是展示如何把研报理念转化成可执行、可验证、可改进的实盘代码。真正重要的不是双因子模型本身,而是这种从研究到实盘的完整方法论。
在实际使用中,建议先用小资金验证,重点关注策略的稳定性和适应性,而不是追求短期高收益。好的量化策略应该是时间的函数,能够在不同市场环境下持续创造价值。
489



被折叠的 条评论
为什么被折叠?



