Files
sanguo_vnpy_v2/sanguo_factor/tears_data.py
T
claude_dev 0dd81d709f
CI/CD / test (push) Successful in 5s
CI/CD / nas-deploy (push) Successful in 8s
CI/CD / nas-verify (push) Successful in 8s
feat(factor): tears报告方案A原生重构+「加入对比」做实(用户08-29拍板) [vps]
①后端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绿
2026-08-29 08:45:10 +08:00

126 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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