feat(polish): 因子报告 IC 提取 + periods 提参 + 缓存上限 + 代码整洁
- analyzer.py: 提取 IC 值到 ic_summary (mean/std/icir/t_stat),periods 提参 (默认 1,5,10) - alpha_lab.py: _loaded_bars 缓存 LRU 上限 (_MAX_CACHED_SYMBOLS=50) - runner.py: 统一阶段文案 (参数优化中/因子分析中),worker 类型标注,_wait_future 文档 - pool.py: submit_work 添加 task_id debug 日志 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -100,7 +100,13 @@ def test_run_factor_analysis_adds_features():
|
||||
|
||||
|
||||
def test_run_factor_analysis_calls_tears(tmp_path):
|
||||
"""Test that run_factor_analysis calls alphalens tears pipeline."""
|
||||
"""Test that run_factor_analysis calls alphalens tears pipeline.
|
||||
|
||||
Requires polars - runs in container, skips locally.
|
||||
"""
|
||||
import pytest
|
||||
pytest.importorskip("polars")
|
||||
|
||||
from pathlib import Path
|
||||
from sanguo_factor.analyzer import run_factor_analysis
|
||||
|
||||
@@ -121,3 +127,130 @@ def test_run_factor_analysis_calls_tears(tmp_path):
|
||||
assert report.factor_names == ["ma5"]
|
||||
MC.assert_called_once()
|
||||
MT.assert_called_once()
|
||||
|
||||
|
||||
def test_run_factor_analysis_extracts_ic_values(tmp_path):
|
||||
"""Test that run_factor_analysis extracts IC values from alphalens.
|
||||
|
||||
Requires polars/alphalens - runs in container, skips locally.
|
||||
"""
|
||||
import pytest
|
||||
pytest.importorskip("polars")
|
||||
pytest.importorskip("alphalens")
|
||||
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
from sanguo_factor.analyzer import run_factor_analysis
|
||||
|
||||
# Create mock factor_data with MultiIndex (datetime, asset) and IC columns
|
||||
dates = pd.date_range("2024-01-01", periods=10, freq="D")
|
||||
assets = ["AAPL", "GOOGL"]
|
||||
index = pd.MultiIndex.from_product([dates, assets], names=["datetime", "asset"])
|
||||
|
||||
# Mock factor_data with forward returns
|
||||
mock_factor_data = pd.DataFrame(index=index)
|
||||
mock_factor_data["factor"] = [0.5] * 20 # Factor values
|
||||
mock_factor_data["1D"] = [0.01] * 20 # 1-day forward returns
|
||||
mock_factor_data["5D"] = [0.05] * 20 # 5-day forward returns
|
||||
mock_factor_data["10D"] = [0.10] * 20 # 10-day forward returns
|
||||
|
||||
# Mock IC DataFrame returned by factor_information_coefficient
|
||||
mock_ic_df = pd.DataFrame({
|
||||
"1D": [0.05, 0.03, 0.07, 0.04, 0.06, 0.05, 0.04, 0.06, 0.05, 0.04],
|
||||
"5D": [0.08, 0.06, 0.09, 0.07, 0.08, 0.07, 0.08, 0.06, 0.07, 0.08],
|
||||
"10D": [0.12, 0.10, 0.13, 0.11, 0.12, 0.11, 0.12, 0.10, 0.11, 0.12]
|
||||
}, index=dates)
|
||||
|
||||
# Mock polars DataFrame
|
||||
mock_pl_df = MagicMock()
|
||||
mock_pl_df.to_pandas.return_value = pd.DataFrame({
|
||||
"datetime": [d.isoformat() for d in dates for _ in assets],
|
||||
"vt_symbol": assets * len(dates),
|
||||
"ma5": [0.5] * 20,
|
||||
"close": [100.0] * 20
|
||||
})
|
||||
|
||||
with patch("sanguo_factor.analyzer.AlphaLabSession") as MS, \
|
||||
patch("sanguo_factor.analyzer.get_clean_factor_and_forward_returns") as MC, \
|
||||
patch("sanguo_factor.analyzer.create_full_tear_sheet") as MT, \
|
||||
patch("sanguo_factor.analyzer.factor_information_coefficient") as MIC:
|
||||
|
||||
# Mock compute_factors to return polars DataFrame
|
||||
MS.return_value.compute_factors.return_value = mock_pl_df
|
||||
|
||||
# Mock get_clean_factor_and_forward_returns to return our factor_data
|
||||
MC.return_value = mock_factor_data
|
||||
|
||||
# Mock IC function to return our IC DataFrame
|
||||
MIC.return_value = mock_ic_df
|
||||
|
||||
report = run_factor_analysis(
|
||||
["AAPL"], ["ma5"], "2024-01-01", "2024-01-10",
|
||||
cfg=MagicMock(), output_dir=str(tmp_path)
|
||||
)
|
||||
|
||||
# Verify IC values were extracted
|
||||
assert "ma5" in report.ic_summary
|
||||
assert "ic" in report.ic_summary["ma5"]
|
||||
|
||||
# Check IC structure contains expected periods
|
||||
ic_data = report.ic_summary["ma5"]["ic"]
|
||||
assert "1D" in ic_data
|
||||
assert "5D" in ic_data
|
||||
assert "10D" in ic_data
|
||||
|
||||
# Verify IC statistics are computed
|
||||
assert "mean" in ic_data["1D"]
|
||||
assert "icir" in ic_data["1D"]
|
||||
assert "std" in ic_data["1D"]
|
||||
|
||||
# Verify approximate values (mean should be around 0.05 for 1D)
|
||||
assert abs(ic_data["1D"]["mean"] - 0.05) < 0.01 # Allow small rounding errors
|
||||
|
||||
|
||||
def test_run_factor_analysis_ic_extraction_fails_gracefully(tmp_path):
|
||||
"""Test that IC extraction failures don't crash the pipeline.
|
||||
|
||||
Requires polars/alphalens - runs in container, skips locally.
|
||||
"""
|
||||
import pytest
|
||||
pytest.importorskip("polars")
|
||||
pytest.importorskip("alphalens")
|
||||
|
||||
from sanguo_factor.analyzer import run_factor_analysis
|
||||
import pandas as pd
|
||||
|
||||
# Mock polars DataFrame
|
||||
mock_pl_df = MagicMock()
|
||||
mock_pl_df.to_pandas.return_value = pd.DataFrame({
|
||||
"datetime": ["2024-01-01"],
|
||||
"vt_symbol": ["AAPL"],
|
||||
"ma5": [0.5],
|
||||
"close": [100.0]
|
||||
})
|
||||
|
||||
with patch("sanguo_factor.analyzer.AlphaLabSession") as MS, \
|
||||
patch("sanguo_factor.analyzer.get_clean_factor_and_forward_returns") as MC, \
|
||||
patch("sanguo_factor.analyzer.create_full_tear_sheet") as MT, \
|
||||
patch("sanguo_factor.analyzer.factor_information_coefficient") as MIC:
|
||||
|
||||
MS.return_value.compute_factors.return_value = mock_pl_df
|
||||
|
||||
# Mock get_clean_factor_and_forward_returns to return valid data
|
||||
mock_factor_data = MagicMock()
|
||||
MC.return_value = mock_factor_data
|
||||
|
||||
# Mock IC function to raise an exception
|
||||
MIC.side_effect = Exception("IC calculation failed")
|
||||
|
||||
report = run_factor_analysis(
|
||||
["AAPL"], ["ma5"], "2024-01-01", "2024-01-10",
|
||||
cfg=MagicMock(), output_dir=str(tmp_path)
|
||||
)
|
||||
|
||||
# Verify IC error is captured but status/report still exist
|
||||
assert "ma5" in report.ic_summary
|
||||
assert "ic" in report.ic_summary["ma5"]
|
||||
assert "error" in report.ic_summary["ma5"]["ic"]
|
||||
# Status and report should still be present
|
||||
assert "status" in report.ic_summary["ma5"]
|
||||
|
||||
Reference in New Issue
Block a user