Compare commits
No commits in common. "5519ab9735672d26e1328d8f7fe97c536ac83cb4" and "9546279fcffee681341d4db431e0505dbfacde55" have entirely different histories.
5519ab9735
...
9546279fcf
Binary file not shown.
|
Before Width: | Height: | Size: 800 KiB |
|
|
@ -1,647 +0,0 @@
|
||||||
# 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 ICIR:IC 的信噪比
|
|
||||||
|
|
||||||
$$\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-S(Q5-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 估计,不容易过拟合
|
|
||||||
- 缺点:对强因子和弱因子一视同仁
|
|
||||||
|
|
||||||
**方法2:IC 加权合成 (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 什么是因子衰减?
|
|
||||||
|
|
||||||
因子衰减曲线回答一个关键的实践问题:
|
|
||||||
**"这个因子的信号,在多少天后就失效了?"**
|
|
||||||
|
|
||||||
我们对不同的持有期 H(1D, 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.065),42D 以后才明显下降。
|
|
||||||
→ 说明动量信号在 1-3 个月内保持有效,**月频换仓足够**,不需要高频交易
|
|
||||||
|
|
||||||
**REV(快变化)**:在 42D 处强度减弱(-0.027),62D 处轻微回升。
|
|
||||||
→ 反转信号在短期更强,长期减弱,适合**周频换仓**
|
|
||||||
|
|
||||||
### 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、分层回测、因子合成、多空组合 |
|
|
||||||
|
|
@ -1,619 +0,0 @@
|
||||||
# 组合优化详解
|
|
||||||
## `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、约束优化 |
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 484 KiB |
File diff suppressed because it is too large
Load Diff
|
|
@ -1,949 +0,0 @@
|
||||||
"""
|
|
||||||
量化交易演示系列 第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)
|
|
||||||
Loading…
Reference in New Issue