返回 Expert 笔记
Expert Day 92

Factor Models(因子模型与加密多因子)

CAPM、Fama-French 3/5 因子、Carhart、加密因子(Liu-Tsyvinski-Wu)、Barra 风险模型

2026-08-01
Phase 2 - 统计套利与Alpha Research (Day 89-102)
量化策略因子模型FamaFrench多因子

日期: 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%
CSMBSmall - 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$:因子解释比例

策略评估流程

  1. 跑因子回归
  2. 检验 $\alpha$ t-stat > 2 (95% 显著)
  3. 看 $\beta$ 是否符合预期
  4. 看 $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+

解法

  1. PCA 正交化
  2. Gram-Schmidt 正交化
  3. 检验 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 MKT2.5%28%0.31
Crypto MOM2.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 特殊
MarketPerp 复合指数DEX TVL 加权指数TVL 因子(DeFi 原生)
Momentum价格动量同 + on-chain momentum链上活跃度 momentum
Size市值市值 + TVLTVL 是 DeFi-native size proxy
Liquidityorder book depthAMM 流动性深度LP token 流动性
Quality难定义revenue/TVL ratioDeFi 协议盈利能力
Network不适用活跃地址、交易笔数链上原生因子

DeFi 原生因子

  1. Revenue factor:协议手续费收入 / TVL 排序,high revenue 跑赢
  2. TVL momentum:TVL 增长率,与代币价格强相关
  3. Holder concentration:top 100 holder 占比,低集中度跑赢
  4. Active developer factor:GitHub commits / week

六、风险管理

6.1 因子组合风控

风险控制
因子拥挤监测因子 valuation spread
因子衰减滚动 IC 监测,IC < 0.02 警报
因子相关性突变30-day rolling corr,> 0.7 减仓
Tail riskfactor-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

因子检验流程

  1. IC(信息系数):cor(signal, future_return),> 0.05 显著
  2. Rank IC:Spearman 相关,更稳健
  3. t-stat:因子组合月收益 t-stat > 2
  4. 衰减分析:IC 在 1d/5d/10d/30d 衰减曲线
  5. 跨子集验证:分大/中/小市值看 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:加密因子和股票因子有什么主要差异?

  1. Size 反转:股票 small > big,加密 big > small(网络效应)
  2. Value 不适用:加密无 EPS、book value,必须用替代指标(TVL/MC)
  3. Momentum 更强:行为偏差更大、信息扩散更慢
  4. Volatility 极端:vol 是股票的 4-5 倍,risk model 必须 regime-aware
  5. Network factor 独有:链上活跃度、developer activity
  6. 流动性结构不同:CEX vs DEX 流动性割裂

Q3:如何处理因子相关性?

  1. 诊断:计算相关矩阵 + VIF
  2. 正交化
    • PCA:得到正交主成分(损失可解释性)
    • Gram-Schmidt:先 MKT 再 MOM 再...,逐步剔除已解释部分
    • 残差因子:把每个因子对前置因子回归取残差
  3. 删除:相关性 > 0.8 的两个因子保留更稳健的
  4. 集成:相关因子的等权组合可能更稳

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、过拟合检测。今天的因子需要严格回测才能可信,明天搭建自己的回测框架。