Files
quanxiel/alpha/factors.py
T
2026-07-31 21:23:35 +08:00

256 lines
9.2 KiB
Python

"""
因子研究模块 — 因子定义、计算、注册与 IC/分层分析
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Callable, Dict, List, Optional, Tuple
import numpy as np
import pandas as pd
from scipy import stats
# ---------------------------------------------------------------------------
# 因子注册表
# ---------------------------------------------------------------------------
class FactorRegistry:
"""因子注册表,用于管理和发现所有因子"""
_factors: Dict[str, type] = {}
@classmethod
def register(cls, factor_cls: type):
"""注册因子类"""
name = factor_cls.__name__
cls._factors[name] = factor_cls
return factor_cls
@classmethod
def get(cls, name: str) -> Optional[type]:
"""根据名称获取因子类"""
return cls._factors.get(name)
@classmethod
def list_all(cls) -> List[str]:
"""列出所有已注册因子"""
return sorted(cls._factors.keys())
@classmethod
def create(cls, name: str, **kwargs):
"""根据名称创建因子实例"""
factor_cls = cls._factors.get(name)
if factor_cls is None:
raise KeyError(f"因子 '{name}' 未注册,可用: {cls.list_all()}")
return factor_cls(**kwargs)
# ---------------------------------------------------------------------------
# 因子基类
# ---------------------------------------------------------------------------
class BaseFactor(ABC):
"""因子抽象基类"""
name: str = "BaseFactor"
category: str = "unknown" # technical / fundamental / alternative
def __init__(self, **params):
self.params = params
@abstractmethod
def compute(self, data: pd.DataFrame) -> pd.Series:
"""计算因子值,返回 Series(index=timestamp) 或多索引 Series"""
...
def __repr__(self):
return f"{self.name}({self.params})"
def neutralize(self,
factor: pd.Series,
group: pd.Series,
market_cap: pd.Series) -> pd.Series:
"""
因子中性化:对市值和行业做正交化处理。
factor : 原始因子值
group : 行业分组
market_cap : 市值
返回中性化后的因子值。
"""
# 创建虚拟变量并拟合线性模型,取残差作为中性化因子
df = pd.DataFrame({
"factor": factor,
"log_mcap": np.log(market_cap.clip(lower=1)),
})
dummies = pd.get_dummies(group, prefix="group")
X = pd.concat([df["log_mcap"], dummies], axis=1)
# 对齐索引
common_idx = factor.index.intersection(X.dropna().index)
X = X.loc[common_idx]
y = factor.loc[common_idx]
# OLS 回归取残差
beta = np.linalg.lstsq(X.values, y.values, rcond=None)[0]
pred = X.values @ beta
residual = y.values - pred
neutralized = pd.Series(residual, index=common_idx, name=factor.name)
return neutralized
# ---------------------------------------------------------------------------
# 技术面因子
# ---------------------------------------------------------------------------
@FactorRegistry.register
class TechnicalFactor(BaseFactor):
"""通用技术面因子 — 支持自定义参数组合"""
name = "TechnicalFactor"
category = "technical"
def compute(self, data: pd.DataFrame) -> pd.Series:
"""
data 必须包含: ['close', 'volume', 'high', 'low']
返回: 标准化后的多因子合成值
"""
# 示例因子:
# 1) 动量因子 (20日收益率)
# 2) 波动率因子 (20日波动率倒数)
close = data["close"]
mom = close.pct_change(self.params.get("mom_window", 20))
vol = close.pct_change().rolling(self.params.get("vol_window", 20)).std()
# 合成 (等权,可按需要改为 ICIR 加权)
mom_z = (mom - mom.mean()) / mom.std()
vol_z = -(vol - vol.mean()) / vol.std() # 低波为正
composite = 0.5 * mom_z + 0.5 * vol_z
return composite.rename("TechnicalFactor")
# ---------------------------------------------------------------------------
# 基本面因子
# ---------------------------------------------------------------------------
@FactorRegistry.register
class FundamentalFactor(BaseFactor):
"""通用基本面因子 — ROE / PE / PB / 营收增速等"""
name = "FundamentalFactor"
category = "fundamental"
def compute(self, data: pd.DataFrame) -> pd.Series:
"""
data 应包含: ['roe', 'pe', 'pb', 'revenue_growth']
返回基本面复合因子
"""
roe = data.get("roe", None)
pe = data.get("pe", None)
pb = data.get("pb", None)
growth = data.get("revenue_growth", None)
# 将各因子标准化后等权合成
scores = []
for series, inv in [(roe, False), (pe, True), (pb, True), (growth, False)]:
if series is not None:
z = (series - series.mean()) / series.std()
scores.append(-z if inv else z)
if not scores:
raise ValueError("无可用基本面数据列")
composite = sum(scores) / len(scores)
return composite.rename("FundamentalFactor")
# ---------------------------------------------------------------------------
# IC / 分层分析工具
# ---------------------------------------------------------------------------
class FactorAnalyzer:
"""因子分析器 — IC 计算、分层回测、换手率分析"""
def __init__(self, factor_values: pd.Series, forward_returns: pd.Series):
"""
factor_values : 因子值 Series (MultiIndex: [date, code] 或 对齐 index)
forward_returns : 未来一期收益率,与 factor 对齐
"""
self.factor = factor_values
self.forward = forward_returns
self.ic_series: Optional[pd.Series] = None
def compute_ic(self, method: str = "rank") -> pd.Series:
"""逐截面计算 IC (Rank IC 或 Pearson IC)"""
if isinstance(self.factor.index, pd.MultiIndex):
grouped = self.factor.groupby(level=0)
fwd = self.forward.groupby(level=0)
if method == "rank":
ic_data = grouped.apply(
lambda g: g.corr(fwd.get_group(g.name), method="spearman")
)
else:
ic_data = grouped.apply(
lambda g: g.corr(fwd.get_group(g.name), method="pearson")
)
else:
# 单截面
if method == "rank":
ic_data = pd.Series(
stats.spearmanr(self.factor, self.forward)[0],
index=[self.factor.index[0] if len(self.factor) > 0 else 0],
)
else:
ic_data = pd.Series(
stats.pearsonr(self.factor, self.forward)[0],
index=[self.factor.index[0] if len(self.factor) > 0 else 0],
)
self.ic_series = ic_data.dropna()
return self.ic_series
def ic_summary(self) -> Dict[str, float]:
"""IC 汇总统计"""
if self.ic_series is None:
self.compute_ic()
ic = self.ic_series
return {
"IC_Mean": ic.mean(),
"IC_Std": ic.std(),
"IR": ic.mean() / (ic.std() + 1e-12),
"IC>0_Ratio": (ic > 0).mean(),
"IC_Abs_Mean": ic.abs().mean(),
}
def ic_decay(self, forward_returns: Dict[int, pd.Series]) -> pd.Series:
"""IC 衰减: 计算不同前瞻期的 IC 均值"""
decay = {}
for horizon, fwd in forward_returns.items():
ratio = fwd.reindex(self.forward.index)
valid = self.factor.notna() & ratio.notna()
ic = stats.spearmanr(
self.factor[valid].values, ratio[valid].values
)[0]
decay[horizon] = ic
return pd.Series(decay, name="IC_Decay")
def quantile_returns(self, n_quantiles: int = 5) -> pd.DataFrame:
"""分层回测:按因子值分 5 组,计算各组平均收益率"""
df = pd.DataFrame({"factor": self.factor, "fwd": self.forward}).dropna()
df["quantile"] = pd.qcut(df["factor"], n_quantiles, labels=False) + 1
result = df.groupby("quantile")["fwd"].mean().to_frame("avg_return")
result.index.name = "quantile"
result["cum_return"] = result["avg_return"].cumsum()
return result
def turnover(self, n_quantiles: int = 5) -> pd.Series:
"""因子换手率分析(相邻期分位数变化比例)"""
df = pd.DataFrame({"factor": self.factor})
df["quantile"] = pd.qcut(df["factor"], n_quantiles, labels=False) + 1
if isinstance(df.index, pd.MultiIndex):
df = df.reset_index()
date_col = df.columns[0]
turnover_list = []
for date, grp in df.groupby(date_col):
pass # 此处需按股票计算 --- 简化处理
# 简化: 逐期计算因子自相关系数
if isinstance(self.ic_series, pd.Series):
auto_corr = self.factor.autocorr(lag=1)
return pd.Series({"auto_corr": auto_corr})
return pd.Series()