8eba3e1cf1
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)
150 lines
6.1 KiB
Python
150 lines
6.1 KiB
Python
"""Tears 分层序列序列化(方案A tears 端点数据源).
|
||
|
||
create_full_tear_sheet 的 matplotlib 图,本质是"已算好的序列被画成图"——
|
||
本模块把同一批 alphalens performance 序列(日度IC / 分组日度收益 / 因子秩自相关)
|
||
直接序列化成前端 ECharts 可渲染的 dict,替代 iframe 内嵌浅色 HTML 报告
|
||
(用户 2026-08-29 拍板方案A:原生重构)。
|
||
"""
|
||
import pandas as pd
|
||
|
||
# 与 analyzer.py 同款幂等补丁:alphalens-reloaded 的 demean_forward_returns 用
|
||
# groupby.transform(lambda) 在 pandas2 崩,独立 import 本模块(单测路径)时
|
||
# analyzer 可能未加载 → 补丁不在 → mean_return_by_quantile 崩。重复 patch 无害。
|
||
try:
|
||
import alphalens.utils as _al_utils
|
||
|
||
def _demean_forward_returns_pandas2(factor_data, grouper=None):
|
||
factor_data = factor_data.copy()
|
||
if not grouper:
|
||
grouper = factor_data.index.get_level_values("date")
|
||
cols = _al_utils.get_forward_returns_columns(factor_data.columns)
|
||
means = factor_data.groupby(grouper)[cols].transform("mean")
|
||
factor_data[cols] = factor_data[cols] - means
|
||
return factor_data
|
||
|
||
_al_utils.demean_forward_returns = _demean_forward_returns_pandas2
|
||
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
|
||
|
||
|
||
def _nav(daily: pd.Series) -> pd.Series:
|
||
"""日度收益(可含重叠窗口)→ 累计净值,首值 1;NaN 日视为空仓(0 收益)."""
|
||
return (1.0 + daily.fillna(0.0)).cumprod()
|
||
|
||
|
||
def _max_drawdown(nav: pd.Series) -> float:
|
||
"""净值序列最大回撤(≤0)."""
|
||
v = (nav / nav.cummax() - 1.0).min()
|
||
return float(v) if pd.notna(v) else 0.0
|
||
|
||
|
||
def _annualized(daily: pd.Series, period: int) -> float:
|
||
"""日均 × 252 / period:线性去重叠年化,1/5/10D 三周期可比(1D≈eval 链路口径)."""
|
||
s = daily.dropna()
|
||
if s.empty:
|
||
return 0.0
|
||
return float(s.mean() * _TRADING_DAYS / period)
|
||
|
||
|
||
def _monthly_ic(ic: pd.Series) -> list[dict]:
|
||
"""日度 IC 按月聚合(月度柱/热力图数据),口径同 sanguo_factor.metrics.monthly_ic."""
|
||
s = ic.dropna()
|
||
if s.empty:
|
||
return []
|
||
g = s.groupby(s.index.to_period("M")).mean()
|
||
return [{"month": t.strftime("%Y-%m"), "ic": round(float(v), 6)} for t, v in g.items()]
|
||
|
||
|
||
def build_tears_data(merged_data, periods: tuple = (1, 5, 10)) -> dict:
|
||
"""alphalens factor_data → tears 序列 dict.
|
||
|
||
每序列自带日期轴(ic_dates/nav_dates);IC/分组收益均调 alphalens 原函数,
|
||
与 tearsheet 同源。quantile_nav 给全 10 组,前端按需画 Q1/Q5/Q10。
|
||
"""
|
||
from alphalens.performance import (
|
||
factor_information_coefficient,
|
||
factor_rank_autocorrelation,
|
||
mean_return_by_quantile,
|
||
)
|
||
|
||
ic_df = factor_information_coefficient(merged_data)
|
||
qr_by_date, _ = mean_return_by_quantile(merged_data, by_date=True, demeaned=True)
|
||
|
||
# 因子秩自相关(1D,指标条一项):失败不致命 → None
|
||
try:
|
||
ac = factor_rank_autocorrelation(merged_data, period=1).dropna()
|
||
factor_autocorr = round(float(ac.mean()), 4) if len(ac) else None
|
||
except Exception:
|
||
factor_autocorr = None
|
||
|
||
out: dict = {"factor_autocorr": factor_autocorr, "periods": {}}
|
||
for p in periods:
|
||
# 列名兼容:alphalens-reloaded 生成 "1D",老版纯数字 "1"(同 analyzer 口径)
|
||
col = next((c for c in ic_df.columns if c in (f"{p}D", str(p))), None)
|
||
if col is None:
|
||
continue
|
||
|
||
ic = ic_df[col].dropna()
|
||
qd = qr_by_date[col].unstack("factor_quantile") # date × quantile
|
||
qkeys = sorted(int(q) for q in qd.columns)
|
||
|
||
quantile_nav: dict = {}
|
||
quantile_annual: dict = {}
|
||
for q in qkeys:
|
||
daily_q = qd[q]
|
||
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)。
|
||
# 离散因子 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)
|
||
m = float(ic.mean()) if n else 0.0
|
||
sd = float(ic.std()) if n > 1 else 0.0
|
||
out["periods"][f"{p}D"] = {
|
||
"count": int(n),
|
||
"ic_mean": round(m, 6) if n else None,
|
||
"ic_std": round(sd, 6) if n > 1 else None,
|
||
"icir": round(m / sd, 4) if sd > 0 else None,
|
||
"t_stat": round(m / (sd / n ** 0.5), 4) if sd > 0 and n > 1 else None,
|
||
"win_rate": round(float((ic > 0).mean()), 4) if n else None,
|
||
"ic_dates": [d.strftime("%Y-%m-%d") for d in ic.index],
|
||
"ic_values": [round(float(v), 6) for v in ic],
|
||
"monthly_ic": _monthly_ic(ic),
|
||
"quantile_keys": [str(q) for q in qkeys],
|
||
"nav_dates": [d.strftime("%Y-%m-%d") for d in qd.index],
|
||
"quantile_nav": quantile_nav,
|
||
"quantile_annual": quantile_annual,
|
||
"ls_nav": [round(float(v), 6) for v in ls_nav],
|
||
"ls_annual": round(_annualized(ls_daily, p), 4),
|
||
"ls_max_dd": round(_max_drawdown(ls_nav), 4),
|
||
}
|
||
return out
|