feat:添加alpha模块
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
因子研究模块 — 因子定义、计算、注册与 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()
|
||||
Reference in New Issue
Block a user