- cross_sectional_zscore / composite_score / build_factor_panels 从 local_engine 迁入 quant/composite.py;新增统一入口 build_score_panel(daily, factor_specs) - local_engine re-export 保持旧引用兼容;selection/engine 的评分面板构建均指向 composite —— 选股与回测的复合分实现收敛于一处 - 回归:quant/eval/research/selection 一致性/qlib 引擎测试全过;全量 pytest 通过
74 lines
2.5 KiB
Python
74 lines
2.5 KiB
Python
"""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 计算面板与权重(因子不存在即报错)。"""
|
||
panels: list[tuple[str, pd.DataFrame, float, str]] = []
|
||
for fs in factor_specs:
|
||
defn: FactorDef
|
||
defn, panel = compute_factor(fs.name, daily)
|
||
panels.append((fs.name, panel, fs.weight, defn.direction))
|
||
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))
|