fix(factor): 因子分析端到端修复(依赖缺失+vt_symbol+IC/tears解耦)

根因链:
1. alphalens + scikit-learn 没装 → analyzer:76 软错误 return {error:alphalens not installed}(已 pip 装 alphalens-reloaded 0.4.6 + scikit-learn 1.7.2,requirements-lock 已锁)。
2. symbols 传 vt_symbol(600000.SSE)但 read_db_daily 查 DB 用裸代码 → bars 空 → factor/prices 全空 → alphalens infer_trading_calendar weekmask 全零 → busdaycal 崩。
3. statistics 含 date/numpy(之前 optimize 已修 result_store _json_default)。
4. alphalens-reloaded × pandas2 的 groupby.transform 兼容:demean_forward_returns tears 崩 No objects to concatenate。

适配修复(不动 vnpy/alphalens 源码):
- symbols 归一:vt_symbol → 裸代码(split '.'),read_db_daily 能查到。
- IC/tears 解耦:IC 算完先存 ic_summary success,tears 独立 try(失败标 tears_error 不覆盖 IC)——IC 核心数值可用,tears 报告作为已知限制。
- error 加 traceback 字段(调试友好)。
验证: 3 symbol(600000/000001/600519) ma5 因子 IC 成功(1D/5D/10D mean/icir/t_stat count=49),浏览器 /factor/result 页表格渲染。单 symbol IC NaN 是截面用法(需≥2标的),非 bug。
This commit is contained in:
2026-07-17 11:51:08 +08:00
parent 6ed71ed8cf
commit b3ea8e7b04
+35 -17
View File
@@ -2,6 +2,7 @@
import sys
import os
import warnings
import traceback
_VNPY_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "vnpy_v4.4.0"))
if _VNPY_SRC not in sys.path:
sys.path.insert(0, _VNPY_SRC)
@@ -66,6 +67,11 @@ def run_factor_analysis(
"""
from .registry import get_factor
# symbols 兼容:前端/vnpy 可能传 vt_symbol("600000.SSE"),read_db_daily 查 DB 的 key
# 是裸代码(同 cta_engine),带后缀查不到 → bars 空 → factor/prices 全空 → alphalens 崩。
# 统一归一成裸代码(DB key)。
symbols = [str(s).split(".")[0] for s in symbols]
# API path passes cfg=None → load default data_platform.yaml (so read_db_daily
# and AlphaLabSession can find the A-share DB).
if cfg is None:
@@ -203,6 +209,18 @@ def run_factor_analysis(
# Ensure datetime index for prices
prices_df.index = pd.to_datetime(prices_df.index)
# busdaycal 调试:定位 factor/prices 数据是否空或日期不对齐
if prices_df.empty or len(factor_dates) == 0:
fmin = factor_pd['datetime'].min() if len(factor_pd) else None
fmax = factor_pd['datetime'].max() if len(factor_pd) else None
cmin = close_pd['datetime'].min() if len(close_pd) else None
cmax = close_pd['datetime'].max() if len(close_pd) else None
ic_summary[factor_name] = {
"status": "error",
"error": f"DBG empty: factor_df.height={factor_df.height}, factor_dates={len(factor_dates)}, factor_series={len(factor_series)}, close_rows={len(close_pd)}, prices_df={prices_df.shape}, factor_dt={fmin}~{fmax}, close_dt={cmin}~{cmax}",
}
continue
# Call get_clean_factor_and_forward_returns
merged_data = get_clean_factor_and_forward_returns(
factor=factor_series,
@@ -248,12 +266,18 @@ def run_factor_analysis(
# Capture IC extraction error but continue with tears report
ic_data = {"error": f"IC extraction failed: {type(ic_error).__name__}: {ic_error}"}
# Generate tears sheet
# IC 已算完(ic_data)——先存成功。因子分析的核心指标(IC/ICIR/t_stat)可用,
# 即使下面的 tears 报告因 alphalens-reloaded × pandas2 的 groupby.transform
# 兼容问题崩,也不影响 IC 数值。
ic_summary[factor_name] = {
"status": "success",
"ic": ic_data,
}
# Generate tears sheet(独立 trytears 失败只标注,不覆盖上面的 IC 成功)
from io import StringIO
import sys
old_stdout = sys.stdout
sys.stdout = StringIO() # Capture stdout to avoid display issues
try:
create_full_tear_sheet(
merged_data,
@@ -261,27 +285,21 @@ def run_factor_analysis(
group_neutral=False,
by_group=False
)
factor_report_path = os.path.join(output_dir, f"{factor_name}_tears.html")
plt.savefig(factor_report_path.replace(".html", ".png")) # Save as PNG
report_paths[factor_name] = factor_report_path.replace(".png", ".html")
ic_summary[factor_name]["report"] = factor_report_path
except Exception as tears_e:
ic_summary[factor_name]["tears_error"] = f"{type(tears_e).__name__}: {tears_e}"
finally:
sys.stdout = old_stdout # Restore stdout
# Save the tears report
factor_report_path = os.path.join(output_dir, f"{factor_name}_tears.html")
plt.savefig(factor_report_path.replace(".html", ".png")) # Save as PNG
report_paths[factor_name] = factor_report_path.replace(".png", ".html") # Mark HTML as report
# Store basic IC summary (simplified) — close prices sourced from DB (real)
status = "success"
ic_summary[factor_name] = {
"status": status,
"report": factor_report_path,
"ic": ic_data # Add IC statistics
}
except Exception as e:
err_type = type(e).__name__
ic_summary[factor_name] = {
"status": "error",
"error": f"{err_type}: {e}"
"error": f"{err_type}: {e}",
"traceback": traceback.format_exc()
}
return FactorReport(