← 返回 下载 factors.py
"""价量因子库。

所有因子只用 T 日及之前的数据计算(严格避免前向偏差),
因子值统一设计为"越大越好",截面 Z-score 标准化后加权合成。

当前为纯价量因子(日线 OHLCV 可得)。基本面因子(PE/PB/ROE)
需要历史基本面面板数据支持,属于扩展点,见 README。
"""
from __future__ import annotations

from typing import Dict

import numpy as np
import pandas as pd

# 因子注册表:名称 -> 计算函数(输入单只股票 OHLCV,输出按日期索引的因子序列)
FACTOR_FUNCS = {}


def factor(name, lookback=None):
    def deco(fn):
        FACTOR_FUNCS[name] = (fn, lookback)
        return fn
    return deco


@factor("momentum")
def f_momentum(df: pd.DataFrame, lb: int = 60) -> pd.Series:
    """动量:过去 lb 日累计收益(剔除近 5 日,避免短期反转污染,Jegadeesh-Titman 式)。"""
    close = df["close"]
    return close.shift(5) / close.shift(lb) - 1.0


@factor("quality_momentum")
def f_quality_momentum(df: pd.DataFrame, lb: int = 120) -> pd.Series:
    """风险调整动量:区间收益 / 区间波动(夏普式动量,更平滑稳健)。"""
    ret = df["close"].pct_change()
    win = ret.rolling(lb)
    mu, sd = win.mean(), win.std()
    return (mu / sd.replace(0, np.nan)) * np.sqrt(252)


@factor("reversal")
def f_reversal(df: pd.DataFrame, lb: int = 5) -> pd.Series:
    """短期反转:近 lb 日收益取负(跌多了反弹)。"""
    close = df["close"]
    return -(close / close.shift(lb) - 1.0)


@factor("low_vol")
def f_low_vol(df: pd.DataFrame, lb: int = 60) -> pd.Series:
    """低波动:近 lb 日收益波动取负(低波动异象)。"""
    ret = df["close"].pct_change()
    return -ret.rolling(lb).std()


@factor("trend")
def f_trend(df: pd.DataFrame, lb: int = 60) -> pd.Series:
    """趋势强度:价格相对 lb 日均线的偏离。"""
    close = df["close"]
    return close / close.rolling(lb).mean() - 1.0


@factor("high_52w")
def f_high_52w(df: pd.DataFrame, lb: int = 250) -> pd.Series:
    """52 周高点邻近度(George-Hwang 动量):越接近前高,动量延续概率越高。"""
    close = df["close"]
    return close / close.rolling(lb, min_periods=120).max()


@factor("liquidity")
def f_liquidity(df: pd.DataFrame, lb: int = 20) -> pd.Series:
    """流动性:近 lb 日均成交额(对数)。保证可交易性,避免选中僵尸股。"""
    amount = df["close"] * df["volume"]
    return np.log(amount.rolling(lb).mean().clip(lower=1.0))


def compute_factor_panel(
    data_map: Dict[str, pd.DataFrame],
    factor_name: str,
    lookback: int,
    dates: pd.DatetimeIndex,
) -> pd.DataFrame:
    """对全部标的计算某因子的宽表(index=日期, columns=代码)。"""
    fn, _ = FACTOR_FUNCS[factor_name]
    out = {}
    for code, df in data_map.items():
        try:
            s = fn(df, lookback)
            out[code] = s.reindex(dates)
        except Exception:
            continue
    panel = pd.DataFrame(out)
    return panel


def winsorize_zscore(row: pd.Series, n_mad: float = 3.0) -> float:
    """截面去极值 + Z-score(逐日调用太慢,改用向量化版本,见 cross_section_z)。"""
    raise NotImplementedError


def cross_section_z(panel: pd.DataFrame) -> pd.DataFrame:
    """逐日截面:去极值(MAD 3 倍)后 Z-score 标准化。"""
    med = panel.median(axis=1)
    mad = (panel.sub(med, axis=0)).abs().median(axis=1)
    upper = med + 3.0 * 1.4826 * mad
    lower = med - 3.0 * 1.4826 * mad
    clipped = panel.clip(lower=lower, upper=upper, axis=0)
    mean = clipped.mean(axis=1)
    std = clipped.std(axis=1).replace(0, np.nan)
    return clipped.sub(mean, axis=0).div(std, axis=0)