diff --git a/alpha_factor_demo.png b/alpha_factor_demo.png new file mode 100644 index 0000000..f712c73 Binary files /dev/null and b/alpha_factor_demo.png differ diff --git a/doc_04_alpha_factor.md b/doc_04_alpha_factor.md new file mode 100644 index 0000000..a91a898 --- /dev/null +++ b/doc_04_alpha_factor.md @@ -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 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、分层回测、因子合成、多空组合 | diff --git a/quant_alpha_factor_demo.py b/quant_alpha_factor_demo.py new file mode 100644 index 0000000..1025450 --- /dev/null +++ b/quant_alpha_factor_demo.py @@ -0,0 +1,1022 @@ +# ============================================================================= +# Quantitative Trading — Alpha Factor Research Demo +# 量化交易 — Alpha 因子研究演示 +# ============================================================================= +# +# Alpha 因子 (Alpha Factor) 是量化选股的核心工具。 +# 它是一个数学公式,从市场数据中提取信号,预测哪些股票未来会跑赢大盘。 +# +# Alpha factors are the core tool of quantitative stock selection. +# Each factor is a mathematical formula that extracts a signal from market data +# to predict which stocks will outperform in the future. +# +# 研究流程 / Research Workflow: +# §0 合成股票池 Synthetic Universe (50只股票 × 3年日线数据) +# §1 因子构建 Factor Construction (5个经典因子) +# §2 因子预处理 Factor Preprocessing (去极值、截面标准化、市值中性化) +# §3 IC 分析 IC Analysis (信息系数 / 因子预测能力量化) +# §4 分层回测 Quantile Analysis (五分位收益分层验证) +# §5 因子合成 Factor Combination (等权 & IC加权合成因子) +# §6 多空组合 Long-Short Portfolio (Top/Bottom 20% 多空策略) +# §7 因子衰减 Factor Decay (IC 随持有期延长的衰减曲线) +# §8 可视化 Visualization (9面板汇总图) +# +# 前置条件 / Prerequisites: +# pip install numpy pandas matplotlib scipy +# +# 运行方式 / Run: +# python quant_alpha_factor_demo.py +# ============================================================================= + +import numpy as np +import pandas as pd +import matplotlib +matplotlib.use('Agg') # 非交互模式,避免 GUI 阻塞 / non-interactive, prevents GUI block +import matplotlib.pyplot as plt +import matplotlib.gridspec as gridspec +from matplotlib.ticker import FuncFormatter +from scipy import stats +import warnings +warnings.filterwarnings('ignore') + +# 中文字体配置 / Chinese font configuration +plt.rcParams['font.sans-serif'] = ['WenQuanYi Zen Hei', 'Arial Unicode MS', 'SimHei', 'DejaVu Sans'] +plt.rcParams['axes.unicode_minus'] = False + +np.random.seed(42) +print("=" * 70) +print(" 量化交易 Alpha 因子研究演示") +print(" Quantitative Trading: Alpha Factor Research Demo") +print("=" * 70) + + +# ============================================================================= +# §0 合成股票池 Synthetic Stock Universe +# ----------------------------------------------------------------------------- +# 真实因子研究需要几百到几千只股票的截面数据 (cross-sectional data)。 +# 我们用"隐藏因子生成模型" (Hidden Factor Generative Model) 合成数据, +# 确保我们构建的因子确实具有预测能力: +# +# Real factor research requires cross-sectional data of hundreds of stocks. +# We use a Hidden Factor Generative Model to synthesize data, ensuring the +# factors we build actually have genuine predictive power. +# +# 每只股票的日收益 = 市场分量 + 质量Alpha + 动量Alpha + 特质噪音 +# stock_daily_return = market_component + quality_alpha + momentum_alpha + noise +# +# 其中: +# market_component = β_i × r_market (系统性风险 / systematic risk) +# quality_alpha = λ_q × quality_score_{i,t} (质量因子加载, 季度持续) +# momentum_alpha = λ_m × momentum_score_{i,t} (动量因子加载, 月度持续) +# noise ~ N(0, σ_idio) (特质风险 / idiosyncratic risk) +# +# 关键: quality_score 和 momentum_score 是"隐藏"的,我们无法直接观察。 +# 但当我们从价格数据中计算因子时,会自然捕捉到这些隐藏信号。 +# Key: these hidden scores are unobservable, but our computed factors will +# naturally capture them — this is exactly what alpha factors do! +# ============================================================================= + +N_STOCKS = 50 # 股票数量 / number of stocks +N_DAYS = 756 # 交易日 ≈ 3 年 / trading days ≈ 3 years +N_QUARTERS = N_DAYS // 63 + 2 # 季度数 / number of quarters +N_MONTHS = N_DAYS // 21 + 2 # 月度数 / number of months + +# 交易日期序列 / Business-day date index +dates = pd.bdate_range(start="2021-01-04", periods=N_DAYS) + +# 股票代码 (模拟A股格式) / Stock symbols (simulated A-share format) +symbols = [f"{str(i).zfill(6)}.{'SH' if i % 2 == 0 else 'SZ'}" for i in range(1, N_STOCKS + 1)] + +# ── 0-A 隐藏质量因子(季度持续)/ Hidden Quality Factor (Quarterly Persistence) ── +# +# 质量因子 AR(1): quality_t = 0.90 × quality_{t-1} + shock +# 季度自回归系数 0.90 → 年度持续性 ≈ 0.90^4 = 0.66(持续性更强) +# Quarterly AR(1) = 0.90 → annual persistence ≈ 0.90^4 = 0.66 (more persistent) +quality_quarterly = np.zeros((N_STOCKS, N_QUARTERS)) +quality_quarterly[:, 0] = np.random.randn(N_STOCKS) +for q in range(1, N_QUARTERS): + # sqrt(1 - 0.90^2) ≈ 0.436 保持方差为 1 / keeps variance = 1 + quality_quarterly[:, q] = (0.90 * quality_quarterly[:, q - 1] + + 0.436 * np.random.randn(N_STOCKS)) + +# 季度 → 每日映射 / Map quarterly to daily +quality_daily = np.zeros((N_DAYS, N_STOCKS)) +for d in range(N_DAYS): + q_idx = min(d // 63, N_QUARTERS - 1) + quality_daily[d] = quality_quarterly[:, q_idx] + +# ── 0-B 隐藏动量因子(月度持续)/ Hidden Momentum Factor (Monthly Persistence) ── +# +# 动量因子 AR(1): momentum_t = 0.60 × momentum_{t-1} + shock +# 月度自回归系数 0.60,意味着 3 个月后的持续性 ≈ 0.60^3 = 0.22 +# Monthly AR(1) coefficient 0.60 → 3-month persistence ≈ 0.22 +momentum_monthly = np.zeros((N_STOCKS, N_MONTHS)) +momentum_monthly[:, 0] = np.random.randn(N_STOCKS) +for m in range(1, N_MONTHS): + # 月度 AR(1) = 0.80,年度持续性 ≈ 0.80^12 = 0.069(合理的动量衰减) + # Monthly AR(1) = 0.80 → annual persistence ≈ 0.80^12 = 0.069 + momentum_monthly[:, m] = (0.80 * momentum_monthly[:, m - 1] + + 0.60 * np.random.randn(N_STOCKS)) + +momentum_daily = np.zeros((N_DAYS, N_STOCKS)) +for d in range(N_DAYS): + m_idx = min(d // 21, N_MONTHS - 1) + momentum_daily[d] = momentum_monthly[:, m_idx] + +# ── 0-C 日收益率生成 / Daily Return Generation ──────────────────────────────── +# +# 日市场收益 (Market Daily Return): 年化μ=8%, σ=20% +mkt_returns = np.random.normal(0.0003, 0.0126, N_DAYS) # 0.20/√252 ≈ 0.0126 + +# 每只股票的市场贝塔 β: 均匀分布在 [0.5, 1.5] +# Each stock's market beta β: uniformly distributed in [0.5, 1.5] +stock_betas = np.random.uniform(0.5, 1.5, N_STOCKS) + +# (N_DAYS, N_STOCKS): 每列是一只股票,每行是一个交易日 +market_component = mkt_returns[:, np.newaxis] * stock_betas[np.newaxis, :] # 市场分量 +quality_component = 0.00080 * quality_daily # 质量贡献:约 ±20% 年化 alpha +momentum_component = 0.00050 * momentum_daily # 动量贡献:约 ±12% 年化 alpha +idio_noise = np.random.normal(0, 0.0075, (N_DAYS, N_STOCKS)) # 特质噪音 + +# 日对数收益率 / Daily log returns +log_returns = market_component + quality_component + momentum_component + idio_noise + +# ── 0-D 价格路径 / Price Paths ──────────────────────────────────────────────── +# P_t = P_0 × exp( Σ log_return_s, s=1..t ) (价格路径 / price path) +initial_prices = np.random.uniform(5.0, 100.0, N_STOCKS) # A股初始价格 +prices_arr = initial_prices * np.exp(np.cumsum(log_returns, axis=0)) + +prices_df = pd.DataFrame(prices_arr, index=dates, columns=symbols) +log_ret_df = pd.DataFrame(log_returns, index=dates, columns=symbols) +simple_ret_df = prices_df.pct_change() # 简单收益率: r_t = P_t/P_{t-1} - 1 + +# ── 0-E 成交量生成 / Volume Generation ────────────────────────────────────── +# 真实市场中,成交量与绝对收益率正相关(大涨大跌时换手更活跃)。 +# In real markets, volume correlates with |return| (more trading on big moves). +# +# log(volume) = log(base) + 3×|log_return| + noise +base_volumes = np.random.uniform(1e6, 5e7, N_STOCKS) # 基础日均成交量(股) +vol_multiplier = np.exp(3.0 * np.abs(log_returns) + 0.3 * np.random.randn(N_DAYS, N_STOCKS)) +volume_df = pd.DataFrame(base_volumes * vol_multiplier, index=dates, columns=symbols) + +# ── 0-F 模拟市值 / Simulated Market Capitalization ─────────────────────────── +# 市值 (Market Cap) = 价格 × 流通股数 / Market Cap = Price × Float Shares +float_shares = np.random.uniform(1e8, 1e10, N_STOCKS) # 流通股数(股) +mktcap_df = prices_df * float_shares # 市值(元) +log_mktcap_df = np.log(mktcap_df) # 对数市值(因子分析常用) + +market_series = pd.Series(mkt_returns, index=dates) # 市场收益率序列 + +print(f"\n[§0] 股票池生成完成 / Universe Generated") +print(f" 股票数量 (Stocks): {N_STOCKS} 只") +print(f" 交易日数 (Days): {N_DAYS} 天 {dates[0].date()} ~ {dates[-1].date()}") +print(f" 价格区间 (Price): {prices_df.min().min():.2f} ~ {prices_df.max().max():.2f} 元") + + +# ============================================================================= +# §1 因子构建 Factor Construction +# ----------------------------------------------------------------------------- +# 我们构建 5 个来自学术文献的经典因子,涵盖不同的 Alpha 来源: +# We build 5 classic factors from academic literature, covering different alpha sources: +# +# ┌──────┬────────────────────────────┬──────────────────────────────────────────┐ +# │ 因子 │ 名称 │ 公式 & 来源 │ +# ├──────┼────────────────────────────┼──────────────────────────────────────────┤ +# │ MOM │ 动量 Momentum │ r(t-252, t-21) Jegadeesh & Titman 1993 │ +# │ REV │ 短期反转 Reversal │ -r(t-21, t) Jegadeesh 1990 │ +# │ LVOL │ 低波动 Low Volatility │ -std(r, 60D) Baker et al. 2011 │ +# │ BAB │ 低贝塔 Betting vs Beta │ -β(60D) Frazzini & Pedersen 2014 │ +# │ ILLIQ│ 非流动性 Amihud Illiquidity│ mean(|r|/V,20D) Amihud 2002 │ +# └──────┴────────────────────────────┴──────────────────────────────────────────┘ +# +# 重要约定: 所有因子都以"因子值越高 → 预期未来收益越高"为正方向。 +# Convention: higher factor value → expected higher future return (unified sign). +# ============================================================================= + +print("\n[§1] 构建因子 / Constructing factors...") + +# ── 1-A 动量因子 (MOM) Momentum Factor ────────────────────────────────────── +# +# 来源 / Source: Jegadeesh & Titman (1993) +# "Returns to Buying Winners and Selling Losers" +# +# 动量效应 (Momentum Effect): 过去 12 个月(跳过最近 1 个月)涨幅最大的股票, +# 未来 3-12 个月通常继续跑赢市场。这是学术上记录最多的股票市场异象之一。 +# +# Momentum effect: stocks with highest 12M-1M returns tend to outperform +# over the next 3-12 months. One of the most documented stock market anomalies. +# +# 为什么要跳过最近 1 个月?/ Why skip the last month? +# 最近 1 个月的收益存在短期反转效应(买卖价差、做市商库存调整等微观结构原因), +# 会污染中期动量信号,所以必须跳过。 +# The last month exhibits short-term reversal due to bid-ask spread and +# market-maker inventory rebalancing, contaminating medium-term momentum. +# +# 公式 / Formula: +# MOM_t = (P_{t-21} / P_{t-252}) - 1 (从12M前 到 1M前 的累积收益) +# MOM_t = (P_{t-21} / P_{t-252}) - 1 (cumulative return from 12M to 1M ago) + +MOM_LONG = 252 # 长回看窗口: 12个月 / lookback long: 12 months +MOM_SHORT = 21 # 跳过窗口: 1个月 / skip window: 1 month + +factor_mom = prices_df.shift(MOM_SHORT) / prices_df.shift(MOM_LONG) - 1.0 +print(f" ✓ MOM 动量因子: 有效值 {factor_mom.notna().mean().mean():.1%}") + +# ── 1-B 短期反转因子 (REV) Short-Term Reversal Factor ─────────────────────── +# +# 来源 / Source: Jegadeesh (1990) +# "Evidence of Predictable Behavior of Security Returns" +# +# 反转效应 (Reversal Effect): 上个月大涨的股票,下个月往往小幅回调; +# 大跌的股票往往小幅反弹。这是对短期"过度反应"的修正。 +# +# Reversal effect: last month's winners tend to reverse; losers tend to bounce. +# This is correction of short-term overreaction. +# +# 注意: 取负号是因为月涨 → 因子值高 → 预期下跌(反转),我们统一正方向。 +# Note: negative sign because high past return → expected to reverse → lower future return, +# we flip to maintain convention: higher factor → higher expected return. +# +# 公式 / Formula: +# REV_t = -(P_t / P_{t-21} - 1) (负1月收益 / negative 1-month return) + +factor_rev = -(prices_df / prices_df.shift(MOM_SHORT) - 1.0) +print(f" ✓ REV 反转因子: 有效值 {factor_rev.notna().mean().mean():.1%}") + +# ── 1-C 低波动因子 (LVOL) Low Volatility Factor ──────────────────────────── +# +# 来源 / Source: Ang et al.(2006); Baker, Bradley & Wurgler (2011) +# "Benchmarks as Limits to Arbitrage: Understanding the Low-Volatility Anomaly" +# +# 低波动异象 (Low Volatility Anomaly): 经典金融理论 CAPM 预测高风险 = 高收益。 +# 但实证研究发现,低波动率的股票反而有更高的原始收益和风险调整后收益! +# +# Low-vol anomaly: contrary to CAPM, low-volatility stocks earn HIGHER +# risk-adjusted and even raw returns than high-volatility stocks. +# +# 可能的解释 / Possible explanations: +# ① 机构投资者受基准约束,被迫持有高贝塔股票 (benchmark constraints) +# ② 投资者对"彩票式"高波动股票有偏好,导致其被高估 (lottery preference) +# ③ 杠杆限制阻止套利者纠正定价偏差 (leverage constraints) +# +# 公式 / Formula: +# LVOL_t = -σ(r_{t-60:t}) (负60日已实现波动率 / negative 60-day realized vol) + +VOL_WINDOW = 60 # 60 个交易日 ≈ 3 个月 / 60 trading days ≈ 3 months +factor_lvol = -(simple_ret_df.rolling(VOL_WINDOW).std()) +print(f" ✓ LVOL 低波动因子: 有效值 {factor_lvol.notna().mean().mean():.1%}") + +# ── 1-D 低贝塔因子 (BAB) Betting Against Beta ────────────────────────────── +# +# 来源 / Source: Frazzini & Pedersen (2014) "Betting Against Beta" +# +# BAB 异象: 做多低贝塔股票、做空高贝塔股票,可以获得持续的超额收益。 +# 与低波动因子类似,但专注于对"市场系统性风险"的暴露,而非总波动率。 +# +# BAB anomaly: long low-beta, short high-beta → persistent excess return. +# Similar to low-vol but focuses on market systematic risk exposure. +# +# 贝塔计算 / Beta computation: +# β_i = Cov(r_i, r_market) / Var(r_market) (60日滚动窗口 / 60-day rolling) +# +# 向量化技巧: 利用 pandas rolling 方法计算所有股票的滚动协方差和市场方差 +# Vectorized trick: use pandas rolling to compute rolling covariance and variance + +BETA_WINDOW = 60 # 60日滚动贝塔 / 60-day rolling beta + +# pandas rolling().cov(other) 计算每个时间点的滚动协方差 +# pandas rolling().cov(other) computes rolling covariance at each time point +rolling_var_mkt = market_series.rolling(BETA_WINDOW).var() # 市场方差 / market variance +rolling_cov = log_ret_df.apply( + lambda col: col.rolling(BETA_WINDOW).cov(market_series) # 逐列协方差 / per-stock cov +) +rolling_beta_df = rolling_cov.div(rolling_var_mkt, axis=0) # β = cov / var +factor_bab = -rolling_beta_df # 取负:低贝塔 → 高因子值 / flip: low-beta → high score +print(f" ✓ BAB 低贝塔因子: 有效值 {factor_bab.notna().mean().mean():.1%}") + +# ── 1-E Amihud 非流动性因子 (ILLIQ) Amihud Illiquidity Factor ─────────────── +# +# 来源 / Source: Amihud (2002) "Illiquidity and Stock Returns" +# +# Amihud 非流动性指标 (Amihud Illiquidity Measure): +# ILLIQ_{i,t} = (1/D) × Σ_{d=t-D+1}^{t} |r_{i,d}| / Volume_{i,d} +# +# 含义 (Interpretation): +# "每单位成交量能引起多大的价格变动?" +# "How much price movement per unit of trading volume?" +# +# 值越高 → 流动性越差 (higher → less liquid) +# 流动性差的股票风险更高,投资者要求额外的"流动性溢价"(liquidity premium) +# Illiquid stocks carry higher risk, investors demand extra liquidity premium +# +# 注: 本因子中,高 ILLIQ(流动性差)→ 预期高收益(溢价补偿)→ 符合正方向约定 +# Note: high ILLIQ (illiquid) → expected high return (premium) → matches positive convention + +ILLIQ_WINDOW = 20 # 20日均值 / 20-day average + +# |日收益率| / 日成交量,再取20日滚动均值 +# |daily return| / daily volume, then 20-day rolling mean +price_impact = simple_ret_df.abs() / volume_df # 每日价格冲击 / daily price impact +factor_illiq = price_impact.rolling(ILLIQ_WINDOW).mean() * 1e8 # 缩放 / scaling +print(f" ✓ ILLIQ 非流动性因子: 有效值 {factor_illiq.notna().mean().mean():.1%}") + +# ── 1-F 汇总 / Consolidate ─────────────────────────────────────────────────── +FACTORS = { + "MOM" : factor_mom, # 动量 + "REV" : factor_rev, # 短期反转 + "LVOL" : factor_lvol, # 低波动 + "BAB" : factor_bab, # 低贝塔 / 贝塔 + "ILLIQ" : factor_illiq, # Amihud 非流动性 +} +print(f"\n 共构建 {len(FACTORS)} 个因子: {list(FACTORS.keys())}") + + +# ============================================================================= +# §2 因子预处理 Factor Preprocessing +# ----------------------------------------------------------------------------- +# 原始因子值不能直接用于分析,需要经过三步标准化预处理流程: +# Raw factor values must go through a 3-step preprocessing pipeline: +# +# Step 1 ── 截面去极值 (Cross-Sectional Winsorization) +# 在每个日期截面,将超出 [μ - 3σ, μ + 3σ] 范围的值截断。 +# At each date, clip values outside [mean - 3σ, mean + 3σ]. +# 为什么?极端异常值会主导相关系数计算,掩盖真实的因子信号。 +# Why? Outliers dominate correlation calculations and mask the true signal. +# +# Step 2 ── 截面 Z-score 标准化 (Cross-Sectional Z-score) +# z_{i,t} = (x_{i,t} - μ_t) / σ_t (在每个时间截面 t 上计算) +# z_{i,t} = (x_{i,t} - μ_t) / σ_t (computed cross-sectionally at each date t) +# 为什么?不同因子量纲不同(动量是%,波动率也是%,ILLIQ 是极小数), +# 标准化后可以直接比较和合成。 +# Why? Factors have different scales; Z-score enables fair comparison and combination. +# +# Step 3 ── 市值中性化 (Market Cap Neutralization) +# 在每个截面,对 log(市值) 做 OLS 回归,用残差替换原因子值: +# At each date, regress on log(mktcap) via OLS; use residuals as factor: +# factor_neutral_i = factor_i - (α + β × log_mktcap_i) +# 为什么?小市值效应 (Size Effect) 会污染其他因子。例如小市值股票往往同时具有 +# 高动量、高波动率、低流动性,如果不剔除市值效应,因子实际上只是 +# 在选小市值股票,而不是真正的动量/波动/流动性信号。 +# Why? Size effect contaminates other factors — small caps often have high momentum, +# high vol, and low liquidity simultaneously. Without neutralization, +# factors simply pick small caps rather than true alpha signals. +# ============================================================================= + +print("\n[§2] 因子预处理 / Factor Preprocessing...") + + +def winsorize_cross_section(factor_df: pd.DataFrame, n_std: float = 3.0) -> pd.DataFrame: + """ + 截面去极值 / Cross-sectional winsorization. + + 在每个日期(行),将超出 n_std 个标准差的值截断到边界。 + At each date (row), clip values that are more than n_std std devs from the mean. + + 向量化实现,逐行 clip / Vectorized via per-row clip applied with apply. + """ + mu = factor_df.mean(axis=1) # 每日截面均值 / daily cross-sectional mean + sig = factor_df.std(axis=1) # 每日截面标准差 / daily cross-sectional std + lower = (mu - n_std * sig).values[:, np.newaxis] # 广播形状 / broadcast shape + upper = (mu + n_std * sig).values[:, np.newaxis] + clipped = np.clip(factor_df.values, lower, upper) + return pd.DataFrame(clipped, index=factor_df.index, columns=factor_df.columns) + + +def zscore_cross_section(factor_df: pd.DataFrame) -> pd.DataFrame: + """ + 截面 Z-score 标准化 / Cross-sectional Z-score normalization. + + 使每个时间截面的因子均值=0, 标准差=1。 + Makes each time cross-section have mean=0 and std=1. + """ + mu = factor_df.mean(axis=1) # 每日截面均值 (N_DAYS,) + sig = factor_df.std(axis=1) # 每日截面标准差 (N_DAYS,) + # sub/div 沿行方向广播 / broadcast along rows + return factor_df.sub(mu, axis=0).div(sig.replace(0, np.nan), axis=0) + + +def neutralize_mktcap(factor_df: pd.DataFrame, log_mktcap: pd.DataFrame) -> pd.DataFrame: + """ + 市值中性化 / Market cap neutralization via cross-sectional OLS. + + 在每个截面,用 OLS 将因子对 log(市值) 回归,取残差作为中性化因子。 + At each cross-section, regress factor on log(mktcap) via OLS; keep residuals. + + residual_i = factor_i - (intercept + slope × log_mktcap_i) + """ + # 对齐列顺序 / Align columns + y_arr = factor_df.values.copy() # (N_DAYS, N_STOCKS) + x_arr = log_mktcap.reindex(columns=factor_df.columns).values # (N_DAYS, N_STOCKS) + + for t in range(len(factor_df)): + y = y_arr[t] + x = x_arr[t] + mask = ~(np.isnan(y) | np.isnan(x)) + if mask.sum() < 10: + continue + y_c, x_c = y[mask], x[mask] + # 手动 OLS / manual OLS (faster than scipy on small arrays) + xm, ym = x_c.mean(), y_c.mean() + denom = np.dot(x_c - xm, x_c - xm) + if denom < 1e-12: + continue + slope = np.dot(x_c - xm, y_c - ym) / denom + intercept = ym - slope * xm + y_arr[t][mask] = y_c - (intercept + slope * x_c) # 残差 / residuals + + return pd.DataFrame(y_arr, index=factor_df.index, columns=factor_df.columns) + + +# 对所有因子应用预处理流水线 / Apply preprocessing pipeline to all factors +FACTORS_PROCESSED = {} +for name, factor in FACTORS.items(): + step1 = winsorize_cross_section(factor) # ① 去极值 + step2 = zscore_cross_section(step1) # ② Z-score + step3 = neutralize_mktcap(step2, log_mktcap_df) # ③ 市值中性化 + FACTORS_PROCESSED[name] = step3 + print(f" ✓ {name:5s}: 去极值 → Z-score → 市值中性化 完成") + +print(" 预处理完成 / Preprocessing complete.") + + +# ============================================================================= +# §3 IC 分析 Information Coefficient Analysis +# ----------------------------------------------------------------------------- +# IC (Information Coefficient 信息系数) 是衡量因子预测能力的黄金标准。 +# IC is the gold standard for measuring a factor's predictive power. +# +# 定义 / Definition: +# IC_t = Spearman_Rank_Correlation( factor_{t}, forward_return_{t+H} ) +# +# 其中: +# factor_{t} = 第 t 日截面因子值(50只股票,每只一个数) +# forward_return_{t+H} = 从第 t 日起持有 H 日的未来收益(未来数据!) +# H = 21 交易日 ≈ 1 个月(最常用的持有期) +# +# 用 Spearman 秩相关而非 Pearson 线性相关的原因: +# ① 对极端值鲁棒 (robust to outliers) +# ② 衡量单调关系(不需要线性)(measures monotonic, not necessarily linear, relationship) +# +# 为什么用 Spearman rank correlation instead of Pearson? +# ① Robust to outliers +# ② Measures monotonic relationship (no linearity assumption needed) +# +# IC 评价标准 / IC benchmark: +# |IC均值| > 0.05 → 因子有效 / factor is effective +# |IC均值| > 0.10 → 因子强 / factor is strong +# ICIR > 0.50 → 因子稳定 / factor is stable +# IC>0 比率 > 55% → 方向一致性好 / directionally consistent +# +# ICIR (IC Information Ratio): +# ICIR = IC均值 / IC标准差 = IC_mean / IC_std +# 类似夏普比率,衡量"每单位波动贡献多少稳定的预测能力" +# Similar to Sharpe ratio — measures how much stable predictive power per unit of volatility +# ============================================================================= + +print("\n[§3] IC 分析 / IC Analysis...") + +FORWARD_PERIOD = 21 # 持有期 H = 21 日 ≈ 1 个月 / holding period ≈ 1 month + + +def compute_ic_series( + factor_df: pd.DataFrame, + forward_ret_df: pd.DataFrame, + holding_period: int = 21, +) -> pd.Series: + """ + 计算因子的 IC 时间序列。 + Compute the IC time series for a factor. + + 在每个日期 t,计算截面 Spearman 相关系数: + At each date t, compute cross-sectional Spearman rank correlation: + IC_t = Spearmanr( factor[t, all_stocks], forward_return[t+H, all_stocks] ) + + Parameters: + factor_df : 预处理后的因子值 (date × stock) + forward_ret_df : 收益率 DataFrame (date × stock) + holding_period : 持有期(交易日)/ holding period in days + Returns: + ic_series : IC 值时间序列 / IC time series indexed by date + """ + # shift(-H): 把 t+H 的收益值移到第 t 行,使得 fwd_returns.loc[t] = return from t to t+H + # shift(-H) aligns so that fwd_returns.loc[t] = the return earned from t to t+H + fwd_returns = forward_ret_df.shift(-holding_period) + + ic_values, ic_dates = [], [] + for date in factor_df.index[:-holding_period]: # 最后 H 行无未来收益 / no future return + f_row = factor_df.loc[date].dropna() + r_row = fwd_returns.loc[date, f_row.index].dropna() + common = f_row.index.intersection(r_row.index) + if len(common) < 10: # 至少 10 只股票 / need at least 10 stocks + continue + ic, _ = stats.spearmanr(f_row[common].values, r_row[common].values) + ic_values.append(ic) + ic_dates.append(date) + + return pd.Series(ic_values, index=ic_dates, name="IC") + + +# 逐因子计算 IC 序列 / Compute IC series for each factor +ic_series_dict = {} +for name, factor in FACTORS_PROCESSED.items(): + ic_series_dict[name] = compute_ic_series(factor, simple_ret_df, FORWARD_PERIOD) + +# ── IC 汇总统计表 / IC Summary Statistics Table ─────────────────────────────── +print(f"\n {'因子':8s} {'IC均值':>8s} {'IC标准差':>9s} {'ICIR':>8s} {'IC>0比率':>9s} 评级") +print(f" " + "─" * 64) + +ic_stats = {} +for name, ic_s in ic_series_dict.items(): + ic_mean = ic_s.mean() + ic_std = ic_s.std() + icir = ic_mean / ic_std if ic_std > 0 else 0.0 + positive_rate = (ic_s > 0).mean() + ic_stats[name] = { + "IC Mean" : ic_mean, + "IC Std" : ic_std, + "ICIR" : icir, + "Positive Rate": positive_rate, + } + # 评级: ★★=强, ★=有效, ○=弱 / rating + if abs(ic_mean) > 0.08: + rating = "★★ 强 Strong" + elif abs(ic_mean) > 0.04: + rating = "★ 有效 Effective" + else: + rating = "○ 弱 Weak" + print(f" {name:8s} {ic_mean:+8.4f} {ic_std:9.4f} {icir:+8.4f} {positive_rate:9.1%} {rating}") + +print(f" " + "─" * 64) +print(f" 评级标准: |IC均值|>0.08 ★★强 |IC均值|>0.04 ★有效 ICIR>0.5 稳定") + + +# ============================================================================= +# §4 分层回测 Quantile Return Analysis +# ----------------------------------------------------------------------------- +# 分层回测 (Quantile Analysis / Bucket Test) 是验证因子有效性的经典直观方法。 +# Quantile analysis is the classic, intuitive way to validate a factor. +# +# 操作步骤 / Procedure: +# ① 在每个调仓日 T,按因子值将全部股票从小到大排序 +# ② 等分为 Q 组(常用 Q=5 五分位) +# ③ 每组各构建等权组合 (equal-weight portfolio),持有到下次调仓日 T+21 +# ④ 记录每组收益,重复直到回测结束 +# +# ① On each rebalance date T, rank all stocks by factor value +# ② Split into Q equal-sized buckets (usually Q=5 quintiles) +# ③ Each bucket forms an equal-weight portfolio, held until next rebalance T+21 +# ④ Record each bucket's return, repeat until end of backtest +# +# 期望结果 / Expected result: +# Q5 累积收益 > Q4 > Q3 > Q2 > Q1 +# 即因子值越高,未来收益越高 —— 这是因子有效的最直观证明 +# Higher factor value → higher future return = most intuitive proof of efficacy +# ============================================================================= + +print("\n[§4] 分层回测 / Quantile Return Analysis...") + +N_QUANTILES = 5 # 五分位 / quintiles +REBAL_PERIOD = 21 # 月度调仓 / monthly rebalancing + +# 选用 ICIR 最高的因子做分层演示 / Use factor with highest |ICIR| for demo +best_factor_name = max(ic_stats, key=lambda n: abs(ic_stats[n]["ICIR"])) +best_factor = FACTORS_PROCESSED[best_factor_name] +print(f" 最强因子 (Best Factor): {best_factor_name} ICIR={ic_stats[best_factor_name]['ICIR']:+.3f}") + + +def compute_quantile_returns( + factor_df: pd.DataFrame, + price_df: pd.DataFrame, + n_quantiles: int = 5, + rebal_period: int = 21, +) -> pd.DataFrame: + """ + 分层回测:每月调仓,计算各五分位等权组合的期间收益。 + Quintile backtest: monthly rebalancing, equal-weight portfolio returns per bucket. + + Returns: + DataFrame, columns=[Q1..Q5], index=rebalance dates, + values=equal-weight return for that holding period + """ + # 调仓日序列 / Rebalance date sequence + rebal_dates = price_df.index[::rebal_period] + + bucket_returns = {f"Q{q}": [] for q in range(1, n_quantiles + 1)} + period_dates = [] + + for i in range(len(rebal_dates) - 1): + t0 = rebal_dates[i] # 建仓日 / entry date + t1 = rebal_dates[i + 1] # 平仓日 / exit date + + f_today = factor_df.loc[t0].dropna() + if len(f_today) < n_quantiles * 3: # 股票太少则跳过 / skip if too few stocks + continue + + # pd.qcut 按因子值等分为 Q 组 / split into Q equal-sized bins + labels = pd.qcut( + f_today.rank(method='first'), # 先按秩排名(避免重复值问题) + n_quantiles, + labels=[f"Q{q}" for q in range(1, n_quantiles + 1)] + ) + + # 每组的持有期收益(等权平均)/ equal-weight return per group + p0 = price_df.loc[t0] + p1 = price_df.loc[t1] + hold_ret = p1 / p0 - 1.0 + + for q in range(1, n_quantiles + 1): + stocks_in_q = labels[labels == f"Q{q}"].index + bucket_returns[f"Q{q}"].append(hold_ret[stocks_in_q].mean()) + + period_dates.append(t0) + + return pd.DataFrame(bucket_returns, index=period_dates) + + +quantile_rets = compute_quantile_returns(best_factor, prices_df, N_QUANTILES, REBAL_PERIOD) +quantile_cumret = (1 + quantile_rets).cumprod() # 累积净值 / cumulative NAV + +# 多空价差 (Long-Short Spread): Q5(多头)- Q1(空头) +ls_spread = quantile_rets["Q5"] - quantile_rets["Q1"] +ls_cumret = (1 + ls_spread).cumprod() + +periods_per_year = 252.0 / REBAL_PERIOD + +print(f"\n {'分组':8s} {'年化收益':>10s} {'累积收益':>10s}") +print(f" " + "─" * 40) +for q in range(1, N_QUANTILES + 1): + ret_s = quantile_rets[f"Q{q}"] + ann_ret = (1 + ret_s.mean()) ** periods_per_year - 1 + cum_ret = quantile_cumret[f"Q{q}"].iloc[-1] - 1 + print(f" Q{q} {ann_ret:+10.2%} {cum_ret:+10.2%}") +ann_ls = (1 + ls_spread.mean()) ** periods_per_year - 1 +print(f" L-S(Q5-Q1) {ann_ls:+9.2%} {ls_cumret.iloc[-1]-1:+10.2%}") +print(f" " + "─" * 40) + + +# ============================================================================= +# §5 因子合成 Factor Combination +# ----------------------------------------------------------------------------- +# 单个因子的预测能力有限(IC 通常只有 0.05~0.10),因子合成可以: +# Individual factors have limited predictive power (IC ≈ 0.05~0.10). +# Factor combination can: +# +# ① 提升综合 ICIR(多个信号互补,减少因子特定噪音) +# Improve composite ICIR (signals complement each other, reduce factor noise) +# ② 覆盖更多 Alpha 来源(动量+质量+流动性的综合) +# Cover more alpha sources (momentum + quality + liquidity combined) +# ③ 降低单因子的"失效期"风险(某个风格因子可能在某段时间失效) +# Reduce regime risk (individual factors can fail in certain market regimes) +# +# 方法1: 等权合成 (Equal-Weight Composite) +# composite_EQ = (z_1 + z_2 + … + z_n) / n +# 优点: 简单、稳健,不依赖历史 IC 估计 缺点: 不区分因子强弱 +# Pro: simple, robust Con: treats all factors equally regardless of efficacy +# +# 方法2: IC 加权合成 (IC-Weighted Composite) +# w_i = max(IC_mean_i, 0) / Σ max(IC_mean_j, 0) (只给正 IC 因子分配权重) +# composite_ICW = Σ w_i × z_i +# 优点: 给更强的因子更高权重 缺点: 历史 IC 可能不稳定(过拟合风险) +# Pro: higher weight to stronger factors Con: historical IC may be unstable (overfitting) +# ============================================================================= + +print("\n[§5] 因子合成 / Factor Combination...") + +# 只合成正 IC 的因子(负 IC 因子方向混乱,不宜纳入) +# Only combine factors with positive IC (negative IC factors are directionally inconsistent) +positive_ic_factors = { + name: f for name, f in FACTORS_PROCESSED.items() + if ic_stats[name]["IC Mean"] > 0 +} +print(f" IC均值为正的因子: {list(positive_ic_factors.keys())}") + +# ── 等权合成 / Equal-Weight Composite ───────────────────────────────────────── +composite_eq = sum( + f.reindex(columns=symbols).fillna(0) + for f in positive_ic_factors.values() +) / len(positive_ic_factors) + +# ── IC 加权合成 / IC-Weighted Composite ─────────────────────────────────────── +ic_weights_raw = {n: max(ic_stats[n]["IC Mean"], 0) for n in positive_ic_factors} +total_w = sum(ic_weights_raw.values()) +ic_weights = {n: w / total_w for n, w in ic_weights_raw.items()} + +composite_icw = sum( + ic_weights[n] * f.reindex(columns=symbols).fillna(0) + for n, f in positive_ic_factors.items() +) + +# 计算合成因子的 IC / Evaluate composite factor IC +ic_eq = compute_ic_series(composite_eq, simple_ret_df, FORWARD_PERIOD) +ic_icw = compute_ic_series(composite_icw, simple_ret_df, FORWARD_PERIOD) + +print(f"\n IC 权重分配 / IC Weights:") +for name, w in ic_weights.items(): + print(f" {name}: {w:.3f}") + +print(f"\n {'合成方法':22s} {'IC均值':>8s} {'ICIR':>8s}") +print(f" " + "─" * 45) +print(f" {'等权 Equal-Weight':22s} {ic_eq.mean():+8.4f} {ic_eq.mean()/ic_eq.std():+8.4f}") +print(f" {'IC加权 IC-Weighted':22s} {ic_icw.mean():+8.4f} {ic_icw.mean()/ic_icw.std():+8.4f}") + + +# ============================================================================= +# §6 多空组合 Long-Short Portfolio +# ----------------------------------------------------------------------------- +# 多空组合 (Long-Short Portfolio) 是因子策略的实际收益实现形式。 +# A long-short portfolio is how a factor strategy generates actual P&L. +# +# 构建逻辑 / Construction logic: +# 做多 (Long) = 买入因子分最高的 Top 20% 股票(预期跑赢,做多享受上涨) +# 做空 (Short) = 卖空因子分最低的 Bottom 20% 股票(预期跑输,做空赚下跌) +# 多空价差 = 多头组合收益 - 空头组合收益(无论市场涨跌都能赚钱) +# +# Long = Buy top 20% stocks by factor score (expected to outperform) +# Short = Sell short bottom 20% stocks (expected to underperform) +# L-S = Long return − Short return (market-neutral, profits in any market) +# +# 注意 A 股限制 / A-share restriction: +# A 股融券做空受到严格限制,本演示仅作为量化研究的理论演示。 +# 在港股、美股等市场中,做空非常普遍。 +# Short selling in A-shares is heavily restricted. This demo is theoretical. +# Short selling is common and practical in HK/US markets. +# ============================================================================= + +print("\n[§6] 多空组合 / Long-Short Portfolio...") + +TOP_PCTILE = 0.20 # 前后 20% / top & bottom 20% + + +def compute_long_short_portfolio( + factor_df: pd.DataFrame, + price_df: pd.DataFrame, + top_pct: float = 0.20, + rebal_period: int = 21, +) -> dict: + """ + 构建多空组合,计算每个持仓期的多头、空头、多空价差收益。 + Build a long-short portfolio; compute per-period long, short, L-S returns. + + Returns: dict with 'long', 'short', 'long_short' keys, each a pd.Series. + """ + rebal_dates = price_df.index[::rebal_period] + long_rets, short_rets, ls_rets, ret_dates = [], [], [], [] + + for i in range(len(rebal_dates) - 1): + t0, t1 = rebal_dates[i], rebal_dates[i + 1] + f_today = factor_df.loc[t0].dropna() + if len(f_today) < 10: + continue + n_top = max(1, int(len(f_today) * top_pct)) + + long_stocks = f_today.nlargest(n_top).index # Top 20% → 买入 + short_stocks = f_today.nsmallest(n_top).index # Bottom 20% → 卖空 + + p0 = price_df.loc[t0] + p1 = price_df.loc[t1] + ret = p1 / p0 - 1.0 + + lr = ret[long_stocks].mean() + sr = ret[short_stocks].mean() + long_rets.append(lr) + short_rets.append(sr) + ls_rets.append(lr - sr) # 多空价差 / long-short spread + ret_dates.append(t0) + + return { + "long" : pd.Series(long_rets, index=ret_dates, name="Long"), + "short" : pd.Series(short_rets, index=ret_dates, name="Short"), + "long_short" : pd.Series(ls_rets, index=ret_dates, name="L-S"), + } + + +def compute_max_drawdown(returns: pd.Series) -> float: + """最大回撤 / Maximum Drawdown""" + cum = (1 + returns).cumprod() + peak = cum.cummax() + return ((cum - peak) / peak).min() + + +def compute_sharpe(returns: pd.Series, ann_factor: float) -> float: + """夏普比率 / Annualized Sharpe Ratio""" + return returns.mean() / returns.std() * np.sqrt(ann_factor) if returns.std() > 0 else 0.0 + + +ls_result = compute_long_short_portfolio(composite_icw, prices_df, TOP_PCTILE, REBAL_PERIOD) +ls_equity = {k: (1 + v).cumprod() for k, v in ls_result.items()} + +ann_factor = periods_per_year +print(f"\n {'组合':14s} {'年化收益':>10s} {'夏普比率':>10s} {'最大回撤':>10s}") +print(f" " + "─" * 54) +for name, rets in ls_result.items(): + ann_ret = (1 + rets.mean()) ** ann_factor - 1 + sharpe = compute_sharpe(rets, ann_factor) + mdd = compute_max_drawdown(rets) + print(f" {name:14s} {ann_ret:+10.2%} {sharpe:+10.3f} {mdd:+10.2%}") +print(f" " + "─" * 54) + + +# ============================================================================= +# §7 因子衰减分析 Factor Decay Analysis +# ----------------------------------------------------------------------------- +# 因子衰减 (Factor Decay) 描述了因子预测能力随持有期增加而减弱的规律: +# Factor decay describes how predictive power weakens as holding period grows: +# +# 短持有期 H=1D: IC 最高(信号最新鲜,预测力最强) +# H 增大: IC 逐渐下降(信号被市场消化,噪音积累) +# H=63D(1季度): IC 接近 0(信号已基本失效) +# +# 实践意义 / Practical implications: +# ┌──────────────────┬──────────────────────────────────────────────┐ +# │ 衰减类型 │ 适合持仓频率 │ +# ├──────────────────┼──────────────────────────────────────────────┤ +# │ 快速衰减 (Fast) │ 日频或周频换仓(换手率高,成本高!) │ +# │ 慢速衰减 (Slow) │ 月频或季频换仓(成本效率更高,适合实盘) │ +# └──────────────────┴──────────────────────────────────────────────┘ +# ============================================================================= + +print("\n[§7] 因子衰减分析 / Factor Decay Analysis...") + +DECAY_HORIZONS = [1, 5, 10, 21, 42, 63] # 持有期(交易日)/ holding periods (days) + +decay_results = {} +for fname in [best_factor_name, "REV"]: # 对比 "慢衰减" vs "快衰减" 因子 + horizon_ics = [] + for h in DECAY_HORIZONS: + ic_h = compute_ic_series(FACTORS_PROCESSED[fname], simple_ret_df, h) + horizon_ics.append(ic_h.mean()) + decay_results[fname] = horizon_ics + ic_str = " ".join([f"H={h}D: {v:+.4f}" for h, v in zip(DECAY_HORIZONS, horizon_ics)]) + print(f" {fname}: {ic_str}") + + +# ============================================================================= +# §8 可视化 Visualization +# ============================================================================= + +print("\n[§8] 生成可视化图表 / Generating visualization...") + +pct_fmt = FuncFormatter(lambda x, _: f"{x:.0%}") + +fig = plt.figure(figsize=(20, 24)) +fig.suptitle( + "Alpha 因子研究演示 | Alpha Factor Research Demo\n" + "50只合成股票 × 3年日线数据 | 50 Synthetic Stocks × 3-Year Daily Data", + fontsize=14, fontweight='bold', y=0.99 +) +gs = gridspec.GridSpec(4, 3, figure=fig, hspace=0.50, wspace=0.35) + +# ── Panel 1: 股票池价格(归一化)/ Universe Prices (Normalized) ────────────── +ax1 = fig.add_subplot(gs[0, 0]) +norm_prices = prices_df / prices_df.iloc[0] # 归一化 / normalize to 1 +for sym in symbols[:12]: + ax1.plot(dates, norm_prices[sym], alpha=0.35, linewidth=0.7, color='#1f77b4') +eq_index = norm_prices.mean(axis=1) # 等权指数 / equal-weight index +ax1.plot(dates, eq_index, color='black', linewidth=2, label='等权指数 EW Index', zorder=5) +ax1.axhline(1, color='gray', linewidth=0.6, linestyle='--') +ax1.set_title("股票池价格(归一化)\nUniverse Prices (Normalized)", fontsize=10) +ax1.set_ylabel("归一化价格") +ax1.legend(fontsize=8) +ax1.tick_params(axis='x', rotation=25, labelsize=8) +ax1.xaxis.set_major_locator(plt.MaxNLocator(4)) + +# ── Panel 2: IC 时间序列(5因子)/ IC Time Series ───────────────────────────── +ax2 = fig.add_subplot(gs[0, 1:]) +ic_colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd'] +for (name, ic_s), color in zip(ic_series_dict.items(), ic_colors): + rolling_ic = ic_s.rolling(20, min_periods=5).mean() # 20日滚动均值平滑 + ax2.plot(ic_s.index, rolling_ic, label=name, color=color, linewidth=1.4) +ax2.axhline(0, color='black', linewidth=0.8, linestyle='--') +ax2.axhline(0.05, color='green', linewidth=0.8, linestyle=':', alpha=0.7) +ax2.axhline(-0.05, color='red', linewidth=0.8, linestyle=':', alpha=0.7) +ax2.set_title("因子 IC 时间序列(20日滚动均值)\nFactor IC Time Series (20D Rolling Mean)", fontsize=10) +ax2.set_ylabel("IC") +ax2.legend(loc='upper right', ncol=5, fontsize=8) +ax2.tick_params(axis='x', rotation=25, labelsize=8) +ax2.xaxis.set_major_locator(plt.MaxNLocator(5)) + +# ── Panel 3: IC 均值 & 误差棒 / IC Mean Bar Chart ───────────────────────────── +ax3 = fig.add_subplot(gs[1, 0]) +names_list = list(ic_stats.keys()) +ic_means = [ic_stats[n]["IC Mean"] for n in names_list] +ic_stds = [ic_stats[n]["IC Std"] for n in names_list] +bar_colors3 = ['#2ca02c' if v > 0 else '#d62728' for v in ic_means] +ax3.bar(names_list, ic_means, yerr=ic_stds, capsize=5, color=bar_colors3, + alpha=0.8, error_kw={'linewidth': 1.5, 'ecolor': 'black'}) +ax3.axhline(0, color='black', linewidth=0.8) +ax3.axhline(0.05, color='green', linewidth=1.2, linestyle='--', alpha=0.7) +ax3.axhline(-0.05, color='red', linewidth=1.2, linestyle='--', alpha=0.7) +ax3.set_title("因子 IC 均值(误差棒=±1σ)\nIC Mean ± 1σ", fontsize=10) +ax3.set_ylabel("IC 均值") +ax3.tick_params(axis='x', labelsize=9) + +# ── Panel 4: ICIR 柱状图 / ICIR Bar Chart ────────────────────────────────────── +ax4 = fig.add_subplot(gs[1, 1]) +icirs = [ic_stats[n]["ICIR"] for n in names_list] +bar_colors4 = ['#2ca02c' if v > 0 else '#d62728' for v in icirs] +ax4.bar(names_list, icirs, color=bar_colors4, alpha=0.8) +ax4.axhline(0, color='black', linewidth=0.8) +ax4.axhline(0.5, color='green', linewidth=1.2, linestyle='--', alpha=0.7, label='ICIR=0.5') +ax4.axhline(-0.5, color='red', linewidth=1.2, linestyle='--', alpha=0.7) +ax4.set_title("因子 ICIR\nFactor ICIR (Mean IC / Std IC)", fontsize=10) +ax4.set_ylabel("ICIR") +ax4.legend(fontsize=8) +ax4.tick_params(axis='x', labelsize=9) + +# ── Panel 5: 因子相关矩阵 / Factor Correlation Matrix ───────────────────────── +ax5 = fig.add_subplot(gs[1, 2]) +# 用各因子的截面均值时序来衡量因子间相关性 / Use cross-sectional mean timeseries +factor_ts = pd.DataFrame({n: f.mean(axis=1) for n, f in FACTORS_PROCESSED.items()}) +corr_matrix = factor_ts.corr() +im5 = ax5.imshow(corr_matrix.values, cmap='RdYlGn', vmin=-1, vmax=1, aspect='auto') +ax5.set_xticks(range(len(names_list))) +ax5.set_yticks(range(len(names_list))) +ax5.set_xticklabels(names_list, fontsize=9) +ax5.set_yticklabels(names_list, fontsize=9) +for i in range(len(names_list)): + for j in range(len(names_list)): + ax5.text(j, i, f"{corr_matrix.values[i, j]:.2f}", + ha='center', va='center', fontsize=8, + color='black' if abs(corr_matrix.values[i, j]) < 0.6 else 'white') +plt.colorbar(im5, ax=ax5) +ax5.set_title("因子相关矩阵\nFactor Correlation Matrix", fontsize=10) + +# ── Panel 6: 分层回测累积净值 / Quantile Cumulative Returns ─────────────────── +ax6 = fig.add_subplot(gs[2, 0]) +q_colors = plt.cm.RdYlGn(np.linspace(0.05, 0.95, N_QUANTILES)) +for q in range(1, N_QUANTILES + 1): + ax6.plot(quantile_cumret.index, quantile_cumret[f"Q{q}"], + label=f"Q{q}", color=q_colors[q - 1], linewidth=1.5) +ax6.plot(ls_cumret.index, ls_cumret, label='L-S', color='black', linewidth=2, linestyle='--') +ax6.axhline(1, color='gray', linewidth=0.6, linestyle=':') +ax6.set_title(f"分层回测({best_factor_name})\nQuantile Returns ({best_factor_name})", fontsize=10) +ax6.set_ylabel("累积净值") +ax6.legend(ncol=3, fontsize=8) +ax6.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{x:.1f}x")) +ax6.tick_params(axis='x', rotation=25, labelsize=8) +ax6.xaxis.set_major_locator(plt.MaxNLocator(4)) + +# ── Panel 7: 分位年化收益柱状图 / Quintile Annualized Return Bar ─────────────── +ax7 = fig.add_subplot(gs[2, 1]) +ann_q_rets = [(1 + quantile_rets[f"Q{q}"].mean()) ** periods_per_year - 1 + for q in range(1, N_QUANTILES + 1)] +bar_colors7 = plt.cm.RdYlGn(np.linspace(0.05, 0.95, N_QUANTILES)) +bars7 = ax7.bar([f"Q{q}" for q in range(1, N_QUANTILES + 1)], ann_q_rets, color=bar_colors7) +ax7.axhline(0, color='black', linewidth=0.8) +ax7.set_title("各分位组年化收益\nAnnualized Return per Quintile", fontsize=10) +ax7.set_ylabel("年化收益 / Ann. Return") +ax7.yaxis.set_major_formatter(pct_fmt) +ax7.tick_params(axis='x', labelsize=9) +for bar, val in zip(bars7, ann_q_rets): + ax7.text(bar.get_x() + bar.get_width() / 2, val, + f"{val:.1%}", ha='center', + va='bottom' if val >= 0 else 'top', fontsize=8) + +# ── Panel 8: 多空组合净值曲线 / Long-Short Equity Curve ─────────────────────── +ax8 = fig.add_subplot(gs[2, 2]) +ax8.plot(ls_equity['long_short'].index, ls_equity['long_short'].values, + color='purple', linewidth=2, label='多空 L-S') +ax8.plot(ls_equity['long'].index, ls_equity['long'].values, + color='#2ca02c', linewidth=1.5, linestyle='--', label='多头 Long', alpha=0.8) +ax8.plot(ls_equity['short'].index, ls_equity['short'].values, + color='#d62728', linewidth=1.5, linestyle='--', label='空头 Short', alpha=0.8) +ax8.axhline(1, color='gray', linewidth=0.6, linestyle=':') +ax8.set_title("多空组合净值曲线\nLong-Short Portfolio NAV", fontsize=10) +ax8.set_ylabel("净值 / NAV") +ax8.legend(fontsize=8) +ax8.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{x:.1f}x")) +ax8.tick_params(axis='x', rotation=25, labelsize=8) +ax8.xaxis.set_major_locator(plt.MaxNLocator(4)) + +# ── Panel 9: 因子衰减曲线 / Factor Decay Curve ──────────────────────────────── +ax9 = fig.add_subplot(gs[3, :]) +decay_colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728'] +horizon_labels = [f"{h}D" for h in DECAY_HORIZONS] +for (fname, decay_ics), color in zip(decay_results.items(), decay_colors): + ax9.plot(range(len(DECAY_HORIZONS)), decay_ics, + 'o-', label=fname, color=color, linewidth=2, markersize=7) + for i, (h, v) in enumerate(zip(DECAY_HORIZONS, decay_ics)): + ax9.annotate(f"{v:+.3f}", (i, v), textcoords="offset points", + xytext=(0, 8), ha='center', fontsize=8, color=color) +ax9.axhline(0, color='black', linewidth=0.8, linestyle='--') +ax9.axhline(0.05, color='green', linewidth=0.8, linestyle=':', alpha=0.7, label='IC=0.05 参考线') +ax9.set_title( + "因子衰减曲线 Factor Decay Curve\n" + "IC 均值随持有期延长的衰减 | How Mean IC Decays with Longer Holding Period", + fontsize=10 +) +ax9.set_xlabel("持有期 / Holding Period") +ax9.set_ylabel("IC 均值 / Mean IC") +ax9.set_xticks(range(len(DECAY_HORIZONS))) +ax9.set_xticklabels(horizon_labels) +ax9.legend(fontsize=9, ncol=4) +ax9.grid(True, alpha=0.3) +ax9.set_xlim(-0.3, len(DECAY_HORIZONS) - 0.7) + +plt.savefig("alpha_factor_demo.png", dpi=150, bbox_inches='tight') +print(f" 图表已保存: alpha_factor_demo.png / Chart saved: alpha_factor_demo.png") + +print("\n" + "=" * 70) +print(" 演示完成! Demo Complete!") +print(" 输出: alpha_factor_demo.png") +print("=" * 70)