Compare commits

..

2 Commits

6 changed files with 3237 additions and 0 deletions

BIN
alpha_factor_demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 KiB

647
doc_04_alpha_factor.md Normal file
View File

@ -0,0 +1,647 @@
# Alpha 因子研究详解
## `quant_alpha_factor_demo.py` 学习文档
> **目标读者**:量化入门者,已完成前三篇学习
> **配套文件**`quant_alpha_factor_demo.py`
> **前置知识**:建议先阅读前三篇文档
> **系列位置**:第 4 篇 — Alpha 因子研究篇
---
## 目录
1. [什么是 Alpha 因子?](#1-什么是-alpha-因子)
2. [因子动物园:主要因子流派](#2-因子动物园factor-zoo)
3. [合成股票池:为什么需要多只股票?](#3-合成股票池)
4. [五个经典因子详解](#4-五个经典因子详解)
5. [因子预处理:三步标准化流程](#5-因子预处理三步标准化)
6. [IC 分析:量化因子预测能力](#6-ic-分析information-coefficient)
7. [分层回测:直观验证因子有效性](#7-分层回测quantile-analysis)
8. [因子合成1+1 > 2](#8-因子合成factor-combination)
9. [多空组合:因子的实际落地](#9-多空组合long-short-portfolio)
10. [因子衰减:信号会过时](#10-因子衰减factor-decay)
11. [结果解读与局限性](#11-结果解读与局限性)
12. [术语速查表](#12-术语速查表)
---
## 1. 什么是 Alpha 因子?
### 1.1 从一个比喻说起
想象你是一个选股顾问,每天从 3000 只 A 股中挑出最有潜力的 50 只。你会怎么做?
初学者可能说:"涨得多的买,跌得多的卖。"
有经验的人可能说:"找低估值、高成长、低波动的股票。"
**Alpha 因子 (Alpha Factor)** 就是这些选股思路的**数学化表达**——用一个公式,对每只股票算出一个"分数",分数越高,预期未来收益越高。
### 1.2 Alpha 的准确含义
在金融学中,收益被分解为两部分:
```
股票总收益 = Beta 收益 + Alpha 收益
股票总收益 = (跟随大盘的那部分) + (超出大盘的那部分)
```
| 术语 | 中文 | 含义 |
|------|------|------|
| **Beta (β)** | 市场贝塔 / 系统性风险 | 随市场涨跌的收益——"大盘涨了 10%,我的股票也涨了 8%" |
| **Alpha (α)** | 超额收益 / 选股能力 | 超越市场的额外收益——"大盘涨了 5%,但我的策略赚了 20%" |
**Alpha 因子**的目标就是找到那些能预测"未来超额收益"的信号。
### 1.3 因子 vs. 策略
很多初学者会混淆"因子"和"策略"的概念:
| | 因子 (Factor) | 策略 (Strategy) |
|--|--------------|----------------|
| 本质 | 一个截面排名信号(谁更好?) | 一套完整的买卖规则 |
| 例子 | MOM = 12月动量 | "当 MOM > 0 时买入,< 0 时卖出" |
| 分析方法 | IC、分层回测 | 净值曲线、夏普比率 |
| 应用范围 | **多只股票的横截面** | 单只或多只股票的时间序列 |
因子是选股的"原材料",策略是用因子构建的"成品"。
---
## 2. 因子动物园Factor Zoo
学术界发现的因子多达 300+ 个,被戏称为"因子动物园 (Factor Zoo)"。按经济学逻辑,主要分为以下几大类:
### 2.1 行为金融类因子(最常用)
| 因子 | 中文 | 核心逻辑 | 代表论文 |
|------|------|----------|----------|
| **Momentum** | 动量因子 | 赢家继续赢,输家继续输(追涨惯性)| Jegadeesh & Titman (1993) |
| **Reversal** | 反转因子 | 短期过度涨跌会均值回归 | Jegadeesh (1990) |
| **Post-Earnings Drift** | 盈利漂移 | 超预期财报后股价持续漂移 | Ball & Brown (1968) |
### 2.2 风险溢价类因子(有理论支撑)
| 因子 | 中文 | 核心逻辑 |
|------|------|----------|
| **Low Volatility** | 低波动因子 | 低波动股票的风险调整后收益更高(违反 CAPM|
| **Low Beta** | 低贝塔因子 | 低市场敏感性股票长期跑赢BAB 效应)|
| **Illiquidity** | 非流动性因子 | 流动性差的股票有额外的流动性溢价 |
### 2.3 基本面类因子(需要财报数据)
| 因子 | 中文 | 核心逻辑 |
|------|------|----------|
| **Value (B/P, E/P)** | 价值因子 | 低估值股票长期跑赢(价值投资量化版)|
| **Profitability (ROE)** | 盈利能力 | 高盈利质量公司持续超额收益 |
| **Growth** | 成长因子 | 营收/利润高速增长的公司 |
> 本 Demo 聚焦于**纯价格/成交量因子**(无需财报),这也是因子研究的起点。
---
## 3. 合成股票池
### 3.1 为什么需要多只股票?
前三篇 Demo 分析的都是**单只股票随时间**的变化(时间序列)。
Alpha 因子研究是一种完全不同的分析维度:
```
时间序列分析: 分析同一只股票在不同时间的行为
苹果公司 → 2020, 2021, 2022, ...
截面分析: 在同一个时间点,比较不同股票之间的差异
2023-01-01 → 苹果, 谷歌, 微软, 亚马逊, ...
```
因子的"截面预测能力"是指:**在某一天,因子值高的股票,是否在未来一段时间表现更好**
这需要"同一天、多只股票"的数据——即**横截面数据 (Cross-Sectional Data)**。
### 3.2 隐藏因子生成模型
为了让演示中的因子真实有效,本 Demo 使用了一个**隐藏因子生成模型 (Hidden Factor Generative Model)**,向合成数据中注入"真实的 Alpha"
```
每只股票每天的收益 = 市场风险 + 质量 Alpha + 动量 Alpha + 噪音
= β × r_市场 + λ_q × Q_i,t + λ_m × M_i,t + ε_i,t
```
| 成分 | 符号 | 变化速度 | 含义 |
|------|------|----------|------|
| 市场风险 | β × r_市场 | 每日变化 | 所有股票共同承担的市场涨跌 |
| 质量 Alpha | λ_q × Q_i,t | 季度持续 | 该股票的"潜在质量分",缓慢变化 |
| 动量 Alpha | λ_m × M_i,t | 月度持续 | 该股票的"当前动量状态",适度变化 |
| 特质噪音 | ε_i,t | 每日随机 | 无法预测的随机波动 |
**关键点**Q 和 M 是**不可直接观察的隐藏变量**。
Alpha 因子的价值就在于:**用可观察的价格数据(动量、波动率等)来间接估计这些隐藏的质量/动量分数**。
---
## 4. 五个经典因子详解
### 4.1 动量因子MOM— 追涨惯性
**来源**Jegadeesh & Titman (1993)
**直觉**:股票市场存在"趋势延续"效应——过去 6-12 个月的赢家,未来 3-12 个月通常继续赢。
$$\text{MOM}_{i,t} = \frac{P_{i,t-21}}{P_{i,t-252}} - 1$$
| 参数 | 说明 |
|------|------|
| $P_{i,t-21}$ | 股票 i 在 t-21 日(约 1 个月前)的价格 |
| $P_{i,t-252}$ | 股票 i 在 t-252 日(约 12 个月前)的价格 |
| 计算窗口 | 从 12 个月前 到 1 个月前(跳过最近 1 个月)|
**为什么要跳过最近 1 个月?**
最近 1 个月的收益往往会**短期反转 (Short-Term Reversal)**,这是由于做市商 (Market Maker) 的库存调整和买卖价差摩擦导致的微观结构效应。如果不跳过最近 1 个月,反转效应会污染动量信号。
```python
# 代码实现
MOM_LONG = 252 # 12 个月
MOM_SHORT = 21 # 跳过 1 个月
factor_mom = prices_df.shift(MOM_SHORT) / prices_df.shift(MOM_LONG) - 1.0
# ↑ P[t-21] ↑ P[t-252]
```
**本 Demo 实验结果**
- IC 均值 = **+0.065** ★ 有效
- ICIR = **+0.452**(接近 0.5 的优良水平)
- 这是因为隐藏质量/动量因子高的股票,自然同时有高 12M 动量 AND 高未来收益
### 4.2 短期反转因子REV— 均值回归
**来源**Jegadeesh (1990)
**直觉**:上个月涨太多的股票,下个月往往小幅回调;跌太多的往往反弹。
$$\text{REV}_{i,t} = -\left(\frac{P_{i,t}}{P_{i,t-21}} - 1\right)$$
取负号是为了保持"因子值越高 → 预期收益越高"的正方向约定:
- 上个月大跌(负收益)→ REV 为正 → 预期反弹 → 因子值高 ✓
**本 Demo 实验结果**
- IC 均值 = **-0.050**(负!意料之中)
- 为什么是负的?因为我们的数据中注入了"动量 Alpha"——动量强的股票上个月涨、下个月还要涨REV 反而是负向预测。这是一个很好的教学点:**同一个市场中,因子之间可能互相矛盾**。
### 4.3 低波动因子LVOL— 低波动异象
**来源**Baker, Bradley & Wurgler (2011)
**直觉**:根据传统金融理论,高风险应获高回报。但实证研究发现,低波动股票的**原始收益**也更高,即"低波动异象 (Low-Volatility Anomaly)"。
$$\text{LVOL}_{i,t} = -\sigma_{i,t-60:t}$$
其中 $\sigma$ 是过去 60 交易日的日收益率标准差(年化波动率的代理指标)。
取负号:波动率越低 → LVOL 越高 → 预期收益越高。
**可能的解释**
- 机构投资者受"跑赢基准"的业绩压力,被迫偏爱高贝塔/高波动股票,导致这些股票被高估
- 散户偏好"彩票式"高波动股票(类似买彩票),也导致高波动股票被高估
- 低波动股票更少被卖空,保护其免遭过度高估后的暴跌
### 4.4 低贝塔因子BAB— 赌对低贝塔
**来源**Frazzini & Pedersen (2014) "Betting Against Beta"
**直觉**CAPM 预测 β = 1.5 的股票比 β = 0.5 的股票多 50% 的期望收益。但实际上两者差距远小于理论值,甚至低贝塔股票反而更赚钱。
$$\text{BAB}_{i,t} = -\hat{\beta}_{i,t}^{(60D)}$$
$$\hat{\beta}_{i,t} = \frac{\text{Cov}(r_i, r_{\text{mkt}})_{60D}}{\text{Var}(r_{\text{mkt}})_{60D}}$$
**代码实现**(完全向量化,不用 Python 循环):
```python
# 利用 pandas rolling 方法批量计算所有股票的滚动贝塔
rolling_var_mkt = market_series.rolling(60).var() # 市场方差
rolling_cov = log_ret_df.apply(
lambda col: col.rolling(60).cov(market_series) # 各股协方差
)
rolling_beta_df = rolling_cov.div(rolling_var_mkt, axis=0) # β = Cov/Var
factor_bab = -rolling_beta_df # 取负值
```
### 4.5 Amihud 非流动性因子ILLIQ— 流动性溢价
**来源**Amihud (2002) "Illiquidity and Stock Returns"
$$\text{ILLIQ}_{i,t} = \frac{1}{D} \sum_{d=t-D}^{t} \frac{|r_{i,d}|}{V_{i,d}}$$
| 符号 | 含义 |
|------|------|
| $|r_{i,d}|$ | 第 d 日的绝对日收益率 |
| $V_{i,d}$ | 第 d 日的成交量(元) |
| D = 20 | 过去 20 个交易日 |
**含义**"每元成交量能引起多大的价格波动?"
- ILLIQ 高 → 流动性差(小单也能推动价格)→ 投资者要求更高回报 → **流动性溢价 (Liquidity Premium)**
---
## 5. 因子预处理:三步标准化
原始因子值"脏"得没法直接用,必须经过以下三步清洗:
### Step 1截面去极值Winsorization
**问题**:某天某股票的动量值是 +500%(因为遭遇了重组事件),这个极端值会完全主导本日截面的相关系数计算。
**解决**:在每个截面,将超出 $[\mu - 3\sigma, \mu + 3\sigma]$ 的值截断到边界。
```
原始: [5%, 12%, 8%, 500%, -2%, 9%]
去极值: [5%, 12%, 8%, 46%, -2%, 9%] ← 500% 被截断到 3σ 边界
```
```python
# 向量化实现(无 Python 循环)
mu = factor_df.mean(axis=1) # 每日截面均值
sig = factor_df.std(axis=1) # 每日截面标准差
lower = (mu - 3 * sig).values[:, np.newaxis] # 广播到矩阵形状
upper = (mu + 3 * sig).values[:, np.newaxis]
clipped = np.clip(factor_df.values, lower, upper)
```
### Step 2截面 Z-score 标准化
**问题**:动量因子单位是%(典型值 ±30%ILLIQ 单位是极小的小数1e-10 量级),无法比较和相加。
**解决**:在每个截面,对因子进行 Z-score 标准化,使均值=0标准差=1。
$$z_{i,t} = \frac{x_{i,t} - \bar{x}_t}{\sigma_t}$$
标准化后:
- 不同因子量纲统一,可以直接相加(因子合成时需要)
- Z-score > 0 → 该股票因子值高于同期平均
- Z-score < 0 该股票因子值低于同期平均
### Step 3市值中性化Market Cap Neutralization
**问题**:在 A 股市场,小市值股票往往同时具有高动量、高波动、低流动性。如果不剔除市值效应,动量因子可能只是在选小市值股票,并非真正的动量信号。
**解决**:对 log(市值) 做 OLS 回归,用**残差**替换原因子值:
$$\text{factor\_neutral}_i = \text{factor}_i - (\hat{\alpha} + \hat{\beta} \cdot \log(\text{mktcap}_i))$$
```python
# 每个截面: y = alpha + beta * x + residual
y_c = factor_values # (50,) 各股因子值
x_c = log_mktcap_values # (50,) 各股对数市值
xm, ym = x_c.mean(), y_c.mean()
slope = np.dot(x_c - xm, y_c - ym) / np.dot(x_c - xm, x_c - xm)
residuals = y_c - (ym + slope * (x_c - xm)) # 市值中性化后的因子值
```
**形象比喻**
这就像做标准化考试时,把"学生身高"这个干扰变量从"篮球成绩"中剔除——矮的学生篮球技术再好,也会因为身高被低估,剔除后才能真正比较技术水平。
---
## 6. IC 分析Information Coefficient
### 6.1 IC 的定义
$$\text{IC}_t = \text{SpearmanCorr}\left(\text{factor}_{t,\cdot},\ \text{return}_{t+H,\cdot}\right)$$
在日期 t计算截面 **Spearman 秩相关系数**
- 左侧50 只股票在 t 日的因子值(处理后)
- 右侧:这 50 只股票在未来 H=21 日的收益率
**为什么用 Spearman 秩相关而不用 Pearson 线性相关?**
| | Pearson | Spearman |
|--|---------|----------|
| 计算 | $(X - \bar{X}) \cdot (Y - \bar{Y})$ 的相关 | $\text{rank}(X)$ 与 $\text{rank}(Y)$ 的相关 |
| 假设 | 线性关系 | 只需单调关系 |
| 对极端值 | 敏感 | 鲁棒 |
| 量化研究常用 | ○ | ✓ 更常用 |
### 6.2 ICIRIC 的信噪比
$$\text{ICIR} = \frac{\text{IC 均值}}{\text{IC 标准差}}$$
ICIR 就是"IC 序列的夏普比率"——衡量因子预测能力的**稳定性**
| ICIR | 含义 |
|------|------|
| < 0.3 | 不稳定可能只是偶然 |
| 0.3 ~ 0.5 | 有一定稳定性 |
| > 0.5 | 稳定的 Alpha 信号 |
### 6.3 IC 评价标准
| 指标 | 强因子 | 有效因子 | 弱因子 |
|------|--------|----------|--------|
| \|IC 均值\| | > 0.10 | 0.05~0.10 | < 0.05 |
| ICIR | > 0.5 | 0.3~0.5 | < 0.3 |
| IC > 0 比率 | > 60% | 55%~60% | < 55% |
### 6.4 本 Demo 的 IC 结果解读
| 因子 | IC 均值 | ICIR | 解读 |
|------|---------|------|------|
| MOM ★ | +0.065 | +0.452 | 动量有效!因为高质量/高动量股票有持续的正 Alpha |
| REV | -0.050 | -0.273 | 反转因子在此模型中"反着来"——短期涨的股票继续涨(动量效应更强)|
| LVOL | -0.010 | -0.028 | 近乎无效——数据中没有注入"低波动溢价"的 Alpha |
| BAB | -0.010 | -0.027 | 近乎无效——原因同上 |
| ILLIQ | +0.009 | +0.058 | 极弱——有轻微的流动性溢价信号 |
> **学习要点**IC 近零不代表因子"写错了",而是反映了**数据生成机制**。本 Demo 的合成数据中Alpha 主要来自质量和动量因子,所以 MOM 有效而 LVOL/BAB 无效。真实市场中,多个因子通常都有正 IC但有时某种因子会在特定市场环境regime下失效。
---
## 7. 分层回测Quantile Analysis
### 7.1 方法步骤
分层回测是最直观的因子验证方式:
```
① 在每个调仓日 T按因子值对 50 只股票从小到大排序
② 等分成 5 组Q1=最低 20%Q5=最高 20%
③ 各组各构建等权组合,持有 21 天
④ 记录各组收益,持续到回测结束
```
**期望的理想结果**
```
累积收益: Q5 > Q4 > Q3 > Q2 > Q1 (单调递增)
```
如果因子有效Q5因子分最高应该持续跑赢 Q1因子分最低
### 7.2 本 Demo 的 MOM 因子分层结果
| 分组 | 年化收益 | 含义 |
|------|----------|------|
| Q1最低动量| -8.9% | 过去一年最弱势的股票,未来继续弱 |
| Q2 | -12.7% | 偏弱 |
| Q3 | -5.0% | 中性 |
| Q4 | +9.7% | 偏强 |
| Q5最高动量| +27.9% | 过去一年最强势的股票,未来继续强 |
| **L-SQ5-Q1**| **+40.1%** | 多空价差,几乎与市场涨跌无关 |
虽然 Q2 < Q1 有轻微异常不完全单调但整体趋势清晰**高动量 高未来收益**
### 7.3 分层回测的实现细节
```python
for i in range(len(rebal_dates) - 1):
t0, t1 = rebal_dates[i], rebal_dates[i + 1] # 建仓日 / 平仓日
# 按因子值分组
labels = pd.qcut(
factor_today.rank(method='first'), # 先取秩(避免重复值问题)
n_quantiles, # 分成 5 组
labels=["Q1", "Q2", "Q3", "Q4", "Q5"]
)
# 计算等权持有收益
hold_ret = price_df.loc[t1] / price_df.loc[t0] - 1.0
for q in ["Q1", "Q2", "Q3", "Q4", "Q5"]:
stocks_in_q = labels[labels == q].index
bucket_returns[q].append(hold_ret[stocks_in_q].mean())
```
**为什么先 `rank()``qcut()`**
`qcut` 按数值边界分割如果因子值有重复e.g., 好几只股票 LVOL 恰好相同),分组会不均匀。先 `rank(method='first')` 将重复值也区分开(先出现的秩更低),再 `qcut` 就能保证每组大小相同。
---
## 8. 因子合成Factor Combination
### 8.1 为什么要合成?
单个因子的 IC 通常只有 0.05~0.10,预测能力有限。多因子合成的优势:
1. **互补**:不同因子捕捉不同维度的 Alpha合成后覆盖更多信息源
2. **降噪**:单个因子可能在某些时期失效;多因子合成后整体更稳定
3. **提升 ICIR**:若各因子 IC 不完全相关,合成后 ICIR > 单个因子的 ICIR
用统计语言表达:设因子 A 和 B 各自 IC 均值 = $\mu$IC 相关系数 = $\rho$,则:
$$\text{ICIR}_{composite} = \frac{2\mu}{2\sigma\sqrt{(1+\rho)/2}} = \frac{\text{ICIR}}{\sqrt{(1+\rho)/2}}$$
当 $\rho < 1$ 合成 ICIR > 单个 ICIR。这就是**多元化的价值**。
### 8.2 两种合成方法
**方法1等权合成 (Equal-Weight Composite)**
$$\text{Composite}_{EQ,i,t} = \frac{1}{N}\sum_{k=1}^{N} z_{k,i,t}$$
- 优点:简单、稳健,不依赖历史 IC 估计,不容易过拟合
- 缺点:对强因子和弱因子一视同仁
**方法2IC 加权合成 (IC-Weighted Composite)**
$$w_k = \frac{\max(\overline{IC}_k, 0)}{\sum_j \max(\overline{IC}_j, 0)}, \quad \text{Composite}_{ICW,i,t} = \sum_{k=1}^{N} w_k \cdot z_{k,i,t}$$
- 优点:给更强的因子更高权重,理论上 ICIR 更高
- 缺点:历史 IC 可能不稳定(特别是样本期短时),有过拟合风险
**本 Demo 结果**
| 方法 | IC 均值 | ICIR |
|------|---------|------|
| 等权合成 | +0.036 | +0.237 |
| IC 加权 | +0.045 | +0.304 |
IC 加权略好,因为它把更多权重给了最强的 MOM 因子(权重 88.4%)。
---
## 9. 多空组合Long-Short Portfolio
### 9.1 构建逻辑
多空策略是因子策略的"利润提取机制"
```
每个调仓日(每月):
① 计算每只股票的合成因子分
② 买入 Top 20%(因子分最高的 10 只)= 多头组合
③ 做空 Bottom 20%(因子分最低的 10 只)= 空头组合
④ 21 天后平仓,重复
```
### 9.2 市场中性Market Neutral
多空组合最大的优点是**市场中性 (Market Neutral)**
```
多空收益 = 多头组合收益 - 空头组合收益
= (Alpha_long + Beta_long × r_市场) - (Alpha_short + Beta_short × r_市场)
≈ Alpha_long - Alpha_short (若 Beta_long ≈ Beta_short市场影响相消
```
这意味着:**无论市场大涨大跌,多空组合只赚 Alpha不受市场影响**。
### 9.3 本 Demo 结果解读
| 组合 | 年化收益 | 夏普比率 | 最大回撤 |
|------|----------|----------|----------|
| 多头 (Long) | +30.9% | 1.36 | -13.9% |
| 空头 (Short) | -2.3% | -0.11 | -43.4% |
| **多空 (L-S)** | **+33.9%** | **5.19** | **-0.4%** |
多空 Sharpe 达到 5.19 并最大回撤仅 -0.4%——这是**演示数据** 注入了较强 Alpha 信号的结果,真实市场中通常 Sharpe 1-3 就算非常优秀的多空策略。
### 9.4 A 股限制与现实考量
> ⚠️ **A 股重要提示**
> A 股融券做空受到严格限制,**普通投资者无法执行多空策略**。
> 多空策略主要适用于:港股、美股市场,或通过股指期货进行市场对冲。
> 在 A 股,因子策略通常只执行"多头"部分(买入高分股票),不做空低分股票。
---
## 10. 因子衰减Factor Decay
### 10.1 什么是因子衰减?
因子衰减曲线回答一个关键的实践问题:
**"这个因子的信号,在多少天后就失效了?"**
我们对不同的持有期 H1D, 5D, 10D, 21D, 42D, 63D分别计算 IC
$$\text{IC}(H)_t = \text{SpearmanCorr}\left(\text{factor}_{t,\cdot},\ \text{return}_{t+H,\cdot}\right)$$
如果 IC(H) 随 H 增大而快速降向 0说明因子是"短期信号",需要高频换仓。
如果 IC(H) 衰减缓慢,说明因子是"长期信号",低频换仓也有效。
### 10.2 本 Demo 的衰减结果
| 持有期 | MOM (IC) | REV (IC) |
|--------|----------|----------|
| 1D | +0.067 | -0.043 |
| 5D | +0.066 | -0.045 |
| 10D | +0.065 | -0.046 |
| 21D | +0.065 | -0.050 |
| 42D | +0.061 | -0.027 |
| 63D | +0.054 | -0.033 |
**MOM慢衰减**IC 从 1D 到 21D 几乎不变(+0.067 → +0.06542D 以后才明显下降。
→ 说明动量信号在 1-3 个月内保持有效,**月频换仓足够**,不需要高频交易
**REV快变化**:在 42D 处强度减弱(-0.02762D 处轻微回升。
→ 反转信号在短期更强,长期减弱,适合**周频换仓**
### 10.3 实践意义
```
快速衰减因子:
优点: 信号新鲜,短期预测准确
缺点: 必须高频换仓 → 高换手率 (Turnover) → 高交易成本
适合: 高频量化交易机构,有直连交易所的低手续费通道
慢速衰减因子:
优点: 不需要频繁换仓,交易成本低
缺点: 信号滞后,无法捕捉短期机会
适合: 中低频量化(月频/季频)、散户量化尝试
```
---
## 11. 结果解读与局限性
### 11.1 为什么 LVOL 和 BAB 无效?
在本 Demo 中LVOL 和 BAB 的 IC ≈ 0原因在于数据生成机制
我们只注入了**质量**和**动量** Alpha没有注入"低波动溢价"或"低贝塔溢价"。因此,低波动率并不预示更高未来收益。
在**真实市场**中低波动因子在许多市场美股、港股、A 股)都有实证支撑。本 Demo 的结果不能说明低波动因子无效——只能说明它在**这个特定的合成数据**中无效。
### 11.2 合成数据的局限性
| 局限 | 真实市场中不存在 | 本 Demo 简化了 |
|------|-----------------|---------------|
| 市场冲击 | 大单买卖会推高/压低价格 | 假设无限流动性 |
| 交易成本 | 买卖价差、印花税、佣金 | 未扣除交易成本 |
| 因子拥挤 | 大家都用同一个因子时,超额收益消失 | 未模拟 |
| 财报数据 | 基本面因子需要财报,有发布延迟 | 未涉及 |
| 风险暴露限制 | 真实组合需控制行业、风格暴露 | 无约束 |
### 11.3 量化因子研究的注意事项
1. **样本外验证Out-of-Sample Test**IC 是在同一数据集上计算的,存在过拟合风险。实践中需要用样本外数据验证。
2. **因子衰减与换仓成本的权衡**IC 高不等于策略盈利,必须考虑换手成本是否侵蚀了因子收益。
3. **市场环境Regime的影响**动量因子在趋势市中有效在剧烈波动的市场如2020年3月可能大幅失效。
4. **因子拥挤Factor Crowding**:当太多资金追同一个因子时,因子溢价会被套利消除。
---
## 12. 术语速查表
| 中文 | English | 简要说明 |
|------|---------|----------|
| Alpha 因子 | Alpha Factor | 能预测股票未来超额收益的数学信号 |
| 超额收益 | Alpha | 超越市场基准的额外收益 |
| 市场贝塔 | Market Beta (β) | 股票收益对市场收益的敏感度 |
| 截面数据 | Cross-Sectional Data | 同一时间点多只股票的数据 |
| 时间序列 | Time Series | 同一只股票随时间变化的数据 |
| 因子动物园 | Factor Zoo | 学术界发现的 300+ 个因子的统称 |
| 动量因子 | Momentum Factor | 基于过去 12-1 月累积收益的趋势延续信号 |
| 动量效应 | Momentum Effect | 赢家继续赢、输家继续输的市场现象 |
| 短期反转 | Short-Term Reversal | 短期内价格过度波动后的均值回归 |
| 低波动异象 | Low Volatility Anomaly | 低波动股票收益反而更高,违反 CAPM |
| 非流动性 | Illiquidity | 每单位成交量引起的价格波动幅度 |
| 流动性溢价 | Liquidity Premium | 流动性差的资产额外要求的收益补偿 |
| 低贝塔 | Low Beta (BAB) | 贝塔系数低的股票,与市场敏感性低 |
| 赌对低贝塔 | Betting Against Beta | Frazzini & Pedersen 提出的低贝塔超额收益策略 |
| Amihud 指标 | Amihud Illiquidity | \|r\|/成交量的均值,衡量流动性的经典指标 |
| 去极值 | Winsorization | 将极端异常值截断到合理范围 |
| 截面标准化 | Cross-Sectional Normalization | 在每个日期截面做 Z-score 标准化 |
| Z-score | Z-score | (x - 均值) / 标准差,使均值=0标准差=1 |
| 市值中性化 | Market Cap Neutralization | 回归剔除市值因子对因子值的影响 |
| 信息系数 | IC (Information Coefficient) | 因子值与未来收益的 Spearman 秩相关系数 |
| IC 均值 | IC Mean | IC 时间序列的均值,衡量因子平均预测能力 |
| IC 标准差 | IC Std | IC 时间序列的波动率,衡量因子稳定性 |
| ICIR | ICIR (IC Information Ratio) | IC均值/IC标准差类似因子的夏普比率 |
| Spearman 秩相关 | Spearman Rank Correlation | 基于排名(秩)而非原始值的相关系数 |
| Pearson 相关 | Pearson Correlation | 基于线性关系的相关系数 |
| 持有期 | Holding Period (H) | 建仓到平仓的时间跨度 |
| 分层回测 | Quantile Analysis / Bucket Test | 按因子值分组,对比各组未来收益 |
| 五分位 | Quintile (Q1-Q5) | 将股票等分为 5 组 |
| 等权组合 | Equal-Weight Portfolio | 每只股票持仓权重相同 |
| 调仓日 | Rebalance Date | 重新计算因子并调整持仓的日期 |
| 换手率 | Turnover | 每期组合发生变化的比例,越高交易成本越大 |
| 因子合成 | Factor Combination | 将多个因子加权合成为一个综合因子 |
| 等权合成 | Equal-Weight Composite | 所有因子等权相加的合成方法 |
| IC 加权合成 | IC-Weighted Composite | 按各因子历史 IC 均值加权合成 |
| 多因子模型 | Multi-Factor Model | 使用多个因子解释股票收益差异的模型 |
| 多空组合 | Long-Short Portfolio | 做多高分股票、做空低分股票的中性化策略 |
| 多头 | Long | 买入持有,期望价格上涨 |
| 空头 | Short / Short Selling | 借入并卖出,期望价格下跌后回购获利 |
| 多空价差 | Long-Short Spread | 多头组合收益 - 空头组合收益 |
| 市场中性 | Market Neutral | 多空两侧的市场敝口相互对冲,净市场风险 ≈ 0 |
| 因子衰减 | Factor Decay | 因子预测能力随持有期延长而减弱的现象 |
| 市场环境 | Market Regime | 具有共同特征的特定市场状态(如牛市/熊市/震荡市)|
| 因子拥挤 | Factor Crowding | 过多资金追逐同一因子导致溢价消失的现象 |
| 样本外验证 | Out-of-Sample Test | 在训练数据之外的独立数据集上验证模型效果 |
| 过拟合 | Overfitting | 模型过度适应训练数据,导致样本外表现差 |
| 隐藏因子 | Hidden Factor / Latent Factor | 不可直接观察、只能间接估计的潜在驱动因子 |
| 截面 | Cross-Section | 在同一时间点对多个实体的观察 |
| OLS 回归 | OLS (Ordinary Least Squares) | 普通最小二乘法,用于因子中性化 |
| 残差 | Residual | OLS 回归中因变量实际值与拟合值的差值 |
---
*上一篇:[事件驱动回测](doc_03_event_driven_backtest.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、分层回测、因子合成、多空组合 |

View File

@ -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-Litterman1990给出了一个优雅的解决方案**从市场均衡出发,而不是从历史均值出发**。
### 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.94vs 无约束的 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、约束优化 |

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

1022
quant_alpha_factor_demo.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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)), # S00S04
"金融 Finance": list(range(5, 9)), # S05S08
"消费 Consumer": list(range(9, 13)), # S09S12
"工业 Industrial": list(range(13, 17)), # S13S16
"医疗 Healthcare": list(range(17, 20)), # S17S19
}
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 × NP[k, i] = 资产 i 在观点 k 中的权重(正=多头,负=空头)
P = np.zeros((K, N_STOCKS))
P[0, 1] = 1.0 # 观点1仅 S01
P[1, 0] = 1.0 # 观点2S00 多头
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)