fix(factor): tears对秩类离散因子必空根治(qcut duplicates+quantiles=10) [vps]
alpha16实锤(08-30用户验收factor_b7f3a2e8无图表):cs_rank(ts_cov(cs_rank,cs_rank,5)) 截面值高度并列(中位日25值仅6唯一,-19.5×12/-5×9)→alphalens quantize_factor的 pd.qcut(x,q)不传duplicates(默认raise)→Bin edges must be unique→no_raise吞成全NaN →binning丢100%(303/303日期全抛)→merged空→IC全'No valid IC values'+tears IndexError+报告空=页面无图表。该形态下此类因子tears永远不可能成功。 修复三件: ①tears_data.py加pd.qcut duplicates='drop'幂等patch(与demean pandas2 patch同位, _SANGUO flag防重入)——并列值并入少数分位,quantile_keys动态短化前端已容错 ②analyzer get_clean调用前import tears_data激活模块级补丁(原import在IC后=补丁 晚到)+quantiles默认5→10显式传参(对齐tears页D1..D10设计与排行榜decile口径) ③build_tears_data qkeys防御:<2组多空退化零序列/0组空Series,不再IndexError +回归测试:离散并列因子走真实alphalens binning(原测试自合成factor_quantile 绕过该路径=漏掉此bug的原因);RED→GREEN双向验证(无patch merged空(0,3)97.5% bin丢/有patch绿)。factor+api 123绿;NAS容器30只股端到端:binning丢0.0%、 merged 7732行、tears三周期出数(ic_mean 5D=-0.0014)
This commit is contained in:
@@ -242,10 +242,15 @@ def run_factor_analysis(
|
||||
continue
|
||||
|
||||
# Call get_clean_factor_and_forward_returns
|
||||
# tears_data 模块级补丁(pd.qcut duplicates='drop' + demean pandas2)
|
||||
# 须在 get_clean 前加载生效:秩类因子(cs_rank 族)截面并列值会使
|
||||
# alphalens 分位边界重合,不 patch 则 binning 丢 100%(2026-08-30 实锤)。
|
||||
from .tears_data import build_tears_data # noqa: F401 (import for side-effect patches)
|
||||
merged_data = get_clean_factor_and_forward_returns(
|
||||
factor=factor_series,
|
||||
prices=prices_df,
|
||||
periods=periods, # Use configurable periods
|
||||
quantiles=10, # 十分组:对齐 tears 页 D1..D10 与排行榜 decile 口径
|
||||
max_loss=1.0 # TEMPORARY: Allow 100% loss to see IC data
|
||||
)
|
||||
|
||||
@@ -299,7 +304,6 @@ def run_factor_analysis(
|
||||
try:
|
||||
import json as _json
|
||||
from datetime import datetime as _dt
|
||||
from .tears_data import build_tears_data
|
||||
tears = build_tears_data(merged_data, periods)
|
||||
tears["factor"] = factor_name
|
||||
tears["generated_at"] = _dt.now().isoformat(timespec="seconds")
|
||||
|
||||
@@ -26,6 +26,24 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# 同款幂等补丁:alphalens quantize_factor 的 pd.qcut(x, q) 不传 duplicates
|
||||
# (默认 'raise')——秩类因子(cs_rank/ts_rank 族,如 alpha16)截面值高度并列,
|
||||
# 分位边界必重合 → ValueError: Bin edges must be unique → 被 no_raise 吞成
|
||||
# 全 NaN → binning 丢 100% → merged 空 → IC 全空 + tears 崩(NAS 2026-08-30
|
||||
# 实锤:alpha16 303/303 日期全抛,中位日 25 值仅 6 唯一)。改默认 duplicates='drop'
|
||||
# 后并列值并入少数分位(quantile_keys 变短,前端已按实际 keys 动态渲染)。
|
||||
_orig_qcut = pd.qcut
|
||||
|
||||
|
||||
def _qcut_dup_drop(*args, **kwargs):
|
||||
kwargs.setdefault("duplicates", "drop")
|
||||
return _orig_qcut(*args, **kwargs)
|
||||
|
||||
|
||||
if not getattr(pd.qcut, "_sanguo_dup_drop", False):
|
||||
_qcut_dup_drop._sanguo_dup_drop = True
|
||||
pd.qcut = _qcut_dup_drop
|
||||
|
||||
_TRADING_DAYS = 252
|
||||
|
||||
|
||||
@@ -97,8 +115,14 @@ def build_tears_data(merged_data, periods: tuple = (1, 5, 10)) -> dict:
|
||||
quantile_nav[str(q)] = [round(float(v), 6) for v in _nav(daily_q)]
|
||||
quantile_annual[str(q)] = round(_annualized(daily_q, p), 4)
|
||||
|
||||
# 多空 = 最高分位 − 最低分位(每日,demeaned 超额口径同 tearsheet)
|
||||
ls_daily = qd[qkeys[-1]] - qd[qkeys[0]]
|
||||
# 多空 = 最高分位 − 最低分位(每日,demeaned 超额口径同 tearsheet)。
|
||||
# 离散因子 duplicates='drop' 后极端形态可能仅 1 组:多空退化为零序列。
|
||||
if len(qkeys) >= 2:
|
||||
ls_daily = qd[qkeys[-1]] - qd[qkeys[0]]
|
||||
elif qkeys:
|
||||
ls_daily = qd[qkeys[0]] * 0.0
|
||||
else:
|
||||
ls_daily = pd.Series(dtype=float)
|
||||
ls_nav = _nav(ls_daily)
|
||||
|
||||
n = len(ic)
|
||||
|
||||
@@ -127,3 +127,45 @@ def test_build_tears_data_json_serializable(built):
|
||||
"""端点要 FileResponse 这个 dict → 必须整棵 json 可序列化."""
|
||||
import json
|
||||
json.dumps(built)
|
||||
|
||||
|
||||
# —— 离散并列因子走真实 alphalens binning(NAS 2026-08-30 alpha16 实锤回归) ——
|
||||
|
||||
def test_discrete_factor_binning_survives():
|
||||
"""alpha16 实锤形态:cs_rank 族因子截面值高度并列(中位日 25 值仅 6 唯一,
|
||||
-19.5×12/-5×9)→ alphalens quantize_factor 的 pd.qcut 不传 duplicates,
|
||||
边界必重合 → ValueError 被 no_raise 吞成全 NaN → binning 丢 100% →
|
||||
merged 空 → IC 全空 + tears IndexError。tears_data 模块级 qcut
|
||||
duplicates='drop' 补丁后:merged 非空、tears 出数、keys 动态短化不崩。"""
|
||||
pytest.importorskip("alphalens")
|
||||
import sanguo_factor.tears_data # noqa: F401 — 触发 pd.qcut duplicates='drop' 补丁
|
||||
from alphalens.utils import get_clean_factor_and_forward_returns
|
||||
from sanguo_factor.tears_data import build_tears_data
|
||||
|
||||
rng = np.random.default_rng(11)
|
||||
n_days, n_assets = 40, 25
|
||||
dates = pd.date_range("2024-01-02", periods=n_days, freq="B", name="date")
|
||||
assets = [f"S{i:03d}" for i in range(n_assets)]
|
||||
idx = pd.MultiIndex.from_product([dates, assets], names=["date", "asset"])
|
||||
levels = np.array([-19.5, -13.0, -12.0, -11.0, -10.0, -5.0])
|
||||
picks = rng.integers(0, len(levels), size=len(idx))
|
||||
factor = pd.Series(levels[picks], index=idx, name="factor")
|
||||
|
||||
px = pd.DataFrame(
|
||||
100 + np.cumsum(rng.normal(0, 1, size=(n_days, n_assets)), axis=0),
|
||||
index=dates,
|
||||
columns=assets,
|
||||
)
|
||||
|
||||
merged = get_clean_factor_and_forward_returns(
|
||||
factor=factor, prices=px, periods=(1,), quantiles=10, max_loss=1.0,
|
||||
)
|
||||
assert not merged.empty, "离散并列因子不应被 binning 丢光(修复回归)"
|
||||
|
||||
tears = build_tears_data(merged, periods=(1,))
|
||||
d = tears["periods"]["1D"]
|
||||
assert d["count"] > 0
|
||||
assert d["quantile_keys"], "至少应有一组"
|
||||
assert len(d["ls_nav"]) == len(d["nav_dates"])
|
||||
import json
|
||||
json.dumps(tears)
|
||||
|
||||
Reference in New Issue
Block a user