一、核心概念与工具准备#
关键公式:
其中:
表示资产收益率向量
表示因子暴露度矩阵(因子载荷)
表示因子收益率向量
表示特质收益向量(随机误差项)
这个公式体现了投资组合收益可以分解为:
-
系统性部分:因子暴露与因子收益的乘积()
-
非系统性部分:特质收益()
工具包配置:
code
import numpy as np
import pandas as pd
import yfinance as yf
import statsmodels.api as sm
import matplotlib.pyplot as plt
from tqdm import tqdm
from pandas_datareader import data as web二、数据获取与预处理#
步骤详解:
- 获取标普500成分股(示例替代QTradableStocksUS):
code
sp500 = pd.read_html('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies')[0]
tickers = sp500.Symbol.tolist()[:50] # 取前50支简化计算这只能作为 API 演示。用“当前成分股”回测 2009 年会产生幸存者偏差;策略评估必须改用每个历史时点可见的成分股和基本面数据。
- 下载历史价格数据:
code
def download_prices(tickers, start, end):
data = yf.download(
tickers, start=start, end=end, auto_adjust=True,
group_by='column', multi_level_index=True,
)['Close']
return data.dropna(axis=1)
prices = download_prices(tickers, '2009-01-01', '2011-01-01')
returns = prices.pct_change().dropna() # 日收益率矩阵数据结构示例:
code
Date AAPL MSFT AMZN ...
2009-01-02 0.012 -0.005 0.018
2009-01-03 -0.007 0.015 0.002三、因子模型构建#
Fama-French三因子数据获取:
code
ff_all = web.DataReader(
'F-F_Research_Data_Factors_daily', 'famafrench',
start='2009-01-01', end='2011-01-01',
)[0] / 100
ff_data = ff_all[['Mkt-RF', 'SMB', 'HML']]因子对齐处理:
code
common_dates = returns.index.intersection(ff_data.index)
F = ff_data.loc[common_dates] # 因子收益矩阵
# 因子中的市场列是 Mkt-RF,因此资产端也使用超额收益
R = returns.loc[common_dates].sub(ff_all.loc[common_dates, 'RF'], axis=0)四、因子暴露计算#
滚动回归示例:
code
betas = pd.DataFrame(index=R.columns, columns=F.columns, dtype=float)
residual_var = pd.Series(index=R.columns, dtype=float)
for stock in tqdm(R.columns):
model = sm.OLS(R[stock], sm.add_constant(F))
results = model.fit()
betas.loc[stock] = results.params.drop('const')
residual_var.loc[stock] = results.resid.var(ddof=1)暴露矩阵结构:
code
Mkt-RF SMB HML
AAPL 1.05 -0.09 -0.34
MSFT 0.92 0.12 0.21
...五、风险分解实战#
方差分解实现:
code
# 协方差矩阵计算
factor_cov = F.cov()
specific_var = residual_var
# 组合权重(等权示例)
weights = np.ones(len(R.columns)) / len(R.columns)
# 风险分解
common_risk = weights @ betas @ factor_cov @ betas.T @ weights
specific_risk = weights @ np.diag(specific_var) @ weights
total_risk = common_risk + specific_risk风险占比计算:
code
print(f"系统性风险占比: {common_risk/total_risk:.2%}")六、优化模型构建#
CVXPY优化示例:
code
import cvxpy as cp
# 定义优化变量和因子模型协方差
w = cp.Variable(len(weights))
total_cov = (
betas.values @ factor_cov.values @ betas.values.T
+ np.diag(specific_var.values)
)
benchmark_exposure = betas.T.values @ weights
exposure_band = 0.05
risk_aversion = 10.0
# 构建约束
constraints = [
cp.sum(w) == 1,
w >= 0,
cp.norm(betas.T.values @ w - benchmark_exposure, 'inf') <= exposure_band,
]
# 在预期收益与总方差之间权衡,避免仅最大化样本均值导致单一持仓
portfolio_return = R.mean().values @ w
portfolio_variance = cp.quad_form(w, cp.psd_wrap(total_cov))
problem = cp.Problem(
cp.Maximize(portfolio_return - risk_aversion * portfolio_variance),
constraints,
)
problem.solve()
if problem.status not in {cp.OPTIMAL, cp.OPTIMAL_INACCURATE}:
raise RuntimeError(f'优化失败:{problem.status}')优化结果分析:
code
print("最优权重:", w.value.round(3))
print("预期收益:", portfolio_return.value)七、绩效归因分析#
使用同一因子模型进行归因:
code
# 各因子的逐日收益贡献
portfolio_exposure = betas.T.values @ w.value
factor_contributions = F.mul(portfolio_exposure, axis=1)
# 特异收益是组合超额收益减去模型解释部分
portfolio_excess_return = R @ w.value
specific_return = portfolio_excess_return - factor_contributions.sum(axis=1)
attribution = factor_contributions.assign(Specific=specific_return)
print('样本期算术收益贡献:')
print(attribution.sum())八、关键演进对比#
| 方法类型 | 优点 | 局限性 |
|---|---|---|
| 均值-方差优化 | 理论完备 | 对输入参数敏感 |
| 风险平价模型 | 风险均衡配置 | 忽略收益预测 |
| 因子风险约束模型 | 明确控制风险来源 | 依赖因子模型准确性 |
小练习:
尝试将市场因子暴露限制在0.8以下,比较优化前后组合的收益波动比变化
code
# 参考答案
market_limit = 0.8
style_exposure = betas[['SMB', 'HML']].T.values @ w
style_benchmark = betas[['SMB', 'HML']].T.values @ weights
exercise_constraints = [
cp.sum(w) == 1,
w >= 0,
cp.norm(style_exposure - style_benchmark, 'inf') <= exposure_band,
betas['Mkt-RF'].values @ w <= market_limit,
]
constrained_problem = cp.Problem(
cp.Maximize(portfolio_return - risk_aversion * portfolio_variance),
exercise_constraints,
)
constrained_problem.solve()
if constrained_problem.status not in {cp.OPTIMAL, cp.OPTIMAL_INACCURATE}:
raise RuntimeError(f'市场暴露约束不可行:{constrained_problem.status}')
# 夏普比率必须由同一段样本外超额收益计算,不能使用约束的对偶变量
test_excess_returns = ... # 与训练期分离、列顺序与 R 一致的 DataFrame
portfolio_test_returns = test_excess_returns @ w.value
sharpe = np.sqrt(252) * portfolio_test_returns.mean() / portfolio_test_returns.std(ddof=1)
print(f"样本外年化夏普比率: {sharpe:.2f}")