Factor Models(因子模型与加密多因子)
CAPM、Fama-French 3/5 因子、Carhart、加密因子(Liu-Tsyvinski-Wu)、Barra 风险模型
日期: 2026-08-01 方向: 量化 / 统计套利 / Alpha 阶段: Phase 2 - 统计套利与Alpha Research (Day 89-102) 标签: #量化策略 #因子模型 #FamaFrench #多因子
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | CAPM、Fama-French 3/5 因子、Carhart、加密因子(Liu-Tsyvinski-Wu)、Barra 风险模型 |
| 实操 | 用真实数据构建加密 3 因子模型(Market、Momentum、Volatility),跑因子回归 |
| 产出 | factors.py — 因子构造 + 风险模型 + Alpha 归因 |
一、理论与模型
1.1 CAPM 与多因子演进
CAPM: $$ E[r_i] - r_f = \beta_i (E[r_m] - r_f) $$
Fama-French 3 Factor: $$ r_i - r_f = \alpha + \beta_M \text{MKT} + \beta_S \text{SMB} + \beta_V \text{HML} + \epsilon $$
- MKT = 市场超额收益
- SMB = Small Minus Big(规模因子)
- HML = High Minus Low(价值因子)
Carhart 4 Factor:加上 MOM(动量) Fama-French 5 Factor:加上 RMW(盈利能力)和 CMA(投资保守)
1.2 加密因子(Liu-Tsyvinski-Wu 2022)
加州大学经济系著名论文 "Common Risk Factors in Cryptocurrency":
| 因子 | 构造 | 月度溢价 | 解释力 |
|---|---|---|---|
| CMKT | 市值加权加密市场 | 月化 ~3% | 高 |
| CSMB | Small - Big(市值排序) | ~2% | 中 |
| CMOM | 过去 7-day return 排序 | ~3-4% | 高 |
加上扩展因子:
- Volatility:低波动率溢价
- Liquidity:低流动性溢价
- Network:链上活跃度因子
1.3 因子构造规范
横截面 z-score: $$ z_{i,t} = \frac{x_{i,t} - \bar x_t}{\sigma_t(x)} $$
Rank-based:把分数转为 [0, 1] 排名,避免离群值。
因子组合(factor portfolio):
- 按因子值排序
- 做多 top quintile,做空 bottom quintile
- 等权或市值加权
- 因子收益 = portfolio 收益 - rf
1.4 因子回归与 Alpha 归因
$$ r_i^{\text{strategy}} = \alpha + \sum_k \beta_k F_k + \epsilon $$
- $\alpha$:策略真正的 alpha(factor-adjusted)
- $\beta_k$:暴露
- $R^2$:因子解释比例
策略评估流程:
- 跑因子回归
- 检验 $\alpha$ t-stat > 2 (95% 显著)
- 看 $\beta$ 是否符合预期
- 看 $R^2$:太高 (> 0.9) 意味着策略只是 factor exposure 不是 alpha
1.5 Barra 风险模型(结构化)
不同于 FF 的统计因子,Barra 模型用预定义的暴露:
$$ r_i = \sum_k X_{i,k} f_k + u_i $$
- $X_{i,k}$:资产 i 对因子 k 的暴露(已知)
- $f_k$:因子收益(截面回归求得)
- $u_i$:特异收益
协方差矩阵: $$ \Sigma = X F X^T + \Delta $$
- $X$:暴露矩阵 N×K
- $F$:因子协方差 K×K
- $\Delta$:特异方差对角阵 N×N
加密 Barra 应用:
- $X$ 包括 sector(DeFi/L1/L2/Meme)、行业、流动性、波动率、市值
- 比 FF 风格更稳定,适合机构组合管理
二、直觉与陷阱
陷阱 1:因子重叠(Multicollinearity)
加密因子之间高度相关:
- Momentum 与 Volatility 相关性 0.6+
- Size 与 Liquidity 相关性 0.7+
解法:
- PCA 正交化
- Gram-Schmidt 正交化
- 检验 VIF(方差膨胀因子),> 10 删除一个
陷阱 2:因子衰减
经典 Value 因子在 2010-2020 年股票市场衰减。加密因子更快衰减:
- Momentum 因子 2014-2017 年化 60%
- 2022-2024 年化 < 20%
解法:实时监测因子 IC(信息系数);用集成因子(多个 lookback)。
陷阱 3:data mining
测 100 个候选因子总能找到一个 t-stat > 2 的(5% 显著性下期望就有 5 个)。 解法:
- Bonferroni / FDR 校正
- 经济直觉先行(先有理论,再验证)
- 跨地理 / 资产类别验证
陷阱 4:横截面太小
加密初期资产池只有 50-100 个流动好的,比股票(数千)小得多。 问题:top/bottom decile 只有 5-10 个,特异性噪声大。 解法:用 quartile(top 25%)而非 decile;增加资产池(含小币需考虑 capacity)。
陷阱 5:rebalance 频率
- 月度 rebalance:因子衰减损失大(加密信号衰减快)
- 日度:交易成本高
- 经验:周度 rebalance + 内部 throttling(变化 < 阈值不动)
陷阱 6:因子定义的研究者自由度
"动量因子"可以是:
- 1m return
- 12-1 return(跳过 1 月)
- risk-adjusted return(÷ vol)
- 平均月收益 / 月度收益标准差
不同定义结果可能差几倍。解法:固定一个标准定义,pre-register;事后改定义就是 p-hacking。
三、代码实现
3.1 加密多因子模型
# factors.py
"""
Crypto Multi-Factor Model
- Factor construction (Market, Momentum, Volatility, Size, Liquidity)
- Factor portfolio returns
- Cross-sectional regression
- Alpha attribution
"""
import numpy as np
import pandas as pd
import statsmodels.api as sm
from typing import Dict, List
# ----------------------------- 因子构造 -----------------------------
class CryptoFactorBuilder:
"""构建加密市场因子"""
@staticmethod
def market_factor(returns_df: pd.DataFrame, mcap_df: pd.DataFrame = None) -> pd.Series:
"""市场因子(市值加权)"""
if mcap_df is None:
return returns_df.mean(axis=1)
weights = mcap_df.div(mcap_df.sum(axis=1), axis=0)
return (returns_df * weights.shift(1)).sum(axis=1)
@staticmethod
def momentum_factor(returns_df: pd.DataFrame, lookback: int = 90,
skip: int = 7, top_pct: float = 0.3) -> pd.Series:
"""动量因子(top - bottom return)"""
cum = (1 + returns_df).rolling(lookback).apply(np.prod) - 1
signal = cum.shift(skip)
factor_ret = pd.Series(index=returns_df.index, dtype=float)
for date in signal.index:
scores = signal.loc[date].dropna()
if len(scores) < 10:
continue
n_top = max(1, int(len(scores) * top_pct))
n_bot = max(1, int(len(scores) * top_pct))
top = scores.nlargest(n_top).index
bot = scores.nsmallest(n_bot).index
top_ret = returns_df.loc[date, top].mean()
bot_ret = returns_df.loc[date, bot].mean()
factor_ret.loc[date] = top_ret - bot_ret
return factor_ret
@staticmethod
def volatility_factor(returns_df: pd.DataFrame, lookback: int = 30,
top_pct: float = 0.3) -> pd.Series:
"""低波动溢价(low - high vol)"""
vol = returns_df.rolling(lookback).std()
factor_ret = pd.Series(index=returns_df.index, dtype=float)
for date in vol.index:
scores = vol.loc[date].dropna()
if len(scores) < 10:
continue
n = int(len(scores) * top_pct)
low = scores.nsmallest(n).index
high = scores.nlargest(n).index
factor_ret.loc[date] = (returns_df.loc[date, low].mean() -
returns_df.loc[date, high].mean())
return factor_ret
@staticmethod
def size_factor(returns_df: pd.DataFrame, mcap_df: pd.DataFrame,
top_pct: float = 0.3) -> pd.Series:
"""规模因子(small - big)"""
factor_ret = pd.Series(index=returns_df.index, dtype=float)
for date in mcap_df.index:
scores = mcap_df.loc[date].dropna()
if len(scores) < 10:
continue
n = int(len(scores) * top_pct)
small = scores.nsmallest(n).index
big = scores.nlargest(n).index
factor_ret.loc[date] = (returns_df.loc[date, small].mean() -
returns_df.loc[date, big].mean())
return factor_ret
@staticmethod
def liquidity_factor(returns_df: pd.DataFrame, volume_df: pd.DataFrame,
lookback: int = 30, top_pct: float = 0.3) -> pd.Series:
"""流动性因子(low - high turnover)— 流动性溢价"""
avg_vol = volume_df.rolling(lookback).mean()
factor_ret = pd.Series(index=returns_df.index, dtype=float)
for date in avg_vol.index:
scores = avg_vol.loc[date].dropna()
if len(scores) < 10:
continue
n = int(len(scores) * top_pct)
illiq = scores.nsmallest(n).index
liq = scores.nlargest(n).index
factor_ret.loc[date] = (returns_df.loc[date, illiq].mean() -
returns_df.loc[date, liq].mean())
return factor_ret
# ----------------------------- 因子回归 -----------------------------
def factor_regression(asset_returns: pd.Series,
factor_df: pd.DataFrame,
rf: float = 0.0) -> dict:
"""
回归 r_i - rf = α + Σ β_k * F_k + ε
"""
excess_ret = asset_returns - rf
aligned = pd.concat([excess_ret, factor_df], axis=1).dropna()
if len(aligned) < 30:
return {'error': 'insufficient data'}
y = aligned.iloc[:, 0]
X = sm.add_constant(aligned.iloc[:, 1:])
model = sm.OLS(y, X).fit()
return {
'alpha': model.params.iloc[0],
'alpha_t': model.tvalues.iloc[0],
'alpha_pvalue': model.pvalues.iloc[0],
'betas': model.params.iloc[1:].to_dict(),
'beta_tstats': model.tvalues.iloc[1:].to_dict(),
'r_squared': model.rsquared,
'n_obs': len(aligned),
}
# ----------------------------- 横截面回归(Fama-MacBeth) -----------------------------
def fama_macbeth(returns_panel: pd.DataFrame,
exposure_panel: Dict[str, pd.DataFrame]) -> dict:
"""
Fama-MacBeth 两步法
Step 1: 每期截面回归 r_i = γ_0 + Σ γ_k X_{i,k}
Step 2: 时间序列均值 + 标准误
"""
common_idx = returns_panel.index
for k, X in exposure_panel.items():
common_idx = common_idx.intersection(X.index)
factor_names = list(exposure_panel.keys())
gamma_history = pd.DataFrame(index=common_idx,
columns=['intercept'] + factor_names,
dtype=float)
for date in common_idx:
ret = returns_panel.loc[date].dropna()
X_mat = pd.DataFrame({k: exposure_panel[k].loc[date] for k in factor_names})
merged = pd.concat([ret.rename('ret'), X_mat], axis=1).dropna()
if len(merged) < 10:
continue
y = merged['ret']
X = sm.add_constant(merged.iloc[:, 1:])
try:
res = sm.OLS(y, X).fit()
gamma_history.loc[date] = res.params.values
except Exception:
continue
# Step 2: 时间序列分析
gamma_history = gamma_history.dropna()
means = gamma_history.mean()
stds = gamma_history.std()
n = len(gamma_history)
t_stats = means / (stds / np.sqrt(n))
p_values = 2 * (1 - sm.stats.stattools.norm.cdf(np.abs(t_stats)))
return {
'mean_factor_returns': means,
't_stats': t_stats,
'p_values': p_values,
'n_periods': n,
'gamma_history': gamma_history,
}
# ----------------------------- 因子相关性诊断 -----------------------------
def factor_diagnostics(factor_df: pd.DataFrame) -> dict:
corr = factor_df.corr()
# VIF
vifs = {}
for col in factor_df.columns:
y = factor_df[col]
X = sm.add_constant(factor_df.drop(columns=[col]))
df_ = pd.concat([y, X], axis=1).dropna()
if len(df_) < 30:
continue
res = sm.OLS(df_.iloc[:, 0], df_.iloc[:, 1:]).fit()
vifs[col] = 1 / (1 - res.rsquared) if res.rsquared < 1 else np.inf
# 因子的 Sharpe
sharpes = (factor_df.mean() / factor_df.std() * np.sqrt(365)).to_dict()
return {
'correlation': corr,
'vif': vifs,
'sharpe': sharpes,
}
# ----------------------------- 主程序 -----------------------------
if __name__ == '__main__':
import requests
def fetch_klines(symbol, days=500):
r = requests.get('https://api.binance.com/api/v3/klines',
params={'symbol': symbol, 'interval': '1d', 'limit': days})
df = pd.DataFrame(r.json(), columns=['ot','o','h','l','c','v','ct',
'qav','t','tb','tq','i'])
df['ct'] = pd.to_datetime(df['ct'], unit='ms')
df['c'] = df['c'].astype(float)
df['v'] = df['v'].astype(float)
return df.set_index('ct')[['c', 'v']]
symbols = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'AVAXUSDT',
'MATICUSDT', 'LINKUSDT', 'UNIUSDT', 'AAVEUSDT', 'ARBUSDT']
print("Fetching data...")
prices, volumes = {}, {}
for s in symbols:
try:
d = fetch_klines(s, 500)
prices[s] = d['c']
volumes[s] = d['v']
except Exception as e:
print(f" Failed {s}: {e}")
price_df = pd.DataFrame(prices).fillna(method='ffill')
vol_df = pd.DataFrame(volumes).fillna(method='ffill')
ret_df = price_df.pct_change()
mcap_proxy = price_df * vol_df # 用价格×成交量近似市值
# 因子构造
builder = CryptoFactorBuilder()
factors = pd.DataFrame({
'MKT': builder.market_factor(ret_df),
'MOM': builder.momentum_factor(ret_df, lookback=30, skip=2),
'LVOL': builder.volatility_factor(ret_df, lookback=30),
'SMB': builder.size_factor(ret_df, mcap_proxy),
'ILLIQ': builder.liquidity_factor(ret_df, vol_df, lookback=30),
}).dropna()
print("\n因子诊断")
diag = factor_diagnostics(factors)
print("Correlation:")
print(diag['correlation'].round(3))
print("\nVIF:")
for k, v in diag['vif'].items():
print(f" {k}: {v:.2f}")
print("\nFactor Sharpes:")
for k, v in diag['sharpe'].items():
print(f" {k}: {v:.3f}")
# 资产因子回归(以 SOL 为例)
print("\nSOL 因子回归")
sol_ret = ret_df['SOLUSDT']
res = factor_regression(sol_ret, factors)
print(f" Alpha (annualized): {res['alpha']*365:.2%}")
print(f" Alpha t-stat: {res['alpha_t']:.2f}")
print(f" R-squared: {res['r_squared']:.3f}")
print(f" Betas: {res['betas']}")
四、真实数据/案例
案例 1:Liu-Tsyvinski-Wu 论文的核心结论
| 因子 | 1m 横截面溢价 | 月化波动 | Sharpe |
|---|---|---|---|
| Crypto MKT | 2.5% | 28% | 0.31 |
| Crypto MOM | 2.0% | 22% | 0.32 |
| Crypto SIZE | -1.5% (rev) | 19% | -0.27 |
结论:动量因子在加密更显著,规模因子反向(大币更强,与股票相反,因为加密大币代表网络效应而非估值偏低)。
案例 2:DeFi summer 因子失效(2020-2021)
2020 年 6 月 DeFi summer 期间:
- Momentum 因子:仍有效(强趋势)
- Size 因子:完全反转(小币 YFI 暴涨 1000+%,大币滞涨)
- Volatility 因子:低 vol 溢价反向(高 vol 山寨币暴涨)
教训:因子在 narrative 转换期会发生反转。需要 regime-aware 因子模型。
案例 3:2022 加密熊市的因子表现
| 因子 | 2022 全年收益 |
|---|---|
| Crypto MKT | -65% |
| Crypto MOM | -25%(动量崩溃) |
| Crypto LVOL | +5%(低波动跑赢) |
| Crypto SIZE | +3%(大币跑赢) |
低波动和大币因子在熊市提供保护,与股票市场相似(flight to quality)。
案例 4:Barra 加密风险模型的机构应用
Vault17(伪名机构)使用类 Barra 模型:
- 23 个因子(含 sector、链、流动性、ESG)
- 因子协方差用 EWMA λ=0.94 估计
- 用于组合优化(min variance s.t. expected return)
实证:相比等权基准,年化收益 +3%,回撤减半。
五、CEX vs DEX 策略差异
| 因子 | CEX 实施 | DEX 实施 | DeFi 特殊 |
|---|---|---|---|
| Market | Perp 复合指数 | DEX TVL 加权指数 | TVL 因子(DeFi 原生) |
| Momentum | 价格动量 | 同 + on-chain momentum | 链上活跃度 momentum |
| Size | 市值 | 市值 + TVL | TVL 是 DeFi-native size proxy |
| Liquidity | order book depth | AMM 流动性深度 | LP token 流动性 |
| Quality | 难定义 | revenue/TVL ratio | DeFi 协议盈利能力 |
| Network | 不适用 | 活跃地址、交易笔数 | 链上原生因子 |
DeFi 原生因子:
- Revenue factor:协议手续费收入 / TVL 排序,high revenue 跑赢
- TVL momentum:TVL 增长率,与代币价格强相关
- Holder concentration:top 100 holder 占比,低集中度跑赢
- Active developer factor:GitHub commits / week
六、风险管理
6.1 因子组合风控
| 风险 | 控制 |
|---|---|
| 因子拥挤 | 监测因子 valuation spread |
| 因子衰减 | 滚动 IC 监测,IC < 0.02 警报 |
| 因子相关性突变 | 30-day rolling corr,> 0.7 减仓 |
| Tail risk | factor-level VaR + 总组合 stress test |
6.2 因子组合权重
简单等权 vs Mean-Variance:
# Risk parity 因子组合
weights = (1 / factor_vols)
weights /= weights.sum()
6.3 容量约束
每个因子的容量 ≈ universe 总流动性 × 因子相关 turnover:
| 因子 | 加密容量估计 |
|---|---|
| MKT | $10B+ |
| MOM | $1-3B |
| SIZE | $300M-1B |
| ILLIQ | $50-200M |
七、关键速查
因子构造模板
def factor_portfolio(signal: pd.DataFrame, returns: pd.DataFrame, top_q=0.3):
"""通用因子组合构造(zero-cost long-short)"""
rank = signal.rank(axis=1, pct=True)
long_mask = rank >= 1 - top_q
short_mask = rank <= top_q
long_ret = (returns * long_mask).sum(axis=1) / long_mask.sum(axis=1)
short_ret = (returns * short_mask).sum(axis=1) / short_mask.sum(axis=1)
return long_ret - short_ret
因子检验流程
- IC(信息系数):cor(signal, future_return),> 0.05 显著
- Rank IC:Spearman 相关,更稳健
- t-stat:因子组合月收益 t-stat > 2
- 衰减分析:IC 在 1d/5d/10d/30d 衰减曲线
- 跨子集验证:分大/中/小市值看 IC 是否一致
加密 vs 股票因子对比
| 因子 | 股票 | 加密 |
|---|---|---|
| Value | 强 | 弱(PE 等指标不适用) |
| Momentum | 中等 | 强 |
| Size | 小盘溢价 | 大盘溢价(网络效应) |
| Quality | 强(ROE) | 中(revenue/TVL) |
| Volatility | 低 vol 溢价 | 同方向 |
| Liquidity | 流动性溢价 | 强 |
八、面试题
Q1:你怎么验证一个因子是真正的 alpha 而不是 risk premium?
答:
- 真 alpha 在多个 regime 都存在(牛/熊/横盘)
- 真 alpha 不被已知因子解释(残差 alpha 显著)
- 真 alpha 有经济直觉(行为偏差 / 市场摩擦)
- 真 alpha 容量有限(被套利消失),随 AUM 增加衰减
- 真 alpha 在样本外稳定(out-of-sample Sharpe / in-sample > 0.5)
Q2:加密因子和股票因子有什么主要差异?
答:
- Size 反转:股票 small > big,加密 big > small(网络效应)
- Value 不适用:加密无 EPS、book value,必须用替代指标(TVL/MC)
- Momentum 更强:行为偏差更大、信息扩散更慢
- Volatility 极端:vol 是股票的 4-5 倍,risk model 必须 regime-aware
- Network factor 独有:链上活跃度、developer activity
- 流动性结构不同:CEX vs DEX 流动性割裂
Q3:如何处理因子相关性?
答:
- 诊断:计算相关矩阵 + VIF
- 正交化:
- PCA:得到正交主成分(损失可解释性)
- Gram-Schmidt:先 MKT 再 MOM 再...,逐步剔除已解释部分
- 残差因子:把每个因子对前置因子回归取残差
- 删除:相关性 > 0.8 的两个因子保留更稳健的
- 集成:相关因子的等权组合可能更稳
Q4:Fama-MacBeth 和时间序列回归的区别?
答:
- 时间序列回归:固定 i,对 r_{i,t} = α + Σ β X_{k,t} + ε 跑回归。每个资产一组 β
- Fama-MacBeth:每期截面回归得到 γ_t,再对 γ 序列做时间均值
- 用途:
- 时间序列:评估单一策略的 alpha 和 risk exposure
- Fama-MacBeth:评估因子风险溢价(这个因子真的得到补偿吗?)
- 加密应用:FM 用来确认因子定价;TS 回归用来对策略归因
Q5:因子衰减怎么发现和应对?
答:
- 发现:
- 滚动 6m IC,看是否单调下降
- 因子组合滚动 Sharpe < 0.3 持续 3 月
- factor portfolio P/E(自身估值)创新高(因子拥挤)
- 应对:
- 暂停或减半仓
- 寻找替代因子(信号衍生)
- 进入更小流动性的子集(小币 momentum 仍有效)
- 或转向高频版本(intraday momentum)
- 加密案例:2018-2019 简单 momentum 衰减,但加 vol-target + 趋势过滤后再次有效
明日预告
Day 93: Backtesting 框架 — vectorbt vs backtrader、避免 look-ahead、walk-forward、过拟合检测。今天的因子需要严格回测才能可信,明天搭建自己的回测框架。