1103 lines
53 KiB
Plaintext
1103 lines
53 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "26fdd646",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"\"\"\"\n",
|
||
"量化交易演示系列 第5篇:组合优化\n",
|
||
"Quantitative Trading Demo Series - Part 5: Portfolio Optimization\n",
|
||
"\n",
|
||
"涵盖内容 / Topics Covered:\n",
|
||
" §0 合成股票池与因子模型 (Synthetic Universe & Factor Model)\n",
|
||
" §1 协方差矩阵估计 (Covariance Matrix Estimation)\n",
|
||
" §2 马科维兹均值-方差优化 (Markowitz Mean-Variance Optimization)\n",
|
||
" §3 有效前沿 (Efficient Frontier)\n",
|
||
" §4 特殊组合:等权/最小方差/最大夏普 (Special Portfolios: EW / MinVar / MaxSharpe)\n",
|
||
" §5 风险平价 (Risk Parity)\n",
|
||
" §6 Black-Litterman 模型 (Black-Litterman Model)\n",
|
||
" §7 带约束的组合优化 (Constrained Portfolio Optimization)\n",
|
||
" §8 滚动回测:五策略对比 (Rolling Backtest: 5-Strategy Comparison)\n",
|
||
" §9 可视化 (9-Panel Visualization)\n",
|
||
"\n",
|
||
"依赖库 / Dependencies:\n",
|
||
" numpy, pandas, scipy, matplotlib, sklearn\n",
|
||
"\n",
|
||
"作者 / Author: Quant Demo Series\n",
|
||
"\"\"\"\n",
|
||
"\n",
|
||
"# ── 无界面绘图模式,必须在 import pyplot 之前设置 ──\n",
|
||
"# Headless plotting mode — MUST be set before importing pyplot\n",
|
||
"import matplotlib\n",
|
||
"matplotlib.use('Agg')\n",
|
||
"\n",
|
||
"import warnings\n",
|
||
"warnings.filterwarnings('ignore')\n",
|
||
"\n",
|
||
"import numpy as np\n",
|
||
"import pandas as pd\n",
|
||
"import matplotlib.pyplot as plt\n",
|
||
"import matplotlib.gridspec as gridspec\n",
|
||
"from matplotlib.ticker import FuncFormatter\n",
|
||
"from scipy.optimize import minimize\n",
|
||
"\n",
|
||
"# ── 随机种子(确保结果可复现)/ Random seed for reproducibility ──\n",
|
||
"np.random.seed(42)\n",
|
||
"\n",
|
||
"# ── 中文字体配置 / Chinese font configuration ──\n",
|
||
"plt.rcParams['font.sans-serif'] = [\n",
|
||
" 'WenQuanYi Zen Hei', 'Arial Unicode MS', 'SimHei', 'DejaVu Sans'\n",
|
||
"]\n",
|
||
"plt.rcParams['axes.unicode_minus'] = False\n",
|
||
"\n",
|
||
"print(\"=\" * 70)\n",
|
||
"print(\" 量化交易 组合优化演示\")\n",
|
||
"print(\" Quantitative Trading: Portfolio Optimization Demo\")\n",
|
||
"print(\"=\" * 70)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "100ee590",
|
||
"metadata": {},
|
||
"source": [
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"# §0 合成股票池与行业因子模型\n",
|
||
"# Synthetic Universe & Sector Factor Model\n",
|
||
"\n",
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"#\n",
|
||
"# 使用 Barra 风格的因子模型生成收益率:\n",
|
||
"# Returns are generated using a Barra-style factor model:\n",
|
||
"#\n",
|
||
"# r_{i,t} = β_{mkt,i} × r_{mkt,t} ← 市场因子 (Market factor)\n",
|
||
"# + Σ_k β_{sec,i,k} × f_{k,t} ← 行业因子 (Sector factors)\n",
|
||
"# + α_i ← 个股 Alpha (Stock-specific alpha)\n",
|
||
"# + ε_{i,t} ← 特质噪音 (Idiosyncratic noise)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "94603f7d",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"N_STOCKS = 20 # 股票数量 (Number of stocks)\n",
|
||
"N_YEARS = 3 # 历史年数 (History in years)\n",
|
||
"FREQ = 252 # 年化因子 / 每年交易日数 (Annualization factor)\n",
|
||
"RF = 0.02 # 无风险利率 (Risk-free rate), 年化\n",
|
||
"\n",
|
||
"START_DATE = \"2021-01-04\"\n",
|
||
"dates = pd.bdate_range(START_DATE, periods=N_YEARS * FREQ) # 工作日序列\n",
|
||
"N_DAYS = len(dates)\n",
|
||
"\n",
|
||
"# ── 行业分组 / Sector assignments ──\n",
|
||
"SECTORS = {\n",
|
||
" \"科技 Tech\": list(range(0, 5)), # S00–S04\n",
|
||
" \"金融 Finance\": list(range(5, 9)), # S05–S08\n",
|
||
" \"消费 Consumer\": list(range(9, 13)), # S09–S12\n",
|
||
" \"工业 Industrial\": list(range(13, 17)), # S13–S16\n",
|
||
" \"医疗 Healthcare\": list(range(17, 20)), # S17–S19\n",
|
||
"}\n",
|
||
"SECTOR_NAMES = list(SECTORS.keys())\n",
|
||
"SECTOR_SHORT = [\"科技\", \"金融\", \"消费\", \"工业\", \"医疗\"]\n",
|
||
"N_SECTORS = len(SECTORS)\n",
|
||
"stock_names = [f\"S{i:02d}\" for i in range(N_STOCKS)]\n",
|
||
"\n",
|
||
"# ── 行业公共因子收益率 / Sector factor daily returns ──\n",
|
||
"sector_ann_ret = np.array([0.16, 0.10, 0.12, 0.08, 0.13]) # 各行业年化期望收益\n",
|
||
"sector_ann_vol = np.array([0.25, 0.18, 0.20, 0.15, 0.22]) # 各行业年化波动率\n",
|
||
"\n",
|
||
"sector_factor_rets = np.zeros((N_DAYS, N_SECTORS))\n",
|
||
"for k in range(N_SECTORS):\n",
|
||
" mu_d = sector_ann_ret[k] / FREQ\n",
|
||
" sig_d = sector_ann_vol[k] / np.sqrt(FREQ)\n",
|
||
" sector_factor_rets[:, k] = np.random.normal(mu_d, sig_d, N_DAYS)\n",
|
||
"\n",
|
||
"# ── 市场公共因子 / Market common factor ──\n",
|
||
"mkt_daily = np.random.normal(0.10 / FREQ, 0.18 / np.sqrt(FREQ), N_DAYS)\n",
|
||
"\n",
|
||
"# ── 各股的因子载荷(Beta 暴露)/ Factor loadings (Beta exposures) ──\n",
|
||
"betas_mkt = np.random.uniform(0.6, 1.4, N_STOCKS) # 市场 Beta\n",
|
||
"betas_sec = np.zeros((N_STOCKS, N_SECTORS)) # 行业 Beta\n",
|
||
"\n",
|
||
"for k, (sname, sidx) in enumerate(SECTORS.items()):\n",
|
||
" for i in sidx:\n",
|
||
" betas_sec[i, k] = np.random.uniform(0.5, 1.0) # 本行业:强载荷\n",
|
||
" for k2 in range(N_SECTORS):\n",
|
||
" if k2 != k:\n",
|
||
" betas_sec[i, k2] = np.random.uniform(0.0, 0.15) # 其他行业:弱载荷\n",
|
||
"\n",
|
||
"# ── 个股特质波动率 / Idiosyncratic volatility ──\n",
|
||
"idio_vols = np.random.uniform(0.12, 0.30, N_STOCKS) / np.sqrt(FREQ)\n",
|
||
"idio_rets = np.random.normal(0, idio_vols, (N_DAYS, N_STOCKS))\n",
|
||
"\n",
|
||
"# ── 个股特质 Alpha / Stock-specific alpha ──\n",
|
||
"# 科技股注入正 Alpha,让最大夏普组合有明显的倾向性\n",
|
||
"idio_alpha = np.zeros(N_STOCKS)\n",
|
||
"idio_alpha[:5] = np.random.uniform(0.04, 0.09, 5) / FREQ # 科技股:正 Alpha\n",
|
||
"idio_alpha[5:9] = np.random.uniform(-0.02, 0.02, 4) / FREQ # 金融股:中性\n",
|
||
"\n",
|
||
"# ── 合成日收益率矩阵 / Composite daily return matrix ──\n",
|
||
"daily_ret_mat = np.zeros((N_DAYS, N_STOCKS))\n",
|
||
"for i in range(N_STOCKS):\n",
|
||
" daily_ret_mat[:, i] = (\n",
|
||
" betas_mkt[i] * mkt_daily # 市场贡献\n",
|
||
" + betas_sec[i] @ sector_factor_rets.T # 行业贡献(5 行业因子)\n",
|
||
" + idio_alpha[i] # 个股 Alpha\n",
|
||
" + idio_rets[:, i] # 特质噪音\n",
|
||
" )\n",
|
||
"\n",
|
||
"ret_df = pd.DataFrame(daily_ret_mat, index=dates, columns=stock_names)\n",
|
||
"prices_df = (1 + ret_df).cumprod() * 100.0 # 从 100 元开始的价格序列\n",
|
||
"\n",
|
||
"# ── 市值权重(Black-Litterman 需要作为先验市场组合)──\n",
|
||
"# Market-cap weights as Black-Litterman equilibrium prior\n",
|
||
"mktcap_weights = pd.Series(\n",
|
||
" np.random.dirichlet(np.ones(N_STOCKS) * 2.0),\n",
|
||
" index=stock_names\n",
|
||
")\n",
|
||
"\n",
|
||
"ann_rets = (1 + ret_df).prod() ** (FREQ / N_DAYS) - 1\n",
|
||
"print(f\"\\n[§0] 股票池生成完成 / Universe Generated\")\n",
|
||
"print(f\" 股票数量 (Stocks): {N_STOCKS} 只\")\n",
|
||
"print(f\" 交易日数 (Days): {N_DAYS} 天 {dates[0].date()} ~ {dates[-1].date()}\")\n",
|
||
"print(f\" 行业数量 (Sectors): {N_SECTORS} 个\")\n",
|
||
"print(f\" 个股年化收益区间: {ann_rets.min():.1%} ~ {ann_rets.max():.1%}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "7edbe870",
|
||
"metadata": {},
|
||
"source": [
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"# §1 协方差矩阵估计:样本 vs Ledoit-Wolf 收缩\n",
|
||
"# Covariance Matrix Estimation: Sample vs Ledoit-Wolf Shrinkage\n",
|
||
"\n",
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"#\n",
|
||
"# 协方差矩阵 (Covariance Matrix) Σ 是组合优化的核心输入。\n",
|
||
"# 样本协方差矩阵 (Sample Covariance Matrix) 存在两个问题:\n",
|
||
"# 1. 估计误差大(小样本情形,T/N 较小时)/ Large estimation error when T/N is small\n",
|
||
"# 2. 条件数 (Condition Number) 高,矩阵\"病态\",优化对估计误差非常敏感\n",
|
||
"#\n",
|
||
"# Ledoit-Wolf 收缩 (Ledoit-Wolf Shrinkage):\n",
|
||
"# Σ_shrunk = (1-α) × Σ_sample + α × F\n",
|
||
"# 其中 F 是结构化目标矩阵(如对角矩阵),α 是最优收缩系数(数据驱动自动选择)\n",
|
||
"# F is a structured target matrix; α is the optimal shrinkage coefficient.\n",
|
||
"\n",
|
||
"# 样本协方差(年化) / Sample covariance (annualized)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "b8d795d7",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"cov_sample = ret_df.cov() * FREQ\n",
|
||
"mu_sample = ret_df.mean() * FREQ # 年化预期收益向量 (Annualized expected return vector)\n",
|
||
"\n",
|
||
"# Ledoit-Wolf 收缩估计 / Ledoit-Wolf shrinkage estimation\n",
|
||
"try:\n",
|
||
" from sklearn.covariance import LedoitWolf\n",
|
||
" lw = LedoitWolf()\n",
|
||
" lw.fit(ret_df.values)\n",
|
||
" cov_shrink = pd.DataFrame(\n",
|
||
" lw.covariance_ * FREQ,\n",
|
||
" index=stock_names, columns=stock_names\n",
|
||
" )\n",
|
||
" shrink_alpha = lw.shrinkage_ # 最优收缩系数 α\n",
|
||
" USE_SHRINK = True\n",
|
||
"except ImportError:\n",
|
||
" cov_shrink = cov_sample.copy()\n",
|
||
" shrink_alpha = 0.0\n",
|
||
" USE_SHRINK = False\n",
|
||
"\n",
|
||
"print(f\"\\n[§1] 协方差估计 / Covariance Estimation\")\n",
|
||
"print(f\" 样本协方差条件数 (Sample Cov Cond#): {np.linalg.cond(cov_sample.values):.1f}\")\n",
|
||
"if USE_SHRINK:\n",
|
||
" print(f\" Ledoit-Wolf 收缩系数 (Shrinkage α): {shrink_alpha:.4f}\")\n",
|
||
" print(f\" 收缩后条件数 (Shrunk Cov Cond#): {np.linalg.cond(cov_shrink.values):.1f}\")\n",
|
||
" print(f\" → 条件数降低意味着优化更稳健 / Lower cond# = more robust optimization\")\n",
|
||
"\n",
|
||
"# 正式使用的协方差矩阵(收缩版更稳健)/ Use shrunk covariance for optimization\n",
|
||
"cov_opt = cov_shrink if USE_SHRINK else cov_sample\n",
|
||
"mu_opt = mu_sample.copy()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "aab1c601",
|
||
"metadata": {},
|
||
"source": [
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"# §2 核心优化工具函数\n",
|
||
"# Core Optimization Utility Functions\n",
|
||
"\n",
|
||
"# ══════════════════════════════════════════════════════════════════════"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "d0f68b49",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def portfolio_stats(weights, mu, cov, rf=RF):\n",
|
||
" \"\"\"\n",
|
||
" 计算组合的三个核心绩效指标(年化)\n",
|
||
" Compute 3 key portfolio statistics (annualized).\n",
|
||
"\n",
|
||
" 参数 / Args:\n",
|
||
" weights : np.ndarray, 权重向量 (weight vector), shape (N,)\n",
|
||
" mu : pd.Series, 年化预期收益向量 (annualized expected returns)\n",
|
||
" cov : pd.DataFrame, 年化协方差矩阵 (annualized covariance matrix)\n",
|
||
" rf : float, 无风险利率 (risk-free rate)\n",
|
||
"\n",
|
||
" 返回 / Returns:\n",
|
||
" ret : 组合年化收益率 (portfolio annualized return)\n",
|
||
" vol : 组合年化波动率 (portfolio annualized volatility)\n",
|
||
" sharpe : 夏普比率 (Sharpe ratio) = (ret - rf) / vol\n",
|
||
" \"\"\"\n",
|
||
" w = np.asarray(weights)\n",
|
||
" ret = float(w @ mu) # w'μ\n",
|
||
" var = float(w @ cov.values @ w) # w'Σw(组合方差)\n",
|
||
" vol = np.sqrt(max(var, 1e-12)) # 组合波动率(年化)\n",
|
||
" sharpe = (ret - rf) / vol\n",
|
||
" return ret, vol, sharpe\n",
|
||
"\n",
|
||
"\n",
|
||
"def _base_optimize(objective, n, bounds, extra_constraints=None):\n",
|
||
" \"\"\"\n",
|
||
" 通用 SLSQP 优化框架(内部使用)\n",
|
||
" Generic SLSQP optimization scaffold (internal use).\n",
|
||
"\n",
|
||
" SLSQP = Sequential Least Squares Programming / 序列二次规划法\n",
|
||
" \"\"\"\n",
|
||
" w0 = np.ones(n) / n # 初始猜测:等权 (Initial guess: equal weight)\n",
|
||
" constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1.0}] # 全投资约束\n",
|
||
" if extra_constraints:\n",
|
||
" constraints.extend(extra_constraints)\n",
|
||
" return minimize(\n",
|
||
" objective, w0,\n",
|
||
" method='SLSQP',\n",
|
||
" bounds=bounds,\n",
|
||
" constraints=constraints,\n",
|
||
" options={'ftol': 1e-12, 'maxiter': 1500}\n",
|
||
" )\n",
|
||
"\n",
|
||
"\n",
|
||
"def min_variance(mu, cov, allow_short=False):\n",
|
||
" \"\"\"\n",
|
||
" 最小方差组合 (Minimum Variance Portfolio, MinVar)\n",
|
||
"\n",
|
||
" 这是组合优化中最稳健的组合,因为它完全不依赖对预期收益的估计。\n",
|
||
" Most robust portfolio: it does NOT require any return estimates.\n",
|
||
"\n",
|
||
" 问题形式 / Optimization problem:\n",
|
||
" minimize w'Σw\n",
|
||
" subject to Σw_i = 1 (全投资约束 / full investment)\n",
|
||
" w_i ≥ 0 (多头约束 / long-only, when allow_short=False)\n",
|
||
" \"\"\"\n",
|
||
" n = len(mu)\n",
|
||
" bnd = (-0.3, 1.0) if allow_short else (0.0, 1.0)\n",
|
||
" bounds = [bnd] * n\n",
|
||
" result = _base_optimize(lambda w: w @ cov.values @ w, n, bounds)\n",
|
||
" return pd.Series(result.x, index=cov.index)\n",
|
||
"\n",
|
||
"\n",
|
||
"def max_sharpe(mu, cov, rf=RF, allow_short=False):\n",
|
||
" \"\"\"\n",
|
||
" 最大夏普比率组合 / 切线组合 (Maximum Sharpe / Tangency Portfolio)\n",
|
||
"\n",
|
||
" 在均值-方差空间中,从无风险资产出发到有效前沿的切线点。\n",
|
||
" Tangency point from the risk-free asset to the efficient frontier (Capital Market Line).\n",
|
||
"\n",
|
||
" 技巧 / Trick:\n",
|
||
" max Sharpe ≡ min -Sharpe (把最大化转为最小化 / flip to minimization)\n",
|
||
" \"\"\"\n",
|
||
" n = len(mu)\n",
|
||
" bnd = (-0.3, 1.0) if allow_short else (0.0, 1.0)\n",
|
||
" bounds = [bnd] * n\n",
|
||
"\n",
|
||
" def neg_sharpe(w):\n",
|
||
" r, v, sr = portfolio_stats(w, mu, cov, rf)\n",
|
||
" return -sr\n",
|
||
"\n",
|
||
" result = _base_optimize(neg_sharpe, n, bounds)\n",
|
||
" return pd.Series(result.x, index=cov.index)\n",
|
||
"\n",
|
||
"\n",
|
||
"def efficient_portfolio(target_return, mu, cov):\n",
|
||
" \"\"\"\n",
|
||
" 目标收益下的最小方差组合(有效前沿上的一点)\n",
|
||
" Minimum-variance portfolio for a given target return (a point on efficient frontier).\n",
|
||
"\n",
|
||
" 问题形式 / Optimization problem:\n",
|
||
" minimize w'Σw\n",
|
||
" subject to w'μ = target_return (收益约束 / return constraint)\n",
|
||
" Σw_i = 1\n",
|
||
" w_i ≥ 0\n",
|
||
" \"\"\"\n",
|
||
" n = len(mu)\n",
|
||
" bnd = (0.0, 1.0)\n",
|
||
" result = _base_optimize(\n",
|
||
" lambda w: w @ cov.values @ w, n,\n",
|
||
" bounds=[bnd] * n,\n",
|
||
" extra_constraints=[\n",
|
||
" {'type': 'eq', 'fun': lambda w: float(w @ mu.values) - target_return}\n",
|
||
" ]\n",
|
||
" )\n",
|
||
" return pd.Series(result.x, index=cov.index) if result.success else None"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "c5dc678e",
|
||
"metadata": {},
|
||
"source": [
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"# §3 有效前沿\n",
|
||
"# Efficient Frontier\n",
|
||
"\n",
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"#\n",
|
||
"# 有效前沿 (Efficient Frontier) 是所有\"有效组合\"的集合:\n",
|
||
"# → 在相同风险(波动率)下,收益最高\n",
|
||
"# → 在相同收益下,风险最低\n",
|
||
"#\n",
|
||
"# 有效前沿由两点确定:\n",
|
||
"# 左端点:最小方差组合 (Global Minimum Variance Portfolio, GMV)\n",
|
||
"# 右端点:预期收益最高的单只股票(极端集中)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "c4f0caa1",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(f\"\\n[§3] 计算有效前沿 / Computing Efficient Frontier...\")\n",
|
||
"\n",
|
||
"# ── 计算关键节点组合 / Compute key special portfolios ──\n",
|
||
"w_minvar = min_variance(mu_opt, cov_opt)\n",
|
||
"w_maxsharpe = max_sharpe(mu_opt, cov_opt, rf=RF)\n",
|
||
"w_eq = pd.Series(np.ones(N_STOCKS) / N_STOCKS, index=stock_names)\n",
|
||
"\n",
|
||
"ret_min, vol_min, sr_min = portfolio_stats(w_minvar, mu_opt, cov_opt)\n",
|
||
"ret_msr, vol_msr, sr_msr = portfolio_stats(w_maxsharpe, mu_opt, cov_opt)\n",
|
||
"ret_eq, vol_eq, sr_eq = portfolio_stats(w_eq, mu_opt, cov_opt)\n",
|
||
"\n",
|
||
"# ── 扫描有效前沿 / Trace the efficient frontier ──\n",
|
||
"n_pts = 60\n",
|
||
"ret_lo = ret_min * 0.99\n",
|
||
"ret_hi = mu_opt.max() * 0.88\n",
|
||
"frontier_rets = []\n",
|
||
"frontier_vols = []\n",
|
||
"\n",
|
||
"for tgt in np.linspace(ret_lo, ret_hi, n_pts):\n",
|
||
" w_eff = efficient_portfolio(tgt, mu_opt, cov_opt)\n",
|
||
" if w_eff is not None:\n",
|
||
" r, v, _ = portfolio_stats(w_eff, mu_opt, cov_opt)\n",
|
||
" frontier_rets.append(r)\n",
|
||
" frontier_vols.append(v)\n",
|
||
"\n",
|
||
"# ── Monte Carlo 随机组合云(背景参照)/ Random portfolio cloud (background reference) ──\n",
|
||
"N_RANDOM = 3000\n",
|
||
"rand_rets = []\n",
|
||
"rand_vols = []\n",
|
||
"rand_sharpes = []\n",
|
||
"\n",
|
||
"for _ in range(N_RANDOM):\n",
|
||
" w_r = np.random.dirichlet(np.ones(N_STOCKS)) # 满足 w≥0, Σw=1 的随机权重\n",
|
||
" r, v, sr = portfolio_stats(w_r, mu_opt, cov_opt)\n",
|
||
" rand_rets.append(r)\n",
|
||
" rand_vols.append(v)\n",
|
||
" rand_sharpes.append(sr)\n",
|
||
"\n",
|
||
"print(f\" 有效前沿点数 (Frontier points): {len(frontier_rets)}\")\n",
|
||
"print(f\" 最小方差组合 (MinVar): 收益={ret_min:.1%}, 波动={vol_min:.1%}, 夏普={sr_min:.2f}\")\n",
|
||
"print(f\" 最大夏普组合 (MaxSharpe): 收益={ret_msr:.1%}, 波动={vol_msr:.1%}, 夏普={sr_msr:.2f}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "69d5e842",
|
||
"metadata": {},
|
||
"source": [
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"# §4 风险平价\n",
|
||
"# Risk Parity\n",
|
||
"\n",
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"#\n",
|
||
"# 核心思想:每只资产对组合总风险的贡献相等\n",
|
||
"# Core idea: each asset contributes equally to total portfolio risk.\n",
|
||
"#\n",
|
||
"# 风险贡献 (Risk Contribution, RC) 的定义:\n",
|
||
"#\n",
|
||
"# RC_i = w_i × ∂σ_p/∂w_i = w_i × (Σw)_i / σ_p\n",
|
||
"# ↑ 边际风险贡献 (Marginal Risk Contribution, MRC)\n",
|
||
"#\n",
|
||
"# 等风险贡献条件 (Equal Risk Contribution):\n",
|
||
"# RC_i = σ_p / N ←→ w_i × (Σw)_i = w_j × (Σw)_j for all i, j\n",
|
||
"#\n",
|
||
"# 优化形式(最小化各股风险贡献比例与目标 1/N 的偏差平方和):\n",
|
||
"# minimize Σ_i (RC_i/σ_p − 1/N)²"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "a033b66c",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(f\"\\n[§4] 计算风险平价组合 / Risk Parity Portfolio...\")\n",
|
||
"\n",
|
||
"\n",
|
||
"def risk_contributions(weights, cov):\n",
|
||
" \"\"\"\n",
|
||
" 计算每只资产的绝对风险贡献和风险贡献占比\n",
|
||
" Compute absolute risk contribution and risk contribution fraction for each asset.\n",
|
||
"\n",
|
||
" Returns:\n",
|
||
" rc : 绝对风险贡献向量 (Absolute Risk Contribution vector), shape (N,)\n",
|
||
" sigma_p : 组合总波动率 (Total portfolio volatility)\n",
|
||
" \"\"\"\n",
|
||
" w = np.asarray(weights)\n",
|
||
" sigma_p = np.sqrt(float(w @ cov.values @ w)) # 组合波动率 (Portfolio vol)\n",
|
||
" mrc = cov.values @ w / sigma_p # 边际风险贡献 (MRC = ∂σ/∂w)\n",
|
||
" rc = w * mrc # 绝对风险贡献 (RC = w × MRC)\n",
|
||
" return rc, sigma_p\n",
|
||
"\n",
|
||
"\n",
|
||
"def risk_parity(cov):\n",
|
||
" \"\"\"\n",
|
||
" 等风险贡献组合 (Equal Risk Contribution Portfolio)\n",
|
||
"\n",
|
||
" 约束:w_i ≥ 0 (严格多头,做空会有负的风险贡献,没有意义)\n",
|
||
" Strictly long-only: short positions would give negative RC (meaningless).\n",
|
||
" \"\"\"\n",
|
||
" N = cov.shape[0]\n",
|
||
" tgt = 1.0 / N # 目标风险贡献占比 = 1/N\n",
|
||
"\n",
|
||
" def objective(w):\n",
|
||
" rc, sigma_p = risk_contributions(w, cov)\n",
|
||
" rc_pct = rc / sigma_p # 风险贡献占比 (RC fraction)\n",
|
||
" return np.sum((rc_pct - tgt) ** 2)\n",
|
||
"\n",
|
||
" result = _base_optimize(\n",
|
||
" objective, N,\n",
|
||
" bounds=[(1e-6, 1.0)] * N # 严格正数(不允许做空)\n",
|
||
" )\n",
|
||
" return pd.Series(result.x, index=cov.index)\n",
|
||
"\n",
|
||
"\n",
|
||
"w_rp = risk_parity(cov_opt)\n",
|
||
"rc_rp, sigma_rp = risk_contributions(w_rp, cov_opt)\n",
|
||
"ret_rp, vol_rp, sr_rp = portfolio_stats(w_rp, mu_opt, cov_opt)\n",
|
||
"\n",
|
||
"rc_pct_rp = rc_rp / sigma_rp\n",
|
||
"max_rc_dev = np.max(np.abs(rc_pct_rp - 1.0 / N_STOCKS)) # 与目标的最大偏差\n",
|
||
"\n",
|
||
"print(f\" 风险平价组合: 收益={ret_rp:.1%}, 波动={vol_rp:.1%}, 夏普={sr_rp:.2f}\")\n",
|
||
"print(f\" 风险贡献最大偏差 (Max RC deviation from 1/N): {max_rc_dev:.5f}\")\n",
|
||
"print(f\" → 接近 0 表示各股风险贡献几乎相等 / ≈0 means near-perfect equal RC\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "c44140a6",
|
||
"metadata": {},
|
||
"source": [
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"# §5 Black-Litterman 模型\n",
|
||
"# Black-Litterman Model\n",
|
||
"\n",
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"#\n",
|
||
"# 马科维兹优化的两大痛点 / Two pain points of Markowitz optimization:\n",
|
||
"# ① 预期收益 μ 难以估计,哪怕小幅误差也会导致权重大幅波动\n",
|
||
"# Expected returns are noisy; small errors cause wild weight swings.\n",
|
||
"# ② 结果过度集中(\"角点解\"),实际不可用\n",
|
||
"# Results are over-concentrated \"corner solutions\".\n",
|
||
"#\n",
|
||
"# Black-Litterman (1990) 的贝叶斯框架 / Bayesian framework:\n",
|
||
"# ① 先验 (Prior): 从市场均衡推导隐含收益(CAPM 反向优化)\n",
|
||
"# Market-implied returns from reverse-optimizing CAPM.\n",
|
||
"# ② 观点 (Views): 投资者对某些股票/组合的主观预期\n",
|
||
"# Investor's subjective views on specific stocks/portfolios.\n",
|
||
"# ③ 后验 (Posterior): 贝叶斯更新,先验与观点的加权混合\n",
|
||
"# Bayesian posterior blending prior and views.\n",
|
||
"#\n",
|
||
"# 后验均值公式 / Posterior mean formula (He & Litterman, 1999):\n",
|
||
"#\n",
|
||
"# μ_BL = [(τΣ)⁻¹ + P'Ω⁻¹P]⁻¹ × [(τΣ)⁻¹Π + P'Ω⁻¹Q]\n",
|
||
"# ───────────────────── ──────────────────────\n",
|
||
"# 精度矩阵之和 加权均值向量\n",
|
||
"# Sum of precision Weighted mean vector\n",
|
||
"#\n",
|
||
"# Π = δ × Σ × w_mkt ← 市场均衡隐含超额收益 (Market equilibrium implied returns)\n",
|
||
"# δ = 风险厌恶系数 (Risk aversion coefficient)\n",
|
||
"# τ = 先验收缩参数 (Prior scaling factor), 通常 0.025~0.10\n",
|
||
"# P = 观点矩阵 K×N (View matrix, K views, N assets)\n",
|
||
"# Q = 观点收益向量 K×1 (View expected excess returns)\n",
|
||
"# Ω = 观点不确定性矩阵 K×K (View uncertainty / confidence matrix)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "b41e55ba",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(f\"\\n[§5] Black-Litterman 模型 / Black-Litterman Model...\")\n",
|
||
"\n",
|
||
"DELTA = 2.5 # 风险厌恶系数 (Risk aversion), 典型值 2.0~3.0\n",
|
||
"TAU = 0.05 # 先验收缩参数 (Prior scaling), 典型值 0.025~0.10\n",
|
||
"\n",
|
||
"# ─── Step 1: 计算市场均衡隐含超额收益 ───\n",
|
||
"# Market equilibrium implied excess returns (Reverse Optimization of CAPM)\n",
|
||
"# Π = δ × Σ × w_mkt\n",
|
||
"w_mkt = mktcap_weights.values\n",
|
||
"Pi = DELTA * cov_opt.values @ w_mkt\n",
|
||
"Pi_s = pd.Series(Pi, index=stock_names)\n",
|
||
"\n",
|
||
"# ─── Step 2: 设定投资者观点 (Investor Views) ───\n",
|
||
"#\n",
|
||
"# 观点1 (View 1): \"S01 的年化超额收益率将达到 +5%\"(单资产绝对观点)\n",
|
||
"# \"S01 will earn +5% excess return per year\" (absolute view)\n",
|
||
"#\n",
|
||
"# 观点2 (View 2): \"S00 将比 S04 多赚 3%\"(两资产相对观点)\n",
|
||
"# \"S00 will outperform S04 by 3%\" (relative view)\n",
|
||
"\n",
|
||
"K = 2 # 观点数量 (Number of views)\n",
|
||
"\n",
|
||
"# P 矩阵:K × N;P[k, i] = 资产 i 在观点 k 中的权重(正=多头,负=空头)\n",
|
||
"P = np.zeros((K, N_STOCKS))\n",
|
||
"P[0, 1] = 1.0 # 观点1:仅 S01\n",
|
||
"P[1, 0] = 1.0 # 观点2:S00 多头\n",
|
||
"P[1, 4] = -1.0 # S04 空头(相对观点)\n",
|
||
"\n",
|
||
"# Q 向量:观点的预期超额收益 (View expected excess returns)\n",
|
||
"Q = np.array([0.05, 0.03])\n",
|
||
"\n",
|
||
"# Ω 矩阵:观点的不确定性(He & Litterman 建议的设置方式)\n",
|
||
"# Ω = τ × diag(P Σ P'),对角元素越大表示该观点越不确定\n",
|
||
"Omega = np.diag(TAU * np.diag(P @ cov_opt.values @ P.T))\n",
|
||
"Omega_inv = np.linalg.inv(Omega)\n",
|
||
"\n",
|
||
"# ─── Step 3: 贝叶斯更新,计算后验预期超额收益 ───\n",
|
||
"# Bayesian posterior expected excess returns\n",
|
||
"tau_cov_inv = np.linalg.inv(TAU * cov_opt.values)\n",
|
||
"A = tau_cov_inv + P.T @ Omega_inv @ P # 精度矩阵之和\n",
|
||
"b = tau_cov_inv @ Pi + P.T @ Omega_inv @ Q\n",
|
||
"\n",
|
||
"mu_bl = np.linalg.solve(A, b) + RF # 后验 = 超额 + 无风险利率\n",
|
||
"mu_bl_s = pd.Series(mu_bl, index=stock_names)\n",
|
||
"\n",
|
||
"# ─── Step 4: 用后验收益做最大夏普优化 ───\n",
|
||
"w_bl = max_sharpe(mu_bl_s, cov_opt, rf=RF)\n",
|
||
"ret_bl, vol_bl, sr_bl = portfolio_stats(w_bl, mu_bl_s, cov_opt)\n",
|
||
"\n",
|
||
"print(f\" 市场均衡隐含收益范围: {Pi_s.min():.1%} ~ {Pi_s.max():.1%}\")\n",
|
||
"print(f\" BL 后验收益范围: {mu_bl_s.min():.1%} ~ {mu_bl_s.max():.1%}\")\n",
|
||
"print(f\" BL 组合: 收益={ret_bl:.1%}, 波动={vol_bl:.1%}, 夏普={sr_bl:.2f}\")\n",
|
||
"print(f\" 观点受益股(S01, S00)权重 BL: {w_bl['S01']:.1%}, {w_bl['S00']:.1%}\")\n",
|
||
"print(f\" 观点受益股(S01, S00)权重 等权: {w_eq['S01']:.1%}, {w_eq['S00']:.1%}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "b76492d7",
|
||
"metadata": {},
|
||
"source": [
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"# §6 带约束的组合优化\n",
|
||
"# Constrained Portfolio Optimization\n",
|
||
"\n",
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"#\n",
|
||
"# 实际组合管理中必须满足多重约束 / Real portfolios need multiple constraints:\n",
|
||
"# ① 单只股票权重上限(集中度管理)/ Max individual weight (concentration limit)\n",
|
||
"# ② 行业权重区间(风格/行业暴露管理)/ Sector weight bounds (style control)\n",
|
||
"# ③ 换手率约束(控制交易成本)/ Turnover constraint (transaction cost control)\n",
|
||
"#\n",
|
||
"# 换手率 (Turnover) 的定义:\n",
|
||
"# Turnover = Σ_i |w_new,i - w_old,i| / 2 (双边换手率,除以 2 避免重复计算)\n",
|
||
"# 或简化为 L1 距离:Σ_i |w_new,i - w_old,i|"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "d5b2f82e",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(f\"\\n[§6] 带约束的最大夏普组合 / Constrained Max-Sharpe Portfolio...\")\n",
|
||
"\n",
|
||
"MAX_STOCK_WT = 0.15 # 单只股票最大 15% / Max 15% per stock\n",
|
||
"MAX_SECTOR_WT = 0.35 # 单行业最大 35% / Max 35% per sector\n",
|
||
"MIN_SECTOR_WT = 0.10 # 单行业最小 10% / Min 10% per sector\n",
|
||
"MAX_TURNOVER = 0.50 # 最大换手率 50% / Max 50% one-way turnover\n",
|
||
"\n",
|
||
"w_prev = np.ones(N_STOCKS) / N_STOCKS # 上期持仓(假设为等权)\n",
|
||
"\n",
|
||
"n = N_STOCKS\n",
|
||
"w0 = np.ones(n) / n\n",
|
||
"\n",
|
||
"extra_cons = []\n",
|
||
"\n",
|
||
"# 约束1: 行业权重上下限\n",
|
||
"for sname, sidx in SECTORS.items():\n",
|
||
" def make_lb(idx): return lambda w: np.sum(w[idx]) - MIN_SECTOR_WT\n",
|
||
" def make_ub(idx): return lambda w: MAX_SECTOR_WT - np.sum(w[idx])\n",
|
||
" extra_cons.append({'type': 'ineq', 'fun': make_lb(sidx)}) # ≥ 10%\n",
|
||
" extra_cons.append({'type': 'ineq', 'fun': make_ub(sidx)}) # ≤ 35%\n",
|
||
"\n",
|
||
"# 约束2: 换手率约束(L1 距离 ≤ 50%)\n",
|
||
"extra_cons.append({'type': 'ineq',\n",
|
||
" 'fun': lambda w: MAX_TURNOVER - np.sum(np.abs(w - w_prev))})\n",
|
||
"\n",
|
||
"result_c = _base_optimize(\n",
|
||
" lambda w: -portfolio_stats(w, mu_opt, cov_opt, RF)[2], # 最小化负夏普\n",
|
||
" n,\n",
|
||
" bounds=[(0.0, MAX_STOCK_WT)] * n,\n",
|
||
" extra_constraints=extra_cons\n",
|
||
")\n",
|
||
"w_constrained = pd.Series(result_c.x, index=stock_names)\n",
|
||
"ret_c, vol_c, sr_c = portfolio_stats(w_constrained, mu_opt, cov_opt)\n",
|
||
"\n",
|
||
"print(f\" 带约束最大夏普: 收益={ret_c:.1%}, 波动={vol_c:.1%}, 夏普={sr_c:.2f}\")\n",
|
||
"print(f\" 最大单股权重: {w_constrained.max():.1%} (约束 ≤{MAX_STOCK_WT:.0%})\")\n",
|
||
"for sname, sidx in SECTORS.items():\n",
|
||
" sw = w_constrained.iloc[sidx].sum()\n",
|
||
" ok = \"✓\" if MIN_SECTOR_WT <= sw <= MAX_SECTOR_WT else \"✗\"\n",
|
||
" print(f\" {ok} {sname}: {sw:.1%}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "d91f9e2a",
|
||
"metadata": {},
|
||
"source": [
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"# §7 滚动回测:五策略对比\n",
|
||
"# Rolling Backtest: 5-Strategy Comparison\n",
|
||
"\n",
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"#\n",
|
||
"# 流程 / Workflow:\n",
|
||
"# 每个调仓日 T(每月一次):\n",
|
||
"# ① 用过去 LOOKBACK 天估计 μ 和 Σ\n",
|
||
"# ② 用当前估计值重新计算各策略权重\n",
|
||
"# ③ 用新权重持有到下次调仓日,每日记录组合收益\n",
|
||
"#\n",
|
||
"# 五种策略 / 5 Strategies:\n",
|
||
"# EW : 等权(不需要优化)\n",
|
||
"# MinVar : 最小方差(只需 Σ,不需要 μ)\n",
|
||
"# MaxSharpe : 最大夏普(需要 μ 和 Σ)\n",
|
||
"# RiskParity : 风险平价(只需 Σ)\n",
|
||
"# BL : Black-Litterman(贝叶斯混合)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "b74dc367",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(f\"\\n[§7] 滚动回测 / Rolling Backtest...\")\n",
|
||
"\n",
|
||
"LOOKBACK = 126 # 协方差回看期(约 6 个月)/ 6-month estimation window\n",
|
||
"REBAL_FREQ = 21 # 调仓频率(约 1 个月)/ Monthly rebalancing\n",
|
||
"\n",
|
||
"STRATEGY_NAMES = [\n",
|
||
" \"等权 EW\",\n",
|
||
" \"最小方差 MinVar\",\n",
|
||
" \"最大夏普 MaxSharpe\",\n",
|
||
" \"风险平价 RP\",\n",
|
||
" \"Black-Litterman BL\",\n",
|
||
"]\n",
|
||
"\n",
|
||
"# 初始化:回看期内用 0 填充(尚未开始实际持仓)\n",
|
||
"daily_pnl = {name: [0.0] * LOOKBACK for name in STRATEGY_NAMES}\n",
|
||
"\n",
|
||
"# 当前持仓权重(初始为等权)\n",
|
||
"cur_w = {name: np.ones(N_STOCKS) / N_STOCKS for name in STRATEGY_NAMES}\n",
|
||
"\n",
|
||
"rebal_idx = list(range(LOOKBACK, N_DAYS, REBAL_FREQ))\n",
|
||
"\n",
|
||
"for t_pos, t in enumerate(rebal_idx):\n",
|
||
" # 取过去 LOOKBACK 天的历史收益率\n",
|
||
" hist = ret_df.iloc[t - LOOKBACK: t]\n",
|
||
"\n",
|
||
" # 估计滚动协方差矩阵(加微小正则化保证正定性)\n",
|
||
" # Rolling covariance (add tiny ridge term to ensure positive definiteness)\n",
|
||
" cov_r_arr = hist.cov().values * FREQ + 1e-7 * np.eye(N_STOCKS)\n",
|
||
" mu_r = hist.mean() * FREQ\n",
|
||
" cov_r = pd.DataFrame(cov_r_arr, index=stock_names, columns=stock_names)\n",
|
||
"\n",
|
||
" try:\n",
|
||
" new_w = {}\n",
|
||
" new_w[\"等权 EW\"] = np.ones(N_STOCKS) / N_STOCKS\n",
|
||
" new_w[\"最小方差 MinVar\"] = min_variance(mu_r, cov_r).values\n",
|
||
" new_w[\"最大夏普 MaxSharpe\"] = max_sharpe(mu_r, cov_r, rf=RF).values\n",
|
||
" new_w[\"风险平价 RP\"] = risk_parity(cov_r).values\n",
|
||
"\n",
|
||
" # Black-Litterman 滚动版本:使用滚动先验 + 固定观点\n",
|
||
" Pi_r = DELTA * cov_r_arr @ mktcap_weights.values\n",
|
||
" try:\n",
|
||
" tci = np.linalg.inv(TAU * cov_r_arr)\n",
|
||
" A_r = tci + P.T @ Omega_inv @ P\n",
|
||
" b_r = tci @ Pi_r + P.T @ Omega_inv @ Q\n",
|
||
" mu_bl_r = np.linalg.solve(A_r, b_r) + RF\n",
|
||
" except np.linalg.LinAlgError:\n",
|
||
" mu_bl_r = mu_r.values\n",
|
||
" new_w[\"Black-Litterman BL\"] = max_sharpe(\n",
|
||
" pd.Series(mu_bl_r, index=stock_names), cov_r, rf=RF\n",
|
||
" ).values\n",
|
||
"\n",
|
||
" cur_w = new_w\n",
|
||
"\n",
|
||
" except Exception:\n",
|
||
" pass # 优化失败时保持上期权重 / Keep previous weights if optimization fails\n",
|
||
"\n",
|
||
" # 计算持有期内每日组合收益\n",
|
||
" next_t = rebal_idx[t_pos + 1] if t_pos + 1 < len(rebal_idx) else N_DAYS\n",
|
||
" for name in STRATEGY_NAMES:\n",
|
||
" w = cur_w[name]\n",
|
||
" for d in range(t, next_t):\n",
|
||
" daily_pnl[name].append(float(w @ ret_df.iloc[d].values))\n",
|
||
"\n",
|
||
"# 截齐到 N_DAYS(防止因最后一段超出)\n",
|
||
"for name in STRATEGY_NAMES:\n",
|
||
" daily_pnl[name] = daily_pnl[name][:N_DAYS]\n",
|
||
"\n",
|
||
"# ── 计算净值曲线 (NAV Curves) ──\n",
|
||
"nav_df = pd.DataFrame(\n",
|
||
" {name: (1 + pd.Series(daily_pnl[name], index=dates)).cumprod()\n",
|
||
" for name in STRATEGY_NAMES}\n",
|
||
")\n",
|
||
"\n",
|
||
"# ── 计算绩效指标 (Performance Metrics) ──\n",
|
||
"perf_rows = []\n",
|
||
"for name in STRATEGY_NAMES:\n",
|
||
" r_s = pd.Series(daily_pnl[name][LOOKBACK:], index=dates[LOOKBACK:])\n",
|
||
" ann_ret = (1 + r_s).prod() ** (FREQ / len(r_s)) - 1 # 年化收益率 (Ann. return)\n",
|
||
" ann_vol = r_s.std() * np.sqrt(FREQ) # 年化波动率 (Ann. vol)\n",
|
||
" sharpe = (ann_ret - RF) / ann_vol if ann_vol > 0 else 0\n",
|
||
" nav_s = (1 + r_s).cumprod()\n",
|
||
" max_dd = (nav_s / nav_s.cummax() - 1).min() # 最大回撤 (Max drawdown)\n",
|
||
" calmar = ann_ret / (-max_dd) if max_dd < 0 else 99.0 # Calmar 比率\n",
|
||
" perf_rows.append({\n",
|
||
" \"策略\": name, \"年化收益\": ann_ret, \"年化波动\": ann_vol,\n",
|
||
" \"夏普比率\": sharpe, \"最大回撤\": max_dd, \"Calmar\": calmar\n",
|
||
" })\n",
|
||
"\n",
|
||
"perf_df = pd.DataFrame(perf_rows).set_index(\"策略\")\n",
|
||
"\n",
|
||
"print(f\"\\n 策略对比 / Strategy Comparison:\")\n",
|
||
"print(f\" {'策略':<28} {'年化收益':>8} {'年化波动':>8} {'夏普':>7} {'最大回撤':>9} {'Calmar':>8}\")\n",
|
||
"print(f\" {'─' * 65}\")\n",
|
||
"for name, row in perf_df.iterrows():\n",
|
||
" print(f\" {name:<28} {row['年化收益']:>8.1%} {row['年化波动']:>8.1%} \"\n",
|
||
" f\"{row['夏普比率']:>7.3f} {row['最大回撤']:>9.1%} {min(row['Calmar'], 99.0):>8.2f}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "0c16a50a",
|
||
"metadata": {},
|
||
"source": [
|
||
"# ══════════════════════════════════════════════════════════════════════\n",
|
||
"# §8 可视化:9 图综合面板\n",
|
||
"# 9-Panel Visualization Dashboard\n",
|
||
"\n",
|
||
"# ══════════════════════════════════════════════════════════════════════"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "b16f0192",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"print(f\"\\n[§8] 生成可视化图表 / Generating visualization...\")\n",
|
||
"\n",
|
||
"# 颜色配置 / Color palette\n",
|
||
"COLORS = {\n",
|
||
" \"等权 EW\": \"#888888\",\n",
|
||
" \"最小方差 MinVar\": \"#2196F3\",\n",
|
||
" \"最大夏普 MaxSharpe\": \"#FF5722\",\n",
|
||
" \"风险平价 RP\": \"#4CAF50\",\n",
|
||
" \"Black-Litterman BL\": \"#9C27B0\",\n",
|
||
"}\n",
|
||
"PCT_FMT = FuncFormatter(lambda x, _: f\"{x:.0%}\")\n",
|
||
"PCT1_FMT = FuncFormatter(lambda x, _: f\"{x:.1%}\")\n",
|
||
"DARK_BG = '#1A1D27'\n",
|
||
"LT = '#E0E0E0' # 浅色文字 (Light text)\n",
|
||
"GRID_C = '#2A2D3A' # 网格颜色 (Grid color)\n",
|
||
"\n",
|
||
"fig = plt.figure(figsize=(22, 22), facecolor='#0F1117')\n",
|
||
"fig.suptitle(\n",
|
||
" \"量化组合优化演示 / Portfolio Optimization Demo\",\n",
|
||
" fontsize=18, color='white', y=0.98, fontweight='bold'\n",
|
||
")\n",
|
||
"gs = gridspec.GridSpec(3, 3, figure=fig, hspace=0.42, wspace=0.32)\n",
|
||
"\n",
|
||
"ax1 = fig.add_subplot(gs[0, 0]) # 有效前沿 Efficient Frontier\n",
|
||
"ax2 = fig.add_subplot(gs[0, 1]) # 组合权重堆叠 Portfolio Weights\n",
|
||
"ax3 = fig.add_subplot(gs[0, 2]) # 行业权重对比 Sector Weights\n",
|
||
"ax4 = fig.add_subplot(gs[1, 0]) # 净值曲线 NAV Curves\n",
|
||
"ax5 = fig.add_subplot(gs[1, 1]) # 风险贡献 Risk Contribution (RP)\n",
|
||
"ax6 = fig.add_subplot(gs[1, 2]) # 滚动夏普 Rolling Sharpe\n",
|
||
"ax7 = fig.add_subplot(gs[2, 0]) # 月度收益箱形图 Monthly Return Box\n",
|
||
"ax8 = fig.add_subplot(gs[2, 1]) # 相关系数热力图 Correlation Heatmap\n",
|
||
"ax9 = fig.add_subplot(gs[2, 2]) # 绩效汇总表 Performance Table\n",
|
||
"\n",
|
||
"for ax in [ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8, ax9]:\n",
|
||
" ax.set_facecolor(DARK_BG)\n",
|
||
" ax.tick_params(colors=LT, labelsize=8)\n",
|
||
" for sp in ax.spines.values():\n",
|
||
" sp.set_edgecolor(GRID_C)\n",
|
||
"\n",
|
||
"# ── 图1:有效前沿 + 随机组合云 + 关键节点 ──\n",
|
||
"# Efficient Frontier + Monte Carlo cloud + key portfolios\n",
|
||
"ax1.set_title(\"有效前沿 Efficient Frontier\", color=LT, fontsize=10, pad=6)\n",
|
||
"ax1.scatter(rand_vols, rand_rets,\n",
|
||
" c=rand_sharpes, cmap='RdYlGn', vmin=0, vmax=2.5,\n",
|
||
" alpha=0.25, s=6, zorder=1)\n",
|
||
"ax1.plot(frontier_vols, frontier_rets, '-', color='#FFD700', lw=2.2, zorder=4,\n",
|
||
" label='有效前沿')\n",
|
||
"# 资本市场线 (Capital Market Line, CML) — 从无风险利率到最大夏普组合并延伸\n",
|
||
"cml_x = np.linspace(0, vol_msr * 1.4, 50)\n",
|
||
"cml_y = RF + sr_msr * cml_x\n",
|
||
"ax1.plot(cml_x, cml_y, '--', color='#FFEB3B', lw=1.0, alpha=0.7,\n",
|
||
" zorder=3, label='资本市场线 CML')\n",
|
||
"# 关键组合点\n",
|
||
"specials = [\n",
|
||
" (\"最小方差\\nMinVar\", vol_min, ret_min, COLORS[\"最小方差 MinVar\"]),\n",
|
||
" (\"最大夏普\\nMaxSharpe\",vol_msr, ret_msr, COLORS[\"最大夏普 MaxSharpe\"]),\n",
|
||
" (\"等权\\nEW\", vol_eq, ret_eq, COLORS[\"等权 EW\"]),\n",
|
||
" (\"风险平价\\nRP\", vol_rp, ret_rp, COLORS[\"风险平价 RP\"]),\n",
|
||
" (\"BL\", vol_bl, ret_bl, COLORS[\"Black-Litterman BL\"]),\n",
|
||
"]\n",
|
||
"for label, v, r, c in specials:\n",
|
||
" ax1.scatter(v, r, s=100, color=c, zorder=6, edgecolors='white', lw=0.8)\n",
|
||
" ax1.annotate(label, (v, r), color=LT, fontsize=6,\n",
|
||
" xytext=(5, 3), textcoords='offset points')\n",
|
||
"# 无风险利率点 (Risk-free rate point)\n",
|
||
"ax1.scatter(0, RF, s=80, color='yellow', zorder=6, marker='*')\n",
|
||
"ax1.annotate(f\"无风险\\nRf={RF:.0%}\", (0, RF), color='yellow', fontsize=6,\n",
|
||
" xytext=(4, 2), textcoords='offset points')\n",
|
||
"ax1.xaxis.set_major_formatter(PCT_FMT)\n",
|
||
"ax1.yaxis.set_major_formatter(PCT_FMT)\n",
|
||
"ax1.set_xlabel(\"年化波动率 Volatility\", color=LT, fontsize=8)\n",
|
||
"ax1.set_ylabel(\"年化收益率 Return\", color=LT, fontsize=8)\n",
|
||
"ax1.legend(fontsize=7, labelcolor=LT, facecolor=DARK_BG, edgecolor=GRID_C)\n",
|
||
"ax1.grid(color=GRID_C, alpha=0.5)\n",
|
||
"\n",
|
||
"# ── 图2:各策略权重堆叠柱状图 ──\n",
|
||
"# Stacked bar chart of portfolio weights\n",
|
||
"ax2.set_title(\"组合权重 Portfolio Weights\", color=LT, fontsize=10, pad=6)\n",
|
||
"weight_order = [\"EW\", \"MinVar\", \"MaxSharpe\", \"RP\", \"BL\"]\n",
|
||
"weight_arrays = [w_eq.values, w_minvar.values, w_maxsharpe.values, w_rp.values, w_bl.values]\n",
|
||
"bar_colors = plt.cm.tab20(np.linspace(0, 1, N_STOCKS))\n",
|
||
"bottoms = np.zeros(len(weight_order))\n",
|
||
"x_pos = np.arange(len(weight_order))\n",
|
||
"for i in range(N_STOCKS):\n",
|
||
" heights = [wa[i] for wa in weight_arrays]\n",
|
||
" ax2.bar(x_pos, heights, bottom=bottoms, color=bar_colors[i], width=0.72)\n",
|
||
" bottoms += heights\n",
|
||
"ax2.set_xticks(x_pos)\n",
|
||
"ax2.set_xticklabels(weight_order, color=LT, fontsize=9)\n",
|
||
"ax2.yaxis.set_major_formatter(PCT_FMT)\n",
|
||
"ax2.set_ylabel(\"权重 Weight\", color=LT, fontsize=8)\n",
|
||
"ax2.grid(axis='y', color=GRID_C, alpha=0.5)\n",
|
||
"\n",
|
||
"# ── 图3:行业权重对比(分组柱状图)──\n",
|
||
"# Grouped bar chart: sector weights per strategy\n",
|
||
"ax3.set_title(\"行业权重对比 Sector Weights\", color=LT, fontsize=10, pad=6)\n",
|
||
"x_sec = np.arange(N_SECTORS)\n",
|
||
"bw = 0.15\n",
|
||
"strat_colors_list = list(COLORS.values())\n",
|
||
"for k, (wname, warr) in enumerate(zip(weight_order, weight_arrays)):\n",
|
||
" sec_wts = [np.sum(warr[SECTORS[s]]) for s in SECTOR_NAMES]\n",
|
||
" off = (k - len(weight_order) / 2 + 0.5) * bw\n",
|
||
" ax3.bar(x_sec + off, sec_wts, width=bw,\n",
|
||
" color=strat_colors_list[k], label=wname, alpha=0.85)\n",
|
||
"ax3.set_xticks(x_sec)\n",
|
||
"ax3.set_xticklabels(SECTOR_SHORT, color=LT, fontsize=8)\n",
|
||
"ax3.yaxis.set_major_formatter(PCT_FMT)\n",
|
||
"ax3.axhline(MAX_SECTOR_WT, ls='--', color='#FF7043', lw=0.8, alpha=0.6,\n",
|
||
" label=f'上限 {MAX_SECTOR_WT:.0%}')\n",
|
||
"ax3.legend(fontsize=6, labelcolor=LT, facecolor=DARK_BG, edgecolor=GRID_C,\n",
|
||
" ncol=2, loc='upper right')\n",
|
||
"ax3.grid(axis='y', color=GRID_C, alpha=0.5)\n",
|
||
"\n",
|
||
"# ── 图4:净值曲线 ──\n",
|
||
"ax4.set_title(\"净值曲线 NAV Curves\", color=LT, fontsize=10, pad=6)\n",
|
||
"for name in STRATEGY_NAMES:\n",
|
||
" is_key = \"MaxSharpe\" in name or \"BL\" in name\n",
|
||
" ax4.plot(nav_df.index, nav_df[name],\n",
|
||
" color=COLORS[name], lw=2.0 if is_key else 1.2,\n",
|
||
" alpha=1.0 if is_key else 0.7,\n",
|
||
" label=name.split(\" \")[0])\n",
|
||
"ax4.set_ylabel(\"净值 NAV\", color=LT, fontsize=8)\n",
|
||
"ax4.legend(fontsize=7, labelcolor=LT, facecolor=DARK_BG,\n",
|
||
" edgecolor=GRID_C, ncol=2)\n",
|
||
"ax4.grid(color=GRID_C, alpha=0.5)\n",
|
||
"ax4.xaxis.set_tick_params(rotation=20)\n",
|
||
"\n",
|
||
"# ── 图5:风险平价组合的风险贡献分解 ──\n",
|
||
"# Risk contribution breakdown for Risk Parity portfolio\n",
|
||
"ax5.set_title(\"风险贡献分布 Risk Contribution (RP)\", color=LT, fontsize=10, pad=6)\n",
|
||
"rc_sorted = pd.Series(rc_pct_rp * 100, index=stock_names).sort_values(ascending=False)\n",
|
||
"bar_c = ['#4CAF50' if abs(v / 100 - 1.0 / N_STOCKS) < 5e-3\n",
|
||
" else '#FF7043' for v in rc_sorted]\n",
|
||
"ax5.bar(range(N_STOCKS), rc_sorted.values, color=bar_c, alpha=0.85)\n",
|
||
"ax5.axhline(100.0 / N_STOCKS, color='white', lw=1.2, ls='--',\n",
|
||
" label=f'目标 1/N={100/N_STOCKS:.1f}%')\n",
|
||
"ax5.set_xticks(range(N_STOCKS))\n",
|
||
"ax5.set_xticklabels(rc_sorted.index, rotation=45, fontsize=6, color=LT)\n",
|
||
"ax5.set_ylabel(\"风险贡献 RC%\", color=LT, fontsize=8)\n",
|
||
"ax5.legend(fontsize=7, labelcolor=LT, facecolor=DARK_BG, edgecolor=GRID_C)\n",
|
||
"ax5.grid(axis='y', color=GRID_C, alpha=0.5)\n",
|
||
"\n",
|
||
"# ── 图6:滚动夏普比率(63 日窗口)──\n",
|
||
"# Rolling Sharpe ratio (63-day window)\n",
|
||
"ax6.set_title(\"滚动夏普比率 Rolling Sharpe (63D)\", color=LT, fontsize=10, pad=6)\n",
|
||
"ROLL_WIN = 63\n",
|
||
"for name in STRATEGY_NAMES:\n",
|
||
" r_s = pd.Series(daily_pnl[name], index=dates)\n",
|
||
" roll_sr = r_s.rolling(ROLL_WIN).apply(\n",
|
||
" lambda x: (x.mean() * FREQ - RF) / (x.std() * np.sqrt(FREQ) + 1e-10)\n",
|
||
" )\n",
|
||
" ax6.plot(dates, roll_sr, color=COLORS[name], lw=1.0,\n",
|
||
" alpha=0.85, label=name.split(\" \")[0])\n",
|
||
"ax6.axhline(0, color='white', lw=0.7, ls='--')\n",
|
||
"ax6.set_ylabel(\"夏普比率 Sharpe\", color=LT, fontsize=8)\n",
|
||
"ax6.legend(fontsize=7, labelcolor=LT, facecolor=DARK_BG,\n",
|
||
" edgecolor=GRID_C, ncol=2)\n",
|
||
"ax6.grid(color=GRID_C, alpha=0.5)\n",
|
||
"ax6.xaxis.set_tick_params(rotation=20)\n",
|
||
"\n",
|
||
"# ── 图7:月度收益分布箱形图 ──\n",
|
||
"# Monthly return distribution boxplot\n",
|
||
"ax7.set_title(\"月度收益分布 Monthly Return Distribution\", color=LT, fontsize=10, pad=6)\n",
|
||
"monthly_rets = {}\n",
|
||
"for name in STRATEGY_NAMES:\n",
|
||
" r_s = pd.Series(daily_pnl[name], index=dates)\n",
|
||
" monthly_rets[name.split(\" \")[0]] = r_s.resample('M').apply(\n",
|
||
" lambda x: (1 + x).prod() - 1\n",
|
||
" ).values\n",
|
||
"\n",
|
||
"bp = ax7.boxplot(\n",
|
||
" list(monthly_rets.values()),\n",
|
||
" patch_artist=True,\n",
|
||
" labels=list(monthly_rets.keys()),\n",
|
||
" medianprops=dict(color='white', lw=1.5),\n",
|
||
" whiskerprops=dict(color=LT),\n",
|
||
" capprops=dict(color=LT),\n",
|
||
" flierprops=dict(marker='.', color='#888888', ms=3)\n",
|
||
")\n",
|
||
"for patch, c in zip(bp['boxes'], list(COLORS.values())):\n",
|
||
" patch.set_facecolor(c)\n",
|
||
" patch.set_alpha(0.72)\n",
|
||
"ax7.yaxis.set_major_formatter(PCT1_FMT)\n",
|
||
"ax7.axhline(0, color='white', lw=0.6, ls='--')\n",
|
||
"ax7.set_xticklabels(list(monthly_rets.keys()), color=LT, fontsize=8, rotation=15)\n",
|
||
"ax7.grid(axis='y', color=GRID_C, alpha=0.5)\n",
|
||
"\n",
|
||
"# ── 图8:收益相关系数热力图(含行业分割线)──\n",
|
||
"# Return correlation heatmap with sector dividers\n",
|
||
"ax8.set_title(\"收益相关系数 Return Correlation Matrix\", color=LT, fontsize=10, pad=6)\n",
|
||
"corr = ret_df.corr().values\n",
|
||
"im = ax8.imshow(corr, cmap='RdYlGn', vmin=-0.2, vmax=1.0, aspect='auto')\n",
|
||
"ax8.set_xticks(range(N_STOCKS))\n",
|
||
"ax8.set_yticks(range(N_STOCKS))\n",
|
||
"ax8.set_xticklabels(stock_names, rotation=90, fontsize=5.5, color=LT)\n",
|
||
"ax8.set_yticklabels(stock_names, fontsize=5.5, color=LT)\n",
|
||
"# 行业分割线 / Sector boundary lines\n",
|
||
"sector_bounds = [0, 5, 9, 13, 17, 20]\n",
|
||
"for b in sector_bounds[1:-1]:\n",
|
||
" ax8.axhline(b - 0.5, color='white', lw=0.9, alpha=0.8)\n",
|
||
" ax8.axvline(b - 0.5, color='white', lw=0.9, alpha=0.8)\n",
|
||
"cb = plt.colorbar(im, ax=ax8, fraction=0.046, pad=0.04)\n",
|
||
"cb.ax.tick_params(colors=LT, labelsize=7)\n",
|
||
"\n",
|
||
"# ── 图9:绩效汇总表 ──\n",
|
||
"# Performance summary table\n",
|
||
"ax9.axis('off')\n",
|
||
"ax9.set_title(\"绩效汇总 Performance Summary\", color=LT, fontsize=10, pad=6)\n",
|
||
"col_labels = [\"年化收益\\nAnn.Ret\", \"年化波动\\nAnn.Vol\",\n",
|
||
" \"夏普比率\\nSharpe\", \"最大回撤\\nMax DD\", \"Calmar\\n比率\"]\n",
|
||
"row_labels = [n.split(\" \")[0] for n in STRATEGY_NAMES]\n",
|
||
"cell_data = []\n",
|
||
"for name in STRATEGY_NAMES:\n",
|
||
" r = perf_df.loc[name]\n",
|
||
" cell_data.append([\n",
|
||
" f\"{r['年化收益']:+.1%}\",\n",
|
||
" f\"{r['年化波动']:.1%}\",\n",
|
||
" f\"{r['夏普比率']:.2f}\",\n",
|
||
" f\"{r['最大回撤']:.1%}\",\n",
|
||
" f\"{min(r['Calmar'], 99.0):.2f}\",\n",
|
||
" ])\n",
|
||
"\n",
|
||
"tbl = ax9.table(cellText=cell_data, rowLabels=row_labels,\n",
|
||
" colLabels=col_labels, loc='center', cellLoc='center')\n",
|
||
"tbl.auto_set_font_size(False)\n",
|
||
"tbl.set_fontsize(8)\n",
|
||
"tbl.scale(1.12, 2.0)\n",
|
||
"for (row, col), cell in tbl.get_celld().items():\n",
|
||
" cell.set_facecolor(DARK_BG)\n",
|
||
" cell.set_edgecolor(GRID_C)\n",
|
||
" cell.set_text_props(color=LT)\n",
|
||
" if row == 0 or col == -1:\n",
|
||
" cell.set_facecolor('#2A2D3A')\n",
|
||
" cell.set_text_props(color='#FFD700', fontweight='bold')\n",
|
||
"\n",
|
||
"# 保存图表 / Save figure\n",
|
||
"OUTPUT_FILE = \"portfolio_optimization_demo.png\"\n",
|
||
"plt.savefig(OUTPUT_FILE, dpi=150, bbox_inches='tight',\n",
|
||
" facecolor=fig.get_facecolor())\n",
|
||
"\n",
|
||
"print(f\" 图表已保存: {OUTPUT_FILE} / Chart saved: {OUTPUT_FILE}\")\n",
|
||
"\n",
|
||
"print(\"\\n\" + \"=\" * 70)\n",
|
||
"print(\" 演示完成! Demo Complete!\")\n",
|
||
"print(f\" 输出: {OUTPUT_FILE}\")\n",
|
||
"print(\"=\" * 70)"
|
||
]
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": "Python 3 (trading)",
|
||
"language": "python",
|
||
"name": "python3"
|
||
},
|
||
"language_info": {
|
||
"name": "python",
|
||
"version": "3.11.0"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5
|
||
}
|