diff --git a/doc_05_portfolio_optimization.md b/doc_05_portfolio_optimization.md new file mode 100644 index 0000000..d097217 --- /dev/null +++ b/doc_05_portfolio_optimization.md @@ -0,0 +1,619 @@ +# 组合优化详解 +## `quant_portfolio_optimization_demo.py` 学习文档 + +> **目标读者**:量化入门者,已完成前四篇学习 +> **配套文件**:`quant_portfolio_optimization_demo.py` +> **系列位置**:第 5 篇 — 组合优化篇 + +--- + +## 目录 + +1. [为什么需要组合优化?](#1-为什么需要组合优化) +2. [核心数学框架:均值-方差优化](#2-均值-方差优化mean-variance-optimization) +3. [协方差矩阵估计:从样本到收缩](#3-协方差矩阵估计) +4. [有效前沿与资本市场线](#4-有效前沿与资本市场线) +5. [三种特殊组合](#5-三种特殊组合) +6. [风险平价:不依赖预期收益的方法](#6-风险平价risk-parity) +7. [Black-Litterman 模型:贝叶斯框架](#7-black-litterman-模型) +8. [带约束的组合优化](#8-带约束的组合优化) +9. [滚动回测:五策略横向对比](#9-滚动回测) +10. [结果解读](#10-结果解读) +11. [术语速查表](#11-术语速查表) + +--- + +## 1. 为什么需要组合优化? + +### 1.1 从"选股"到"构建组合" + +前几篇 Demo 告诉我们如何**选择好股票**(Alpha 因子)。但知道哪些股票好之后,下一个问题是: + +> "我手里有 20 只股票,分别该买多少?" + +这个问题看似简单,但影响极大: + +``` +方案1:每只股票买 5%(等权,最简单) +方案2:只买最好的 3 只,各 33%(过度集中) +方案3:根据各股的风险-收益特性,科学分配权重 ← 这就是组合优化 +``` + +### 1.2 多元化的数学价值 + +多元化 (Diversification) 的精华在于:**不同资产的涨跌不完全同步(相关系数 < 1),组合的风险可以低于各资产风险的加权平均**。 + +$$\sigma_p = \sqrt{\sum_i \sum_j w_i w_j \sigma_{ij}} \leq \sum_i w_i \sigma_i$$ + +其中 $\sigma_{ij} = \rho_{ij} \sigma_i \sigma_j$ 是协方差,$\rho_{ij}$ 是相关系数。 + +**当 $\rho_{ij} < 1$ 时,等号不成立,组合波动率严格小于各股波动率的加权平均。** + +直觉例子: + +| 资产 | 波动率 | 情景 | +|------|--------|------| +| 科技股 A | 30% | 经济好时大涨,经济差时大跌 | +| 医疗股 B | 25% | 与经济景气关联较小,独立波动 | +| A+B 等权 | ≈ **20%**(低于 27.5%!)| 两者不完全相关,组合风险被"稀释" | + +### 1.3 优化的目标 + +Harry Markowitz (1952) 在诺贝尔奖论文中指出,理性投资者应该: + +> **在承担相同风险的前提下,选择期望收益最高的组合; +> 在期望相同收益的前提下,选择风险最小的组合。** + +这就是"均值-方差框架 (Mean-Variance Framework)"的核心逻辑。 + +--- + +## 2. 均值-方差优化(Mean-Variance Optimization) + +### 2.1 问题形式 + +组合优化的数学问题可以写成: + +$$\boxed{\min_{w}\ w'\Sigma w \quad \text{subject to} \quad w'\mu = \mu^*, \quad \mathbf{1}'w = 1, \quad w \geq 0}$$ + +| 符号 | 含义 | +|------|------| +| $w$ | 权重向量 (weight vector), shape $(N, 1)$ | +| $\Sigma$ | 协方差矩阵 (covariance matrix), shape $(N, N)$ | +| $\mu$ | 预期收益向量 (expected return vector), shape $(N, 1)$ | +| $\mu^*$ | 目标收益率 (target return) | +| $\mathbf{1}'w = 1$ | 全投资约束 (full investment constraint) | +| $w \geq 0$ | 多头约束 (long-only constraint) | + +这个问题在不同目标收益率 $\mu^*$ 下重复求解,就得到了**有效前沿**。 + +### 2.2 算法:SLSQP(序列二次规划) + +本 Demo 使用 `scipy.optimize.minimize` 的 SLSQP 方法: + +``` +SLSQP = Sequential Least Squares Programming + 序列二次规划法 +``` + +SLSQP 每轮迭代将非线性约束优化问题近似为一个二次规划 (QP) 子问题,逐步收敛到最优解。 + +```python +from scipy.optimize import minimize + +result = minimize( + objective, # 目标函数(如最小化组合方差) + w0, # 初始权重(等权作为起点) + method='SLSQP', + bounds=[(0.0, 1.0)] * N, # 权重区间:[0%, 100%] + constraints=[ + {'type': 'eq', 'fun': lambda w: sum(w) - 1} # 权重之和 = 1 + ], + options={'ftol': 1e-12, 'maxiter': 1500} +) +``` + +### 2.3 组合的三个核心指标 + +给定权重 $w$、预期收益 $\mu$、协方差矩阵 $\Sigma$,可以计算: + +| 指标 | 公式 | 说明 | +|------|------|------| +| 组合收益率 | $r_p = w'\mu$ | 各股收益的加权平均 | +| 组合波动率 | $\sigma_p = \sqrt{w'\Sigma w}$ | 关键!非线性,受协方差影响 | +| 夏普比率 | $SR = (r_p - r_f) / \sigma_p$ | 单位风险的超额收益,越高越好 | + +--- + +## 3. 协方差矩阵估计 + +### 3.1 为什么协方差矩阵很重要? + +组合优化的结果对协方差矩阵的估计极其敏感。$\Sigma$ 的一个小误差,可能导致组合权重大幅波动——这是 Markowitz 框架在实践中最大的挑战。 + +### 3.2 样本协方差矩阵的问题 + +$$\hat{\Sigma}_{sample} = \frac{1}{T-1} \sum_{t=1}^{T} (r_t - \bar{r})(r_t - \bar{r})'$$ + +**问题1:维度诅咒 (Curse of Dimensionality)** +估计一个 $N$ 只股票的协方差矩阵,需要估计 $N(N+1)/2$ 个参数。 +20 只股票 → 210 个参数;50 只股票 → 1275 个参数 +当 T(样本长度)与 N(资产数量)之比较小时(如 T/N < 5),估计误差极大。 + +**问题2:条件数过高 (High Condition Number)** + +``` +条件数 (Condition Number) = 最大特征值 / 最小特征值 +``` + +条件数高意味着矩阵"病态"——求逆时微小误差被放大,优化结果不稳定。 + +### 3.3 Ledoit-Wolf 收缩估计 + +**核心思想**:将样本协方差矩阵向一个"结构化目标"方向收缩: + +$$\hat{\Sigma}_{LW} = (1-\alpha) \hat{\Sigma}_{sample} + \alpha \cdot F$$ + +其中: +- $F$ 是目标矩阵(如"单位矩阵×平均方差"),结构简单,估计误差小 +- $\alpha$ 是**最优收缩系数**(由数据自动决定,不需手动调参) + +**收缩的直觉**: +- 样本协方差:复杂,可能过度拟合历史数据 +- 目标矩阵:简单,但忽略了资产间真实相关性 +- 收缩后:二者的折中,在偏差和方差之间取平衡 + +```python +from sklearn.covariance import LedoitWolf +lw = LedoitWolf() +lw.fit(return_data) +cov_shrunk = lw.covariance_ # 收缩后的协方差矩阵 +shrink_coeff = lw.shrinkage_ # 最优收缩系数 α(自动计算) +``` + +**本 Demo 结果**: +- 样本协方差条件数:35.4 +- 收缩后条件数:34.0(收缩系数 α = 0.0098,很小 → 本数据质量较好,无需大幅收缩) + +--- + +## 4. 有效前沿与资本市场线 + +### 4.1 有效前沿(Efficient Frontier) + +**有效前沿**是所有"帕累托最优"组合的集合: +- 在相同波动率下,收益最高 +- 在相同收益下,波动率最低 + +有效前沿由两个端点确定: +- **左端点**:全局最小方差组合 (GMV)——风险最低的点 +- **右端点**:单只股票(预期收益最高的那只,极度集中) + +``` + ▲ 收益率 + │ × × ←← 有效前沿 + │ × MaxSharpe ● + │ × MinVar ● + │ × × × + │ × × × × + │────────────────────→ 波动率 +``` + +位于有效前沿**左下方**的是"劣势组合"——存在一个前沿组合,既有更高收益、又有更低风险。 + +### 4.2 资本市场线(Capital Market Line, CML) + +引入无风险资产(如国债,年化收益率 $r_f = 2\%$)后: + +- 投资者可以在"无风险资产"和"某个风险组合"之间分配资金 +- 最优的风险组合就是从 $r_f$ 出发,与有效前沿相切的那点——**切线组合 (Tangency Portfolio)**,也叫**最大夏普组合** + +资本市场线的方程: + +$$r_p = r_f + SR_{tangency} \times \sigma_p$$ + +- 斜率 $SR_{tangency}$ 就是切线组合的夏普比率 +- **任何有效投资者都应该持有切线组合**(可以加上无风险资产来调节总风险) + +--- + +## 5. 三种特殊组合 + +### 5.1 等权组合(Equal Weight, EW) + +$$w_i = \frac{1}{N} \quad \forall i$$ + +**优点**: +- 最简单,无需任何参数估计 +- 对估计误差完全免疫 +- 实证研究发现,等权组合在许多市场中难以被复杂模型超越("1/N 难题") + +**缺点**: +- 完全忽略各股的风险差异(高波动股和低波动股同等对待) +- 没有利用任何收益预测信息 + +### 5.2 最小方差组合(Minimum Variance Portfolio, MinVar) + +$$\min_w\ w'\Sigma w \quad \text{s.t.}\ \mathbf{1}'w=1,\ w\geq 0$$ + +**重要特点**:**完全不需要预期收益 $\mu$ 的估计!** + +这是一个极大的优势——预期收益 $\mu$ 是非常难以估计的,而协方差矩阵 $\Sigma$ 相对更稳定(因为波动率和相关性的历史规律更具持久性)。 + +**适用场景**:投资者没有可靠的收益预测,但希望最小化风险。 + +**本 Demo 结果**: +- 收益 12.5%,波动 **18.5%**(所有策略中最低!),夏普 0.57 +- 回测中夏普 1.005 — 体现了低波动的稳健性 + +### 5.3 最大夏普组合(Maximum Sharpe / Tangency Portfolio) + +$$\max_w\ \frac{w'\mu - r_f}{\sqrt{w'\Sigma w}} \quad \text{等价于}\ \min_w\ -\text{SR}$$ + +**这是理论上最优的风险组合**,但高度依赖对 $\mu$ 的准确估计。 + +实践中的问题: + +1. **误差传导**:$\mu$ 的估计误差被优化器"放大",产生极端权重 +2. **集中效应**:最大夏普组合常常把权重集中在少数几只"看起来最好"的股票上 +3. **过拟合**:历史中表现好的股票,未来未必继续好 + +**本 Demo 结果**: +- 收益 50.4%,波动 **29.7%**(最高!),夏普 1.63(基于全期数据的理论值) +- 回测夏普 0.844 — 因为样本内最优 ≠ 样本外最优,存在过拟合 + +--- + +## 6. 风险平价(Risk Parity) + +### 6.1 核心思想 + +等权组合 → 权重相等 → 但高波动股对组合风险贡献更大! + +风险平价 → **风险贡献相等** → 每只股票对总风险的贡献相同 + +``` +等权组合中: + 低波动股(σ=15%)权重 5% → 风险贡献 很小 + 高波动股(σ=35%)权重 5% → 风险贡献 很大 ← 实际上被高波动股"主导" + +风险平价组合: + 低波动股(σ=15%)权重 大(≈10%)→ 风险贡献 = 1/N + 高波动股(σ=35%)权重 小(≈2%)→ 风险贡献 = 1/N ← 真正均衡 +``` + +### 6.2 风险贡献的数学 + +**边际风险贡献 (Marginal Risk Contribution, MRC)**: + +$$\text{MRC}_i = \frac{\partial \sigma_p}{\partial w_i} = \frac{(\Sigma w)_i}{\sigma_p}$$ + +**绝对风险贡献 (Risk Contribution, RC)**: + +$$\text{RC}_i = w_i \times \text{MRC}_i = \frac{w_i \cdot (\Sigma w)_i}{\sigma_p}$$ + +**验证**:$\sum_i \text{RC}_i = \sigma_p$(各股风险贡献之和 = 总波动率) + +**等风险贡献条件**: + +$$\frac{\text{RC}_i}{\sigma_p} = \frac{1}{N} \quad \Leftrightarrow \quad w_i \cdot (\Sigma w)_i = w_j \cdot (\Sigma w)_j \quad \forall i, j$$ + +### 6.3 风险平价的优化问题 + +$$\min_w \sum_i \left(\frac{\text{RC}_i}{\sigma_p} - \frac{1}{N}\right)^2$$ + +这是一个非线性最小二乘问题(没有解析解),需要数值优化求解。 + +```python +def objective(w): + rc, sigma_p = risk_contributions(w, cov) + rc_pct = rc / sigma_p # 各资产风险贡献占比 + target = 1.0 / N # 目标:1/N + return np.sum((rc_pct - target) ** 2) +``` + +**本 Demo 结果**: +- 各股 RC 最大偏差 = **0.000**(完美等风险贡献!) +- 收益 9.5%,波动 21.0%,夏普 0.36(因为低波动持仓更多低收益股) + +### 6.4 风险平价的适用场景 + +| 场景 | 适合 | 理由 | +|------|------|------| +| 没有可靠的收益预期 | ✓ | 不需要 μ 估计 | +| 管理多资产配置(股债商品)| ✓ | 各资产类别风险差异大 | +| 需要稳健的长期策略 | ✓ | 比最大夏普更稳定 | +| 单一股票池、收益差异明显 | △ | 可能过于保守 | + +--- + +## 7. Black-Litterman 模型 + +### 7.1 Markowitz 框架的根本缺陷 + +**对预期收益的"蝴蝶效应"**: + +Markowitz 优化是一个"误差放大器"——$\mu$ 哪怕有 1% 的估计误差,权重可能变动 30%+。这导致实际使用中,最大夏普组合常常产生极度集中的"角点解"。 + +Black-Litterman(1990)给出了一个优雅的解决方案:**从市场均衡出发,而不是从历史均值出发**。 + +### 7.2 三步 BL 框架 + +**第一步:建立先验(市场均衡隐含收益)** + +市场是所有投资者信息的综合。如果市场是均衡的,当前市值加权组合 $w_{mkt}$ 就是所有投资者的"最优"选择。 + +反向推导这个最优选择对应的隐含收益(CAPM 均衡): + +$$\Pi = \delta \cdot \Sigma \cdot w_{mkt}$$ + +| 符号 | 含义 | +|------|------| +| $\Pi$ | 市场均衡隐含超额收益 (Market-implied excess returns) | +| $\delta$ | 市场风险厌恶系数,通常取 2.0~3.0 | +| $\Sigma$ | 协方差矩阵 | +| $w_{mkt}$ | 市值权重组合 (Market cap weights) | + +这个先验的优点:**总是给出合理的、多元化的权重**,不会产生极端集中。 + +--- + +**第二步:表达投资者观点** + +投资者可以输入对特定股票(或组合)的主观看法: + +``` +观点矩阵 P (View matrix, K × N): + P[k, i] = 股票 i 在观点 k 中的权重(正=多头,负=空头) + +观点收益向量 Q (View returns, K × 1): + Q[k] = 观点 k 的预期超额收益 + +观点不确定性矩阵 Ω (View uncertainty, K × K 对角矩阵): + Ω[k,k] 越大 = 对第 k 个观点越不自信 +``` + +本 Demo 的两个观点: + +```python +# 观点1(绝对观点): "S01 的年化超额收益将达到 +5%" +P[0, 1] = 1.0 +Q[0] = 0.05 + +# 观点2(相对观点): "S00 将比 S04 多赚 3%" +P[1, 0] = +1.0 # S00 多头 +P[1, 4] = -1.0 # S04 空头 +Q[1] = 0.03 +``` + +--- + +**第三步:贝叶斯更新,求后验预期收益** + +$$\mu_{BL} = \underbrace{\left[(\tau\Sigma)^{-1} + P'\Omega^{-1}P\right]^{-1}}_{\text{精度矩阵之和的逆}} \cdot \underbrace{\left[(\tau\Sigma)^{-1}\Pi + P'\Omega^{-1}Q\right]}_{\text{加权均值向量}}$$ + +直观理解: +- $(\tau\Sigma)^{-1}$ 越大 → 先验越"确定",后验越接近先验 $\Pi$ +- $\Omega^{-1}$ 越大 → 观点越"确定",后验越接近观点 $Q$ +- $\tau$ 是先验的"置信度调节参数",通常取 0.025~0.10 + +**本 Demo 结果**: +- BL 将 S00(观点2的受益者)权重从 5%(等权)调升至 **17.1%** +- 后验收益比均衡先验更集中在观点受益股 +- 回测中 BL 夏普 **1.107**,Calmar **1.51**——所有策略中最优 + +### 7.3 BL vs 直接 Markowitz + +| | 直接 Markowitz | Black-Litterman | +|--|----------------|-----------------| +| 先验 | 历史均值(含估计误差) | 市场均衡(反向优化,更稳定) | +| 观点融入 | 直接修改 μ(极端敏感) | 贝叶斯混合(平滑过渡) | +| 结果 | 容易极度集中 | 分散化程度好 | +| 无观点时 | 结果无意义 | 退回到市值加权 | + +--- + +## 8. 带约束的组合优化 + +### 8.1 为什么需要约束? + +纯粹的 Markowitz 优化常常产生: +- 单只股票权重 40%+(过度集中) +- 某行业权重 0%(完全空缺某一行业的风险敞口) +- 与上期持仓相差 80%(极高换手率 → 高交易成本) + +实际组合管理需要通过约束来控制这些问题。 + +### 8.2 三类常见约束 + +**约束1:权重上下限(集中度管理)** + +```python +bounds = [(0.0, 0.15)] * N # 每只股票:0% ≤ w_i ≤ 15% +``` + +**约束2:行业权重区间** + +```python +# 每行业权重在 10%~35% 之间 +for sector_indices in SECTORS.values(): + constraints.append({ + 'type': 'ineq', + 'fun': lambda w: sum(w[sector_indices]) - 0.10 # ≥ 10% + }) + constraints.append({ + 'type': 'ineq', + 'fun': lambda w: 0.35 - sum(w[sector_indices]) # ≤ 35% + }) +``` + +**约束3:换手率约束(交易成本控制)** + +```python +# 本期与上期权重的 L1 距离 ≤ 50% +# L1 distance = Σ |w_new,i - w_old,i| +constraints.append({ + 'type': 'ineq', + 'fun': lambda w: 0.50 - np.sum(np.abs(w - w_prev)) +}) +``` + +**本 Demo 结果(带约束最大夏普)**: +- 夏普 0.94(vs 无约束的 1.63)——约束降低了理论最优解的质量 +- 但实际上更可行:最大单股 15%,各行业 10%~35%,约束全部满足 ✓ + +### 8.3 约束与优化效率的权衡 + +``` +约束越多 → 可行域越小 → 最优解越差(理论值) +约束越多 → 组合越分散 → 实际表现可能更好(防止过拟合) +``` + +这是组合管理的核心矛盾:**理论最优 vs 实践可行**。 + +--- + +## 9. 滚动回测 + +### 9.1 滚动回测的必要性 + +如果用全量历史数据估计 $\mu$ 和 $\Sigma$,再用"最优"权重买入,实际上**使用了未来信息**(前视偏差 / Look-Ahead Bias)——这样的回测结果不真实。 + +正确做法:滚动窗口(Walk-Forward)回测 + +``` +时间轴: ──────── LOOKBACK ──────────────── 测试期 ────────── + + t=0 t=126 t=147 t=168 ... + │─── 估计期 126 天 ──│─ 持有21天 ─│─ 持有21天 ─│ + ↑ ↑ + 估计 μ、Σ 用新数据重新估计,重新优化 +``` + +### 9.2 调仓流程 + +```python +for t in rebalance_dates: + # ① 用过去 126 天历史估计 μ 和 Σ + hist = returns.iloc[t-126 : t] + mu_roll = hist.mean() * 252 + cov_roll = hist.cov() * 252 + + # ② 用当前估计重新计算各策略权重 + w_minvar = min_variance(mu_roll, cov_roll) + w_maxsharpe = max_sharpe(mu_roll, cov_roll) + # ... + + # ③ 持有到下次调仓日,每日记录组合收益 + for d in range(t, next_rebalance): + portfolio_return = w @ returns.iloc[d] +``` + +### 9.3 五策略对比结果 + +| 策略 | 年化收益 | 年化波动 | 夏普比率 | 最大回撤 | Calmar | +|------|----------|----------|----------|----------|--------| +| 等权 EW | 17.6% | 22.4% | 0.70 | -22.3% | 0.79 | +| 最小方差 MinVar | 21.6% | **19.5%** | 1.00 | -22.1% | 0.98 | +| 最大夏普 MaxSharpe | **25.0%** | 27.2% | 0.84 | -24.5% | 1.02 | +| 风险平价 RP | 17.3% | 21.6% | 0.71 | -21.7% | 0.80 | +| **Black-Litterman BL** | **25.6%** | 21.3% | **1.11** | **-16.9%** | **1.51** | + +--- + +## 10. 结果解读 + +### 10.1 最小方差 vs 等权 + +MinVar 收益比等权高 4%,波动比等权低 3%——用更复杂的优化换来了双向改善。 +这通常在**样本量充足、协方差估计可靠**时发生。 + +### 10.2 最大夏普的"过拟合"现象 + +理论值:Sharpe = 1.63(全量数据) +回测值:Sharpe = 0.84(样本外) + +**下降 50%**——这就是过拟合的代价。滚动窗口估计出的 $\mu$ 不稳定,每次重估都有误差,导致权重频繁剧烈变动。 + +### 10.3 Black-Litterman 为什么最好? + +1. **先验稳定**:从市场均衡出发,不过度依赖嘈杂的历史均值 +2. **观点有效**:本 Demo 中 S00/S01(科技/金融)确实注入了较高的 Alpha,观点与真实 Alpha 方向一致 +3. **不极端集中**:贝叶斯混合平滑了权重,避免了角点解 + +> **注意**:如果投资者观点是错误的,BL 的结果可能比等权更差。BL 的价值在于"正确观点"的放大,而不是错误观点的保护。 + +### 10.4 Calmar 比率与最大回撤 + +$$\text{Calmar} = \frac{\text{年化收益}}{\text{最大回撤绝对值}}$$ + +BL 策略 Calmar = 1.51,最大回撤仅 -16.9%,说明在每次大幅下跌时损失相对可控。这对于风险管理 (Risk Management) 极为重要——一个持续盈利但某年大幅亏损的策略,在实际操作中往往难以坚持。 + +--- + +## 11. 术语速查表 + +| 中文 | English | 简要说明 | +|------|---------|----------| +| 组合优化 | Portfolio Optimization | 在约束下选择最优资产权重 | +| 均值-方差框架 | Mean-Variance Framework | Markowitz (1952) 的经典框架 | +| 权重向量 | Weight Vector (w) | 各资产的持仓比例 | +| 协方差矩阵 | Covariance Matrix (Σ) | 资产收益率的协方差关系矩阵 | +| 预期收益向量 | Expected Return Vector (μ) | 各资产的预期年化收益率 | +| 多元化 | Diversification | 持有多种相关性低的资产,降低整体风险 | +| 有效前沿 | Efficient Frontier | 给定风险下收益最高的组合集合 | +| 全局最小方差组合 | Global Minimum Variance Portfolio (GMV) | 有效前沿的左端点,风险最低 | +| 切线组合 | Tangency Portfolio | 资本市场线与有效前沿的切点,即最大夏普组合 | +| 资本市场线 | Capital Market Line (CML) | 从无风险利率到切线组合并延伸的直线 | +| 最大夏普组合 | Maximum Sharpe Portfolio | 夏普比率最高的组合 | +| 无风险利率 | Risk-Free Rate (Rf) | 国债等无风险资产的收益率 | +| 夏普比率 | Sharpe Ratio | (收益-无风险利率) / 波动率,衡量风险调整后收益 | +| Calmar 比率 | Calmar Ratio | 年化收益 / 最大回撤,衡量下行风险调整后收益 | +| 最大回撤 | Maximum Drawdown (MDD) | 历史上从高点到低点的最大跌幅 | +| 样本协方差 | Sample Covariance | 用历史数据直接计算的协方差矩阵 | +| 条件数 | Condition Number | 矩阵最大特征值/最小特征值之比,衡量矩阵"病态"程度 | +| Ledoit-Wolf 收缩 | Ledoit-Wolf Shrinkage | 将样本协方差向结构化目标收缩,减少估计误差 | +| 收缩系数 | Shrinkage Coefficient (α) | Ledoit-Wolf 中收缩强度的参数,由数据自动决定 | +| 风险平价 | Risk Parity | 每只资产对组合总风险贡献相等的配置方法 | +| 边际风险贡献 | Marginal Risk Contribution (MRC) | 增加该资产权重时组合波动率的变化率 | +| 风险贡献 | Risk Contribution (RC) | 某资产对组合总风险的绝对贡献量 | +| Black-Litterman | Black-Litterman (BL) | 贝叶斯框架,融合市场均衡先验与投资者观点 | +| 先验 | Prior | 贝叶斯框架中引入观测数据之前的信念 | +| 后验 | Posterior | 贝叶斯更新后,融合先验与观测的新信念 | +| 观点矩阵 | View Matrix (P) | 表达投资者观点的矩阵,K观点 × N资产 | +| 观点收益向量 | View Returns Vector (Q) | 各观点的预期收益 | +| 观点不确定性矩阵 | View Uncertainty Matrix (Ω) | 对角矩阵,表达对各观点的置信度 | +| 市场均衡隐含收益 | Market Equilibrium Implied Returns (Π) | CAPM均衡下,对应市值加权组合的隐含预期收益 | +| 风险厌恶系数 | Risk Aversion Coefficient (δ) | 衡量投资者对风险-收益权衡的偏好参数 | +| 绝对观点 | Absolute View | 对某资产预期收益的绝对判断(如"S01 将涨 5%")| +| 相对观点 | Relative View | 对两资产相对表现的判断(如"A 比 B 多赚 3%")| +| 全投资约束 | Full Investment Constraint | Σw_i = 1,所有资金都投入 | +| 多头约束 | Long-Only Constraint | w_i ≥ 0,不允许卖空 | +| 权重上限 | Weight Upper Bound | 单只资产的最大持仓比例限制 | +| 行业权重约束 | Sector Weight Constraint | 限制某一行业的总持仓比例范围 | +| 换手率约束 | Turnover Constraint | 限制新旧权重变化总量,控制交易成本 | +| 换手率 | Turnover | Σ|w_new - w_old|,每次调仓时权重变动的总量 | +| 前视偏差 | Look-Ahead Bias | 回测中错误地使用了未来才能获得的数据 | +| 滚动回测 | Rolling / Walk-Forward Backtest | 用历史窗口估计参数,样本外验证的正确回测方式 | +| SLSQP | Sequential Least Squares Programming | 序列最小二乘规划法,scipy 中常用的约束优化算法 | +| 过拟合 | Overfitting | 模型过度拟合历史数据,样本外表现大幅下降 | +| 角点解 | Corner Solution | 优化结果极度集中在少数资产,非分散化的极端解 | +| 误差传导 | Error Propagation | 输入参数的微小误差被优化器放大后产生极端结果 | +| 风险分散 | Risk Diversification | 通过多资产配置降低总组合风险的过程 | +| 行业暴露 | Sector Exposure | 组合对某一行业的权重,影响风格风险 | + +--- + +*上一篇:[Alpha 因子研究](doc_04_alpha_factor.md)* + +--- + +## 附录:系列文档导航 + +| 篇 | 文件 | 文档 | 核心内容 | +|----|------|------|----------| +| 第 1 篇 | `quant_data_pipeline_demo.py` | `doc_01_data_pipeline.md` | 复权、收益率、缺失值、异常值、涨跌停 | +| 第 2 篇 | `quant_strategy_backtest_demo.py` | `doc_02_strategy_backtest.md` | 技术指标、策略逻辑、向量化回测、绩效指标 | +| 第 3 篇 | `quant_event_driven_backtest_demo.py` | `doc_03_event_driven_backtest.md` | 事件驱动架构、6大组件、成本模型 | +| 第 4 篇 | `quant_alpha_factor_demo.py` | `doc_04_alpha_factor.md` | 因子构建、IC/ICIR、分层回测、因子合成、多空组合 | +| **第 5 篇** | `quant_portfolio_optimization_demo.py` | `doc_05_portfolio_optimization.md` | MVO、有效前沿、MinVar、MaxSharpe、风险平价、BL、约束优化 | diff --git a/portfolio_optimization_demo.png b/portfolio_optimization_demo.png new file mode 100644 index 0000000..b2751b4 Binary files /dev/null and b/portfolio_optimization_demo.png differ diff --git a/quant_portfolio_optimization_demo.py b/quant_portfolio_optimization_demo.py new file mode 100644 index 0000000..997a80e --- /dev/null +++ b/quant_portfolio_optimization_demo.py @@ -0,0 +1,949 @@ +""" +量化交易演示系列 第5篇:组合优化 +Quantitative Trading Demo Series - Part 5: Portfolio Optimization + +涵盖内容 / Topics Covered: + §0 合成股票池与因子模型 (Synthetic Universe & Factor Model) + §1 协方差矩阵估计 (Covariance Matrix Estimation) + §2 马科维兹均值-方差优化 (Markowitz Mean-Variance Optimization) + §3 有效前沿 (Efficient Frontier) + §4 特殊组合:等权/最小方差/最大夏普 (Special Portfolios: EW / MinVar / MaxSharpe) + §5 风险平价 (Risk Parity) + §6 Black-Litterman 模型 (Black-Litterman Model) + §7 带约束的组合优化 (Constrained Portfolio Optimization) + §8 滚动回测:五策略对比 (Rolling Backtest: 5-Strategy Comparison) + §9 可视化 (9-Panel Visualization) + +依赖库 / Dependencies: + numpy, pandas, scipy, matplotlib, sklearn + +作者 / Author: Quant Demo Series +""" + +# ── 无界面绘图模式,必须在 import pyplot 之前设置 ── +# Headless plotting mode — MUST be set before importing pyplot +import matplotlib +matplotlib.use('Agg') + +import warnings +warnings.filterwarnings('ignore') + +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +import matplotlib.gridspec as gridspec +from matplotlib.ticker import FuncFormatter +from scipy.optimize import minimize + +# ── 随机种子(确保结果可复现)/ Random seed for reproducibility ── +np.random.seed(42) + +# ── 中文字体配置 / Chinese font configuration ── +plt.rcParams['font.sans-serif'] = [ + 'WenQuanYi Zen Hei', 'Arial Unicode MS', 'SimHei', 'DejaVu Sans' +] +plt.rcParams['axes.unicode_minus'] = False + +print("=" * 70) +print(" 量化交易 组合优化演示") +print(" Quantitative Trading: Portfolio Optimization Demo") +print("=" * 70) + + +# ══════════════════════════════════════════════════════════════════════ +# §0 合成股票池与行业因子模型 +# Synthetic Universe & Sector Factor Model +# ══════════════════════════════════════════════════════════════════════ +# +# 使用 Barra 风格的因子模型生成收益率: +# Returns are generated using a Barra-style factor model: +# +# r_{i,t} = β_{mkt,i} × r_{mkt,t} ← 市场因子 (Market factor) +# + Σ_k β_{sec,i,k} × f_{k,t} ← 行业因子 (Sector factors) +# + α_i ← 个股 Alpha (Stock-specific alpha) +# + ε_{i,t} ← 特质噪音 (Idiosyncratic noise) + +N_STOCKS = 20 # 股票数量 (Number of stocks) +N_YEARS = 3 # 历史年数 (History in years) +FREQ = 252 # 年化因子 / 每年交易日数 (Annualization factor) +RF = 0.02 # 无风险利率 (Risk-free rate), 年化 + +START_DATE = "2021-01-04" +dates = pd.bdate_range(START_DATE, periods=N_YEARS * FREQ) # 工作日序列 +N_DAYS = len(dates) + +# ── 行业分组 / Sector assignments ── +SECTORS = { + "科技 Tech": list(range(0, 5)), # S00–S04 + "金融 Finance": list(range(5, 9)), # S05–S08 + "消费 Consumer": list(range(9, 13)), # S09–S12 + "工业 Industrial": list(range(13, 17)), # S13–S16 + "医疗 Healthcare": list(range(17, 20)), # S17–S19 +} +SECTOR_NAMES = list(SECTORS.keys()) +SECTOR_SHORT = ["科技", "金融", "消费", "工业", "医疗"] +N_SECTORS = len(SECTORS) +stock_names = [f"S{i:02d}" for i in range(N_STOCKS)] + +# ── 行业公共因子收益率 / Sector factor daily returns ── +sector_ann_ret = np.array([0.16, 0.10, 0.12, 0.08, 0.13]) # 各行业年化期望收益 +sector_ann_vol = np.array([0.25, 0.18, 0.20, 0.15, 0.22]) # 各行业年化波动率 + +sector_factor_rets = np.zeros((N_DAYS, N_SECTORS)) +for k in range(N_SECTORS): + mu_d = sector_ann_ret[k] / FREQ + sig_d = sector_ann_vol[k] / np.sqrt(FREQ) + sector_factor_rets[:, k] = np.random.normal(mu_d, sig_d, N_DAYS) + +# ── 市场公共因子 / Market common factor ── +mkt_daily = np.random.normal(0.10 / FREQ, 0.18 / np.sqrt(FREQ), N_DAYS) + +# ── 各股的因子载荷(Beta 暴露)/ Factor loadings (Beta exposures) ── +betas_mkt = np.random.uniform(0.6, 1.4, N_STOCKS) # 市场 Beta +betas_sec = np.zeros((N_STOCKS, N_SECTORS)) # 行业 Beta + +for k, (sname, sidx) in enumerate(SECTORS.items()): + for i in sidx: + betas_sec[i, k] = np.random.uniform(0.5, 1.0) # 本行业:强载荷 + for k2 in range(N_SECTORS): + if k2 != k: + betas_sec[i, k2] = np.random.uniform(0.0, 0.15) # 其他行业:弱载荷 + +# ── 个股特质波动率 / Idiosyncratic volatility ── +idio_vols = np.random.uniform(0.12, 0.30, N_STOCKS) / np.sqrt(FREQ) +idio_rets = np.random.normal(0, idio_vols, (N_DAYS, N_STOCKS)) + +# ── 个股特质 Alpha / Stock-specific alpha ── +# 科技股注入正 Alpha,让最大夏普组合有明显的倾向性 +idio_alpha = np.zeros(N_STOCKS) +idio_alpha[:5] = np.random.uniform(0.04, 0.09, 5) / FREQ # 科技股:正 Alpha +idio_alpha[5:9] = np.random.uniform(-0.02, 0.02, 4) / FREQ # 金融股:中性 + +# ── 合成日收益率矩阵 / Composite daily return matrix ── +daily_ret_mat = np.zeros((N_DAYS, N_STOCKS)) +for i in range(N_STOCKS): + daily_ret_mat[:, i] = ( + betas_mkt[i] * mkt_daily # 市场贡献 + + betas_sec[i] @ sector_factor_rets.T # 行业贡献(5 行业因子) + + idio_alpha[i] # 个股 Alpha + + idio_rets[:, i] # 特质噪音 + ) + +ret_df = pd.DataFrame(daily_ret_mat, index=dates, columns=stock_names) +prices_df = (1 + ret_df).cumprod() * 100.0 # 从 100 元开始的价格序列 + +# ── 市值权重(Black-Litterman 需要作为先验市场组合)── +# Market-cap weights as Black-Litterman equilibrium prior +mktcap_weights = pd.Series( + np.random.dirichlet(np.ones(N_STOCKS) * 2.0), + index=stock_names +) + +ann_rets = (1 + ret_df).prod() ** (FREQ / N_DAYS) - 1 +print(f"\n[§0] 股票池生成完成 / Universe Generated") +print(f" 股票数量 (Stocks): {N_STOCKS} 只") +print(f" 交易日数 (Days): {N_DAYS} 天 {dates[0].date()} ~ {dates[-1].date()}") +print(f" 行业数量 (Sectors): {N_SECTORS} 个") +print(f" 个股年化收益区间: {ann_rets.min():.1%} ~ {ann_rets.max():.1%}") + + +# ══════════════════════════════════════════════════════════════════════ +# §1 协方差矩阵估计:样本 vs Ledoit-Wolf 收缩 +# Covariance Matrix Estimation: Sample vs Ledoit-Wolf Shrinkage +# ══════════════════════════════════════════════════════════════════════ +# +# 协方差矩阵 (Covariance Matrix) Σ 是组合优化的核心输入。 +# 样本协方差矩阵 (Sample Covariance Matrix) 存在两个问题: +# 1. 估计误差大(小样本情形,T/N 较小时)/ Large estimation error when T/N is small +# 2. 条件数 (Condition Number) 高,矩阵"病态",优化对估计误差非常敏感 +# +# Ledoit-Wolf 收缩 (Ledoit-Wolf Shrinkage): +# Σ_shrunk = (1-α) × Σ_sample + α × F +# 其中 F 是结构化目标矩阵(如对角矩阵),α 是最优收缩系数(数据驱动自动选择) +# F is a structured target matrix; α is the optimal shrinkage coefficient. + +# 样本协方差(年化) / Sample covariance (annualized) +cov_sample = ret_df.cov() * FREQ +mu_sample = ret_df.mean() * FREQ # 年化预期收益向量 (Annualized expected return vector) + +# Ledoit-Wolf 收缩估计 / Ledoit-Wolf shrinkage estimation +try: + from sklearn.covariance import LedoitWolf + lw = LedoitWolf() + lw.fit(ret_df.values) + cov_shrink = pd.DataFrame( + lw.covariance_ * FREQ, + index=stock_names, columns=stock_names + ) + shrink_alpha = lw.shrinkage_ # 最优收缩系数 α + USE_SHRINK = True +except ImportError: + cov_shrink = cov_sample.copy() + shrink_alpha = 0.0 + USE_SHRINK = False + +print(f"\n[§1] 协方差估计 / Covariance Estimation") +print(f" 样本协方差条件数 (Sample Cov Cond#): {np.linalg.cond(cov_sample.values):.1f}") +if USE_SHRINK: + print(f" Ledoit-Wolf 收缩系数 (Shrinkage α): {shrink_alpha:.4f}") + print(f" 收缩后条件数 (Shrunk Cov Cond#): {np.linalg.cond(cov_shrink.values):.1f}") + print(f" → 条件数降低意味着优化更稳健 / Lower cond# = more robust optimization") + +# 正式使用的协方差矩阵(收缩版更稳健)/ Use shrunk covariance for optimization +cov_opt = cov_shrink if USE_SHRINK else cov_sample +mu_opt = mu_sample.copy() + + +# ══════════════════════════════════════════════════════════════════════ +# §2 核心优化工具函数 +# Core Optimization Utility Functions +# ══════════════════════════════════════════════════════════════════════ + +def portfolio_stats(weights, mu, cov, rf=RF): + """ + 计算组合的三个核心绩效指标(年化) + Compute 3 key portfolio statistics (annualized). + + 参数 / Args: + weights : np.ndarray, 权重向量 (weight vector), shape (N,) + mu : pd.Series, 年化预期收益向量 (annualized expected returns) + cov : pd.DataFrame, 年化协方差矩阵 (annualized covariance matrix) + rf : float, 无风险利率 (risk-free rate) + + 返回 / Returns: + ret : 组合年化收益率 (portfolio annualized return) + vol : 组合年化波动率 (portfolio annualized volatility) + sharpe : 夏普比率 (Sharpe ratio) = (ret - rf) / vol + """ + w = np.asarray(weights) + ret = float(w @ mu) # w'μ + var = float(w @ cov.values @ w) # w'Σw(组合方差) + vol = np.sqrt(max(var, 1e-12)) # 组合波动率(年化) + sharpe = (ret - rf) / vol + return ret, vol, sharpe + + +def _base_optimize(objective, n, bounds, extra_constraints=None): + """ + 通用 SLSQP 优化框架(内部使用) + Generic SLSQP optimization scaffold (internal use). + + SLSQP = Sequential Least Squares Programming / 序列二次规划法 + """ + w0 = np.ones(n) / n # 初始猜测:等权 (Initial guess: equal weight) + constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1.0}] # 全投资约束 + if extra_constraints: + constraints.extend(extra_constraints) + return minimize( + objective, w0, + method='SLSQP', + bounds=bounds, + constraints=constraints, + options={'ftol': 1e-12, 'maxiter': 1500} + ) + + +def min_variance(mu, cov, allow_short=False): + """ + 最小方差组合 (Minimum Variance Portfolio, MinVar) + + 这是组合优化中最稳健的组合,因为它完全不依赖对预期收益的估计。 + Most robust portfolio: it does NOT require any return estimates. + + 问题形式 / Optimization problem: + minimize w'Σw + subject to Σw_i = 1 (全投资约束 / full investment) + w_i ≥ 0 (多头约束 / long-only, when allow_short=False) + """ + n = len(mu) + bnd = (-0.3, 1.0) if allow_short else (0.0, 1.0) + bounds = [bnd] * n + result = _base_optimize(lambda w: w @ cov.values @ w, n, bounds) + return pd.Series(result.x, index=cov.index) + + +def max_sharpe(mu, cov, rf=RF, allow_short=False): + """ + 最大夏普比率组合 / 切线组合 (Maximum Sharpe / Tangency Portfolio) + + 在均值-方差空间中,从无风险资产出发到有效前沿的切线点。 + Tangency point from the risk-free asset to the efficient frontier (Capital Market Line). + + 技巧 / Trick: + max Sharpe ≡ min -Sharpe (把最大化转为最小化 / flip to minimization) + """ + n = len(mu) + bnd = (-0.3, 1.0) if allow_short else (0.0, 1.0) + bounds = [bnd] * n + + def neg_sharpe(w): + r, v, sr = portfolio_stats(w, mu, cov, rf) + return -sr + + result = _base_optimize(neg_sharpe, n, bounds) + return pd.Series(result.x, index=cov.index) + + +def efficient_portfolio(target_return, mu, cov): + """ + 目标收益下的最小方差组合(有效前沿上的一点) + Minimum-variance portfolio for a given target return (a point on efficient frontier). + + 问题形式 / Optimization problem: + minimize w'Σw + subject to w'μ = target_return (收益约束 / return constraint) + Σw_i = 1 + w_i ≥ 0 + """ + n = len(mu) + bnd = (0.0, 1.0) + result = _base_optimize( + lambda w: w @ cov.values @ w, n, + bounds=[bnd] * n, + extra_constraints=[ + {'type': 'eq', 'fun': lambda w: float(w @ mu.values) - target_return} + ] + ) + return pd.Series(result.x, index=cov.index) if result.success else None + + +# ══════════════════════════════════════════════════════════════════════ +# §3 有效前沿 +# Efficient Frontier +# ══════════════════════════════════════════════════════════════════════ +# +# 有效前沿 (Efficient Frontier) 是所有"有效组合"的集合: +# → 在相同风险(波动率)下,收益最高 +# → 在相同收益下,风险最低 +# +# 有效前沿由两点确定: +# 左端点:最小方差组合 (Global Minimum Variance Portfolio, GMV) +# 右端点:预期收益最高的单只股票(极端集中) + +print(f"\n[§3] 计算有效前沿 / Computing Efficient Frontier...") + +# ── 计算关键节点组合 / Compute key special portfolios ── +w_minvar = min_variance(mu_opt, cov_opt) +w_maxsharpe = max_sharpe(mu_opt, cov_opt, rf=RF) +w_eq = pd.Series(np.ones(N_STOCKS) / N_STOCKS, index=stock_names) + +ret_min, vol_min, sr_min = portfolio_stats(w_minvar, mu_opt, cov_opt) +ret_msr, vol_msr, sr_msr = portfolio_stats(w_maxsharpe, mu_opt, cov_opt) +ret_eq, vol_eq, sr_eq = portfolio_stats(w_eq, mu_opt, cov_opt) + +# ── 扫描有效前沿 / Trace the efficient frontier ── +n_pts = 60 +ret_lo = ret_min * 0.99 +ret_hi = mu_opt.max() * 0.88 +frontier_rets = [] +frontier_vols = [] + +for tgt in np.linspace(ret_lo, ret_hi, n_pts): + w_eff = efficient_portfolio(tgt, mu_opt, cov_opt) + if w_eff is not None: + r, v, _ = portfolio_stats(w_eff, mu_opt, cov_opt) + frontier_rets.append(r) + frontier_vols.append(v) + +# ── Monte Carlo 随机组合云(背景参照)/ Random portfolio cloud (background reference) ── +N_RANDOM = 3000 +rand_rets = [] +rand_vols = [] +rand_sharpes = [] + +for _ in range(N_RANDOM): + w_r = np.random.dirichlet(np.ones(N_STOCKS)) # 满足 w≥0, Σw=1 的随机权重 + r, v, sr = portfolio_stats(w_r, mu_opt, cov_opt) + rand_rets.append(r) + rand_vols.append(v) + rand_sharpes.append(sr) + +print(f" 有效前沿点数 (Frontier points): {len(frontier_rets)}") +print(f" 最小方差组合 (MinVar): 收益={ret_min:.1%}, 波动={vol_min:.1%}, 夏普={sr_min:.2f}") +print(f" 最大夏普组合 (MaxSharpe): 收益={ret_msr:.1%}, 波动={vol_msr:.1%}, 夏普={sr_msr:.2f}") + + +# ══════════════════════════════════════════════════════════════════════ +# §4 风险平价 +# Risk Parity +# ══════════════════════════════════════════════════════════════════════ +# +# 核心思想:每只资产对组合总风险的贡献相等 +# Core idea: each asset contributes equally to total portfolio risk. +# +# 风险贡献 (Risk Contribution, RC) 的定义: +# +# RC_i = w_i × ∂σ_p/∂w_i = w_i × (Σw)_i / σ_p +# ↑ 边际风险贡献 (Marginal Risk Contribution, MRC) +# +# 等风险贡献条件 (Equal Risk Contribution): +# RC_i = σ_p / N ←→ w_i × (Σw)_i = w_j × (Σw)_j for all i, j +# +# 优化形式(最小化各股风险贡献比例与目标 1/N 的偏差平方和): +# minimize Σ_i (RC_i/σ_p − 1/N)² + +print(f"\n[§4] 计算风险平价组合 / Risk Parity Portfolio...") + + +def risk_contributions(weights, cov): + """ + 计算每只资产的绝对风险贡献和风险贡献占比 + Compute absolute risk contribution and risk contribution fraction for each asset. + + Returns: + rc : 绝对风险贡献向量 (Absolute Risk Contribution vector), shape (N,) + sigma_p : 组合总波动率 (Total portfolio volatility) + """ + w = np.asarray(weights) + sigma_p = np.sqrt(float(w @ cov.values @ w)) # 组合波动率 (Portfolio vol) + mrc = cov.values @ w / sigma_p # 边际风险贡献 (MRC = ∂σ/∂w) + rc = w * mrc # 绝对风险贡献 (RC = w × MRC) + return rc, sigma_p + + +def risk_parity(cov): + """ + 等风险贡献组合 (Equal Risk Contribution Portfolio) + + 约束:w_i ≥ 0 (严格多头,做空会有负的风险贡献,没有意义) + Strictly long-only: short positions would give negative RC (meaningless). + """ + N = cov.shape[0] + tgt = 1.0 / N # 目标风险贡献占比 = 1/N + + def objective(w): + rc, sigma_p = risk_contributions(w, cov) + rc_pct = rc / sigma_p # 风险贡献占比 (RC fraction) + return np.sum((rc_pct - tgt) ** 2) + + result = _base_optimize( + objective, N, + bounds=[(1e-6, 1.0)] * N # 严格正数(不允许做空) + ) + return pd.Series(result.x, index=cov.index) + + +w_rp = risk_parity(cov_opt) +rc_rp, sigma_rp = risk_contributions(w_rp, cov_opt) +ret_rp, vol_rp, sr_rp = portfolio_stats(w_rp, mu_opt, cov_opt) + +rc_pct_rp = rc_rp / sigma_rp +max_rc_dev = np.max(np.abs(rc_pct_rp - 1.0 / N_STOCKS)) # 与目标的最大偏差 + +print(f" 风险平价组合: 收益={ret_rp:.1%}, 波动={vol_rp:.1%}, 夏普={sr_rp:.2f}") +print(f" 风险贡献最大偏差 (Max RC deviation from 1/N): {max_rc_dev:.5f}") +print(f" → 接近 0 表示各股风险贡献几乎相等 / ≈0 means near-perfect equal RC") + + +# ══════════════════════════════════════════════════════════════════════ +# §5 Black-Litterman 模型 +# Black-Litterman Model +# ══════════════════════════════════════════════════════════════════════ +# +# 马科维兹优化的两大痛点 / Two pain points of Markowitz optimization: +# ① 预期收益 μ 难以估计,哪怕小幅误差也会导致权重大幅波动 +# Expected returns are noisy; small errors cause wild weight swings. +# ② 结果过度集中("角点解"),实际不可用 +# Results are over-concentrated "corner solutions". +# +# Black-Litterman (1990) 的贝叶斯框架 / Bayesian framework: +# ① 先验 (Prior): 从市场均衡推导隐含收益(CAPM 反向优化) +# Market-implied returns from reverse-optimizing CAPM. +# ② 观点 (Views): 投资者对某些股票/组合的主观预期 +# Investor's subjective views on specific stocks/portfolios. +# ③ 后验 (Posterior): 贝叶斯更新,先验与观点的加权混合 +# Bayesian posterior blending prior and views. +# +# 后验均值公式 / Posterior mean formula (He & Litterman, 1999): +# +# μ_BL = [(τΣ)⁻¹ + P'Ω⁻¹P]⁻¹ × [(τΣ)⁻¹Π + P'Ω⁻¹Q] +# ───────────────────── ────────────────────── +# 精度矩阵之和 加权均值向量 +# Sum of precision Weighted mean vector +# +# Π = δ × Σ × w_mkt ← 市场均衡隐含超额收益 (Market equilibrium implied returns) +# δ = 风险厌恶系数 (Risk aversion coefficient) +# τ = 先验收缩参数 (Prior scaling factor), 通常 0.025~0.10 +# P = 观点矩阵 K×N (View matrix, K views, N assets) +# Q = 观点收益向量 K×1 (View expected excess returns) +# Ω = 观点不确定性矩阵 K×K (View uncertainty / confidence matrix) + +print(f"\n[§5] Black-Litterman 模型 / Black-Litterman Model...") + +DELTA = 2.5 # 风险厌恶系数 (Risk aversion), 典型值 2.0~3.0 +TAU = 0.05 # 先验收缩参数 (Prior scaling), 典型值 0.025~0.10 + +# ─── Step 1: 计算市场均衡隐含超额收益 ─── +# Market equilibrium implied excess returns (Reverse Optimization of CAPM) +# Π = δ × Σ × w_mkt +w_mkt = mktcap_weights.values +Pi = DELTA * cov_opt.values @ w_mkt +Pi_s = pd.Series(Pi, index=stock_names) + +# ─── Step 2: 设定投资者观点 (Investor Views) ─── +# +# 观点1 (View 1): "S01 的年化超额收益率将达到 +5%"(单资产绝对观点) +# "S01 will earn +5% excess return per year" (absolute view) +# +# 观点2 (View 2): "S00 将比 S04 多赚 3%"(两资产相对观点) +# "S00 will outperform S04 by 3%" (relative view) + +K = 2 # 观点数量 (Number of views) + +# P 矩阵:K × N;P[k, i] = 资产 i 在观点 k 中的权重(正=多头,负=空头) +P = np.zeros((K, N_STOCKS)) +P[0, 1] = 1.0 # 观点1:仅 S01 +P[1, 0] = 1.0 # 观点2:S00 多头 +P[1, 4] = -1.0 # S04 空头(相对观点) + +# Q 向量:观点的预期超额收益 (View expected excess returns) +Q = np.array([0.05, 0.03]) + +# Ω 矩阵:观点的不确定性(He & Litterman 建议的设置方式) +# Ω = τ × diag(P Σ P'),对角元素越大表示该观点越不确定 +Omega = np.diag(TAU * np.diag(P @ cov_opt.values @ P.T)) +Omega_inv = np.linalg.inv(Omega) + +# ─── Step 3: 贝叶斯更新,计算后验预期超额收益 ─── +# Bayesian posterior expected excess returns +tau_cov_inv = np.linalg.inv(TAU * cov_opt.values) +A = tau_cov_inv + P.T @ Omega_inv @ P # 精度矩阵之和 +b = tau_cov_inv @ Pi + P.T @ Omega_inv @ Q + +mu_bl = np.linalg.solve(A, b) + RF # 后验 = 超额 + 无风险利率 +mu_bl_s = pd.Series(mu_bl, index=stock_names) + +# ─── Step 4: 用后验收益做最大夏普优化 ─── +w_bl = max_sharpe(mu_bl_s, cov_opt, rf=RF) +ret_bl, vol_bl, sr_bl = portfolio_stats(w_bl, mu_bl_s, cov_opt) + +print(f" 市场均衡隐含收益范围: {Pi_s.min():.1%} ~ {Pi_s.max():.1%}") +print(f" BL 后验收益范围: {mu_bl_s.min():.1%} ~ {mu_bl_s.max():.1%}") +print(f" BL 组合: 收益={ret_bl:.1%}, 波动={vol_bl:.1%}, 夏普={sr_bl:.2f}") +print(f" 观点受益股(S01, S00)权重 BL: {w_bl['S01']:.1%}, {w_bl['S00']:.1%}") +print(f" 观点受益股(S01, S00)权重 等权: {w_eq['S01']:.1%}, {w_eq['S00']:.1%}") + + +# ══════════════════════════════════════════════════════════════════════ +# §6 带约束的组合优化 +# Constrained Portfolio Optimization +# ══════════════════════════════════════════════════════════════════════ +# +# 实际组合管理中必须满足多重约束 / Real portfolios need multiple constraints: +# ① 单只股票权重上限(集中度管理)/ Max individual weight (concentration limit) +# ② 行业权重区间(风格/行业暴露管理)/ Sector weight bounds (style control) +# ③ 换手率约束(控制交易成本)/ Turnover constraint (transaction cost control) +# +# 换手率 (Turnover) 的定义: +# Turnover = Σ_i |w_new,i - w_old,i| / 2 (双边换手率,除以 2 避免重复计算) +# 或简化为 L1 距离:Σ_i |w_new,i - w_old,i| + +print(f"\n[§6] 带约束的最大夏普组合 / Constrained Max-Sharpe Portfolio...") + +MAX_STOCK_WT = 0.15 # 单只股票最大 15% / Max 15% per stock +MAX_SECTOR_WT = 0.35 # 单行业最大 35% / Max 35% per sector +MIN_SECTOR_WT = 0.10 # 单行业最小 10% / Min 10% per sector +MAX_TURNOVER = 0.50 # 最大换手率 50% / Max 50% one-way turnover + +w_prev = np.ones(N_STOCKS) / N_STOCKS # 上期持仓(假设为等权) + +n = N_STOCKS +w0 = np.ones(n) / n + +extra_cons = [] + +# 约束1: 行业权重上下限 +for sname, sidx in SECTORS.items(): + def make_lb(idx): return lambda w: np.sum(w[idx]) - MIN_SECTOR_WT + def make_ub(idx): return lambda w: MAX_SECTOR_WT - np.sum(w[idx]) + extra_cons.append({'type': 'ineq', 'fun': make_lb(sidx)}) # ≥ 10% + extra_cons.append({'type': 'ineq', 'fun': make_ub(sidx)}) # ≤ 35% + +# 约束2: 换手率约束(L1 距离 ≤ 50%) +extra_cons.append({'type': 'ineq', + 'fun': lambda w: MAX_TURNOVER - np.sum(np.abs(w - w_prev))}) + +result_c = _base_optimize( + lambda w: -portfolio_stats(w, mu_opt, cov_opt, RF)[2], # 最小化负夏普 + n, + bounds=[(0.0, MAX_STOCK_WT)] * n, + extra_constraints=extra_cons +) +w_constrained = pd.Series(result_c.x, index=stock_names) +ret_c, vol_c, sr_c = portfolio_stats(w_constrained, mu_opt, cov_opt) + +print(f" 带约束最大夏普: 收益={ret_c:.1%}, 波动={vol_c:.1%}, 夏普={sr_c:.2f}") +print(f" 最大单股权重: {w_constrained.max():.1%} (约束 ≤{MAX_STOCK_WT:.0%})") +for sname, sidx in SECTORS.items(): + sw = w_constrained.iloc[sidx].sum() + ok = "✓" if MIN_SECTOR_WT <= sw <= MAX_SECTOR_WT else "✗" + print(f" {ok} {sname}: {sw:.1%}") + + +# ══════════════════════════════════════════════════════════════════════ +# §7 滚动回测:五策略对比 +# Rolling Backtest: 5-Strategy Comparison +# ══════════════════════════════════════════════════════════════════════ +# +# 流程 / Workflow: +# 每个调仓日 T(每月一次): +# ① 用过去 LOOKBACK 天估计 μ 和 Σ +# ② 用当前估计值重新计算各策略权重 +# ③ 用新权重持有到下次调仓日,每日记录组合收益 +# +# 五种策略 / 5 Strategies: +# EW : 等权(不需要优化) +# MinVar : 最小方差(只需 Σ,不需要 μ) +# MaxSharpe : 最大夏普(需要 μ 和 Σ) +# RiskParity : 风险平价(只需 Σ) +# BL : Black-Litterman(贝叶斯混合) + +print(f"\n[§7] 滚动回测 / Rolling Backtest...") + +LOOKBACK = 126 # 协方差回看期(约 6 个月)/ 6-month estimation window +REBAL_FREQ = 21 # 调仓频率(约 1 个月)/ Monthly rebalancing + +STRATEGY_NAMES = [ + "等权 EW", + "最小方差 MinVar", + "最大夏普 MaxSharpe", + "风险平价 RP", + "Black-Litterman BL", +] + +# 初始化:回看期内用 0 填充(尚未开始实际持仓) +daily_pnl = {name: [0.0] * LOOKBACK for name in STRATEGY_NAMES} + +# 当前持仓权重(初始为等权) +cur_w = {name: np.ones(N_STOCKS) / N_STOCKS for name in STRATEGY_NAMES} + +rebal_idx = list(range(LOOKBACK, N_DAYS, REBAL_FREQ)) + +for t_pos, t in enumerate(rebal_idx): + # 取过去 LOOKBACK 天的历史收益率 + hist = ret_df.iloc[t - LOOKBACK: t] + + # 估计滚动协方差矩阵(加微小正则化保证正定性) + # Rolling covariance (add tiny ridge term to ensure positive definiteness) + cov_r_arr = hist.cov().values * FREQ + 1e-7 * np.eye(N_STOCKS) + mu_r = hist.mean() * FREQ + cov_r = pd.DataFrame(cov_r_arr, index=stock_names, columns=stock_names) + + try: + new_w = {} + new_w["等权 EW"] = np.ones(N_STOCKS) / N_STOCKS + new_w["最小方差 MinVar"] = min_variance(mu_r, cov_r).values + new_w["最大夏普 MaxSharpe"] = max_sharpe(mu_r, cov_r, rf=RF).values + new_w["风险平价 RP"] = risk_parity(cov_r).values + + # Black-Litterman 滚动版本:使用滚动先验 + 固定观点 + Pi_r = DELTA * cov_r_arr @ mktcap_weights.values + try: + tci = np.linalg.inv(TAU * cov_r_arr) + A_r = tci + P.T @ Omega_inv @ P + b_r = tci @ Pi_r + P.T @ Omega_inv @ Q + mu_bl_r = np.linalg.solve(A_r, b_r) + RF + except np.linalg.LinAlgError: + mu_bl_r = mu_r.values + new_w["Black-Litterman BL"] = max_sharpe( + pd.Series(mu_bl_r, index=stock_names), cov_r, rf=RF + ).values + + cur_w = new_w + + except Exception: + pass # 优化失败时保持上期权重 / Keep previous weights if optimization fails + + # 计算持有期内每日组合收益 + next_t = rebal_idx[t_pos + 1] if t_pos + 1 < len(rebal_idx) else N_DAYS + for name in STRATEGY_NAMES: + w = cur_w[name] + for d in range(t, next_t): + daily_pnl[name].append(float(w @ ret_df.iloc[d].values)) + +# 截齐到 N_DAYS(防止因最后一段超出) +for name in STRATEGY_NAMES: + daily_pnl[name] = daily_pnl[name][:N_DAYS] + +# ── 计算净值曲线 (NAV Curves) ── +nav_df = pd.DataFrame( + {name: (1 + pd.Series(daily_pnl[name], index=dates)).cumprod() + for name in STRATEGY_NAMES} +) + +# ── 计算绩效指标 (Performance Metrics) ── +perf_rows = [] +for name in STRATEGY_NAMES: + r_s = pd.Series(daily_pnl[name][LOOKBACK:], index=dates[LOOKBACK:]) + ann_ret = (1 + r_s).prod() ** (FREQ / len(r_s)) - 1 # 年化收益率 (Ann. return) + ann_vol = r_s.std() * np.sqrt(FREQ) # 年化波动率 (Ann. vol) + sharpe = (ann_ret - RF) / ann_vol if ann_vol > 0 else 0 + nav_s = (1 + r_s).cumprod() + max_dd = (nav_s / nav_s.cummax() - 1).min() # 最大回撤 (Max drawdown) + calmar = ann_ret / (-max_dd) if max_dd < 0 else 99.0 # Calmar 比率 + perf_rows.append({ + "策略": name, "年化收益": ann_ret, "年化波动": ann_vol, + "夏普比率": sharpe, "最大回撤": max_dd, "Calmar": calmar + }) + +perf_df = pd.DataFrame(perf_rows).set_index("策略") + +print(f"\n 策略对比 / Strategy Comparison:") +print(f" {'策略':<28} {'年化收益':>8} {'年化波动':>8} {'夏普':>7} {'最大回撤':>9} {'Calmar':>8}") +print(f" {'─' * 65}") +for name, row in perf_df.iterrows(): + print(f" {name:<28} {row['年化收益']:>8.1%} {row['年化波动']:>8.1%} " + f"{row['夏普比率']:>7.3f} {row['最大回撤']:>9.1%} {min(row['Calmar'], 99.0):>8.2f}") + + +# ══════════════════════════════════════════════════════════════════════ +# §8 可视化:9 图综合面板 +# 9-Panel Visualization Dashboard +# ══════════════════════════════════════════════════════════════════════ + +print(f"\n[§8] 生成可视化图表 / Generating visualization...") + +# 颜色配置 / Color palette +COLORS = { + "等权 EW": "#888888", + "最小方差 MinVar": "#2196F3", + "最大夏普 MaxSharpe": "#FF5722", + "风险平价 RP": "#4CAF50", + "Black-Litterman BL": "#9C27B0", +} +PCT_FMT = FuncFormatter(lambda x, _: f"{x:.0%}") +PCT1_FMT = FuncFormatter(lambda x, _: f"{x:.1%}") +DARK_BG = '#1A1D27' +LT = '#E0E0E0' # 浅色文字 (Light text) +GRID_C = '#2A2D3A' # 网格颜色 (Grid color) + +fig = plt.figure(figsize=(22, 22), facecolor='#0F1117') +fig.suptitle( + "量化组合优化演示 / Portfolio Optimization Demo", + fontsize=18, color='white', y=0.98, fontweight='bold' +) +gs = gridspec.GridSpec(3, 3, figure=fig, hspace=0.42, wspace=0.32) + +ax1 = fig.add_subplot(gs[0, 0]) # 有效前沿 Efficient Frontier +ax2 = fig.add_subplot(gs[0, 1]) # 组合权重堆叠 Portfolio Weights +ax3 = fig.add_subplot(gs[0, 2]) # 行业权重对比 Sector Weights +ax4 = fig.add_subplot(gs[1, 0]) # 净值曲线 NAV Curves +ax5 = fig.add_subplot(gs[1, 1]) # 风险贡献 Risk Contribution (RP) +ax6 = fig.add_subplot(gs[1, 2]) # 滚动夏普 Rolling Sharpe +ax7 = fig.add_subplot(gs[2, 0]) # 月度收益箱形图 Monthly Return Box +ax8 = fig.add_subplot(gs[2, 1]) # 相关系数热力图 Correlation Heatmap +ax9 = fig.add_subplot(gs[2, 2]) # 绩效汇总表 Performance Table + +for ax in [ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8, ax9]: + ax.set_facecolor(DARK_BG) + ax.tick_params(colors=LT, labelsize=8) + for sp in ax.spines.values(): + sp.set_edgecolor(GRID_C) + +# ── 图1:有效前沿 + 随机组合云 + 关键节点 ── +# Efficient Frontier + Monte Carlo cloud + key portfolios +ax1.set_title("有效前沿 Efficient Frontier", color=LT, fontsize=10, pad=6) +ax1.scatter(rand_vols, rand_rets, + c=rand_sharpes, cmap='RdYlGn', vmin=0, vmax=2.5, + alpha=0.25, s=6, zorder=1) +ax1.plot(frontier_vols, frontier_rets, '-', color='#FFD700', lw=2.2, zorder=4, + label='有效前沿') +# 资本市场线 (Capital Market Line, CML) — 从无风险利率到最大夏普组合并延伸 +cml_x = np.linspace(0, vol_msr * 1.4, 50) +cml_y = RF + sr_msr * cml_x +ax1.plot(cml_x, cml_y, '--', color='#FFEB3B', lw=1.0, alpha=0.7, + zorder=3, label='资本市场线 CML') +# 关键组合点 +specials = [ + ("最小方差\nMinVar", vol_min, ret_min, COLORS["最小方差 MinVar"]), + ("最大夏普\nMaxSharpe",vol_msr, ret_msr, COLORS["最大夏普 MaxSharpe"]), + ("等权\nEW", vol_eq, ret_eq, COLORS["等权 EW"]), + ("风险平价\nRP", vol_rp, ret_rp, COLORS["风险平价 RP"]), + ("BL", vol_bl, ret_bl, COLORS["Black-Litterman BL"]), +] +for label, v, r, c in specials: + ax1.scatter(v, r, s=100, color=c, zorder=6, edgecolors='white', lw=0.8) + ax1.annotate(label, (v, r), color=LT, fontsize=6, + xytext=(5, 3), textcoords='offset points') +# 无风险利率点 (Risk-free rate point) +ax1.scatter(0, RF, s=80, color='yellow', zorder=6, marker='*') +ax1.annotate(f"无风险\nRf={RF:.0%}", (0, RF), color='yellow', fontsize=6, + xytext=(4, 2), textcoords='offset points') +ax1.xaxis.set_major_formatter(PCT_FMT) +ax1.yaxis.set_major_formatter(PCT_FMT) +ax1.set_xlabel("年化波动率 Volatility", color=LT, fontsize=8) +ax1.set_ylabel("年化收益率 Return", color=LT, fontsize=8) +ax1.legend(fontsize=7, labelcolor=LT, facecolor=DARK_BG, edgecolor=GRID_C) +ax1.grid(color=GRID_C, alpha=0.5) + +# ── 图2:各策略权重堆叠柱状图 ── +# Stacked bar chart of portfolio weights +ax2.set_title("组合权重 Portfolio Weights", color=LT, fontsize=10, pad=6) +weight_order = ["EW", "MinVar", "MaxSharpe", "RP", "BL"] +weight_arrays = [w_eq.values, w_minvar.values, w_maxsharpe.values, w_rp.values, w_bl.values] +bar_colors = plt.cm.tab20(np.linspace(0, 1, N_STOCKS)) +bottoms = np.zeros(len(weight_order)) +x_pos = np.arange(len(weight_order)) +for i in range(N_STOCKS): + heights = [wa[i] for wa in weight_arrays] + ax2.bar(x_pos, heights, bottom=bottoms, color=bar_colors[i], width=0.72) + bottoms += heights +ax2.set_xticks(x_pos) +ax2.set_xticklabels(weight_order, color=LT, fontsize=9) +ax2.yaxis.set_major_formatter(PCT_FMT) +ax2.set_ylabel("权重 Weight", color=LT, fontsize=8) +ax2.grid(axis='y', color=GRID_C, alpha=0.5) + +# ── 图3:行业权重对比(分组柱状图)── +# Grouped bar chart: sector weights per strategy +ax3.set_title("行业权重对比 Sector Weights", color=LT, fontsize=10, pad=6) +x_sec = np.arange(N_SECTORS) +bw = 0.15 +strat_colors_list = list(COLORS.values()) +for k, (wname, warr) in enumerate(zip(weight_order, weight_arrays)): + sec_wts = [np.sum(warr[SECTORS[s]]) for s in SECTOR_NAMES] + off = (k - len(weight_order) / 2 + 0.5) * bw + ax3.bar(x_sec + off, sec_wts, width=bw, + color=strat_colors_list[k], label=wname, alpha=0.85) +ax3.set_xticks(x_sec) +ax3.set_xticklabels(SECTOR_SHORT, color=LT, fontsize=8) +ax3.yaxis.set_major_formatter(PCT_FMT) +ax3.axhline(MAX_SECTOR_WT, ls='--', color='#FF7043', lw=0.8, alpha=0.6, + label=f'上限 {MAX_SECTOR_WT:.0%}') +ax3.legend(fontsize=6, labelcolor=LT, facecolor=DARK_BG, edgecolor=GRID_C, + ncol=2, loc='upper right') +ax3.grid(axis='y', color=GRID_C, alpha=0.5) + +# ── 图4:净值曲线 ── +ax4.set_title("净值曲线 NAV Curves", color=LT, fontsize=10, pad=6) +for name in STRATEGY_NAMES: + is_key = "MaxSharpe" in name or "BL" in name + ax4.plot(nav_df.index, nav_df[name], + color=COLORS[name], lw=2.0 if is_key else 1.2, + alpha=1.0 if is_key else 0.7, + label=name.split(" ")[0]) +ax4.set_ylabel("净值 NAV", color=LT, fontsize=8) +ax4.legend(fontsize=7, labelcolor=LT, facecolor=DARK_BG, + edgecolor=GRID_C, ncol=2) +ax4.grid(color=GRID_C, alpha=0.5) +ax4.xaxis.set_tick_params(rotation=20) + +# ── 图5:风险平价组合的风险贡献分解 ── +# Risk contribution breakdown for Risk Parity portfolio +ax5.set_title("风险贡献分布 Risk Contribution (RP)", color=LT, fontsize=10, pad=6) +rc_sorted = pd.Series(rc_pct_rp * 100, index=stock_names).sort_values(ascending=False) +bar_c = ['#4CAF50' if abs(v / 100 - 1.0 / N_STOCKS) < 5e-3 + else '#FF7043' for v in rc_sorted] +ax5.bar(range(N_STOCKS), rc_sorted.values, color=bar_c, alpha=0.85) +ax5.axhline(100.0 / N_STOCKS, color='white', lw=1.2, ls='--', + label=f'目标 1/N={100/N_STOCKS:.1f}%') +ax5.set_xticks(range(N_STOCKS)) +ax5.set_xticklabels(rc_sorted.index, rotation=45, fontsize=6, color=LT) +ax5.set_ylabel("风险贡献 RC%", color=LT, fontsize=8) +ax5.legend(fontsize=7, labelcolor=LT, facecolor=DARK_BG, edgecolor=GRID_C) +ax5.grid(axis='y', color=GRID_C, alpha=0.5) + +# ── 图6:滚动夏普比率(63 日窗口)── +# Rolling Sharpe ratio (63-day window) +ax6.set_title("滚动夏普比率 Rolling Sharpe (63D)", color=LT, fontsize=10, pad=6) +ROLL_WIN = 63 +for name in STRATEGY_NAMES: + r_s = pd.Series(daily_pnl[name], index=dates) + roll_sr = r_s.rolling(ROLL_WIN).apply( + lambda x: (x.mean() * FREQ - RF) / (x.std() * np.sqrt(FREQ) + 1e-10) + ) + ax6.plot(dates, roll_sr, color=COLORS[name], lw=1.0, + alpha=0.85, label=name.split(" ")[0]) +ax6.axhline(0, color='white', lw=0.7, ls='--') +ax6.set_ylabel("夏普比率 Sharpe", color=LT, fontsize=8) +ax6.legend(fontsize=7, labelcolor=LT, facecolor=DARK_BG, + edgecolor=GRID_C, ncol=2) +ax6.grid(color=GRID_C, alpha=0.5) +ax6.xaxis.set_tick_params(rotation=20) + +# ── 图7:月度收益分布箱形图 ── +# Monthly return distribution boxplot +ax7.set_title("月度收益分布 Monthly Return Distribution", color=LT, fontsize=10, pad=6) +monthly_rets = {} +for name in STRATEGY_NAMES: + r_s = pd.Series(daily_pnl[name], index=dates) + monthly_rets[name.split(" ")[0]] = r_s.resample('M').apply( + lambda x: (1 + x).prod() - 1 + ).values + +bp = ax7.boxplot( + list(monthly_rets.values()), + patch_artist=True, + labels=list(monthly_rets.keys()), + medianprops=dict(color='white', lw=1.5), + whiskerprops=dict(color=LT), + capprops=dict(color=LT), + flierprops=dict(marker='.', color='#888888', ms=3) +) +for patch, c in zip(bp['boxes'], list(COLORS.values())): + patch.set_facecolor(c) + patch.set_alpha(0.72) +ax7.yaxis.set_major_formatter(PCT1_FMT) +ax7.axhline(0, color='white', lw=0.6, ls='--') +ax7.set_xticklabels(list(monthly_rets.keys()), color=LT, fontsize=8, rotation=15) +ax7.grid(axis='y', color=GRID_C, alpha=0.5) + +# ── 图8:收益相关系数热力图(含行业分割线)── +# Return correlation heatmap with sector dividers +ax8.set_title("收益相关系数 Return Correlation Matrix", color=LT, fontsize=10, pad=6) +corr = ret_df.corr().values +im = ax8.imshow(corr, cmap='RdYlGn', vmin=-0.2, vmax=1.0, aspect='auto') +ax8.set_xticks(range(N_STOCKS)) +ax8.set_yticks(range(N_STOCKS)) +ax8.set_xticklabels(stock_names, rotation=90, fontsize=5.5, color=LT) +ax8.set_yticklabels(stock_names, fontsize=5.5, color=LT) +# 行业分割线 / Sector boundary lines +sector_bounds = [0, 5, 9, 13, 17, 20] +for b in sector_bounds[1:-1]: + ax8.axhline(b - 0.5, color='white', lw=0.9, alpha=0.8) + ax8.axvline(b - 0.5, color='white', lw=0.9, alpha=0.8) +cb = plt.colorbar(im, ax=ax8, fraction=0.046, pad=0.04) +cb.ax.tick_params(colors=LT, labelsize=7) + +# ── 图9:绩效汇总表 ── +# Performance summary table +ax9.axis('off') +ax9.set_title("绩效汇总 Performance Summary", color=LT, fontsize=10, pad=6) +col_labels = ["年化收益\nAnn.Ret", "年化波动\nAnn.Vol", + "夏普比率\nSharpe", "最大回撤\nMax DD", "Calmar\n比率"] +row_labels = [n.split(" ")[0] for n in STRATEGY_NAMES] +cell_data = [] +for name in STRATEGY_NAMES: + r = perf_df.loc[name] + cell_data.append([ + f"{r['年化收益']:+.1%}", + f"{r['年化波动']:.1%}", + f"{r['夏普比率']:.2f}", + f"{r['最大回撤']:.1%}", + f"{min(r['Calmar'], 99.0):.2f}", + ]) + +tbl = ax9.table(cellText=cell_data, rowLabels=row_labels, + colLabels=col_labels, loc='center', cellLoc='center') +tbl.auto_set_font_size(False) +tbl.set_fontsize(8) +tbl.scale(1.12, 2.0) +for (row, col), cell in tbl.get_celld().items(): + cell.set_facecolor(DARK_BG) + cell.set_edgecolor(GRID_C) + cell.set_text_props(color=LT) + if row == 0 or col == -1: + cell.set_facecolor('#2A2D3A') + cell.set_text_props(color='#FFD700', fontweight='bold') + +# 保存图表 / Save figure +OUTPUT_FILE = "portfolio_optimization_demo.png" +plt.savefig(OUTPUT_FILE, dpi=150, bbox_inches='tight', + facecolor=fig.get_facecolor()) + +print(f" 图表已保存: {OUTPUT_FILE} / Chart saved: {OUTPUT_FILE}") + +print("\n" + "=" * 70) +print(" 演示完成! Demo Complete!") +print(f" 输出: {OUTPUT_FILE}") +print("=" * 70)