"""Composite Factor Engine(M7.2,ARCHITECTURE_v2 §13)。 把「多因子 → 加权复合分面板」独立成模块,供: - 选股(SelectionEngine.run_score_selection) - 回测(LocalEngine.run_backtest) 共用同一实现(v2 §25 一致性)。 method 先落地 fixed(截面 zscore × 方向 × 权重 求和);Rank/Z/IC 加权留接口。 """ from __future__ import annotations import math import pandas as pd from app.quant.factors import FactorDef, compute_factor def cross_sectional_zscore(panel: pd.DataFrame) -> pd.DataFrame: """截面 z-score。 候选不足 2 只(如单股票池)时退化为 0:无比较基准,但保留为可候选值; 全列缺失才为 NaN(该日不可选股)。 """ def _row_z(row: pd.Series) -> pd.Series: valid = row.dropna() if len(valid) == 0: return pd.Series(float("nan"), index=row.index) if len(valid) == 1: return pd.Series(0.0, index=row.index) mu, sd = valid.mean(), valid.std() if sd == 0 or math.isnan(sd): return pd.Series(0.0, index=row.index) return (row - mu) / sd return panel.apply(_row_z, axis=1) def composite_score(panels: list[tuple[str, pd.DataFrame, float, str]]) -> pd.DataFrame: """按 (name, panel, weight, direction) 计算加权复合 zscore。 direction="lower_is_better" 的因子取负号后相加(统一为「得分高者优先」)。 """ total = None for _name, panel, weight, direction in panels: z = cross_sectional_zscore(panel) if direction == "lower_is_better": z = -z contribution = z * weight total = contribution if total is None else total.add(contribution, fill_value=0) assert total is not None return total def build_factor_panels( daily: pd.DataFrame, factor_specs ) -> list[tuple[str, pd.DataFrame, float, str]]: """按 spec.factors 计算面板与权重(因子不存在即报错)。""" return [ (defn.name, panel, weight, defn.direction) for defn, panel, weight in build_factor_panels_full(daily, factor_specs) ] def build_factor_panels_full( daily: pd.DataFrame, factor_specs ) -> list[tuple[FactorDef, pd.DataFrame, float]]: """同 `build_factor_panels`,但把 `FactorDef` 一并带出来。 回测的「买卖理由」与「因子曲线」需要用到因子的显示名 / 方向 / 单位(`FactorDef`), 而只拿 name 就得回注册表再查一遍 —— 这里一次算完,避免同一次回测里重复计算面板。 """ panels: list[tuple[FactorDef, pd.DataFrame, float]] = [] for fs in factor_specs: defn, panel = compute_factor(fs.name, daily) panels.append((defn, panel, fs.weight)) return panels def build_score_panel(daily: pd.DataFrame, factor_specs) -> pd.DataFrame: """因子加权复合分面板(index=trade_date, columns=symbol)。 回测与选股共用的统一入口 —— 保证 v2 §25/§27 一致性。 """ return composite_score(build_factor_panels(daily, factor_specs))