0dd81d709f
①后端tears序列化:sanguo_factor/tears_data.py新模块,alphalens已算分层序列(日度IC/月度聚合/十分组累计净值/多空Q10−Q1净值+最大回撤/去重叠年化/因子秩自相关)在分析时序列化为{factor}_tears.json,与tearsheet同源;FactorReport加tears_paths,GET /task/{id}/tears/{factor}端点(token header),_persist_factor同步落DB
②前端tears页:TearsPanel.vue按设计稿tab①——指标条7格+月度IC柱(红正绿负)/累计IC线双轴+月度IC热力图(年×月,CSS格)+分组累计净值Q1/Q5/Q10+多空净值(琥珀+面积+○最大回撤标注)+十分组年化(±5%虚线)+IC衰减(1/5/10D),1/5/10D全页联动;Result.vue的iframe→原生渲染;旧任务404自动回退iframe旧alphalens报告
③加入对比(设计稿tab②纯前端):factorCompare store(localStorage持久化,2~6个)+排行榜行内「+对比/✓已选」列+详情页死按钮做实(选中青色态)+全局底部托盘CompareTray(chips可删/清空/对比N因子→)+对比页Compare.vue(指标并排·行最优青色高亮/累计IC叠加多线/月度IC序列Pearson相关性矩阵前端算/十分组小倍数SVG)
测试:tears_data纯函数5+全链真实alphalens6(合成因子IC>0/分层单调/JSON可序列化)+analyzer写盘/容错2+端点401/404/200共3;factor+api+orchestrator 317全绿;npm run build绿
126 lines
5.0 KiB
Python
126 lines
5.0 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
|
||
|
||
_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)
|
||
ls_daily = qd[qkeys[-1]] - qd[qkeys[0]]
|
||
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
|