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:
+35
-17
@@ -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(独立 try:tears 失败只标注,不覆盖上面的 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
|
||||
)
|
||||
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
|
||||
}
|
||||
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
|
||||
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user