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绿
This commit is contained in:
@@ -232,6 +232,102 @@ def test_run_factor_analysis_extracts_ic_values(tmp_path):
|
||||
assert abs(ic_data["1D"]["mean"] - 0.05) < 0.01 # Allow small rounding errors
|
||||
|
||||
|
||||
def test_run_factor_analysis_writes_tears_json(tmp_path):
|
||||
"""tears JSON(方案A)写盘 + FactorReport.tears_paths 记录 + factor/generated_at 补齐."""
|
||||
import pytest
|
||||
pytest.importorskip("polars")
|
||||
pytest.importorskip("alphalens")
|
||||
|
||||
import json
|
||||
import pandas as pd
|
||||
from sanguo_factor.analyzer import run_factor_analysis
|
||||
|
||||
dates = pd.date_range("2024-01-01", periods=6, freq="D", tz="Asia/Shanghai")
|
||||
mock_factor_data = pd.DataFrame(index=pd.MultiIndex.from_product(
|
||||
[dates, ["AAPL"]], names=["datetime", "asset"]))
|
||||
mock_factor_data["factor"] = [0.5] * 6
|
||||
mock_factor_data["1D"] = [0.01] * 6
|
||||
mock_factor_data["5D"] = [0.05] * 6
|
||||
mock_factor_data["10D"] = [0.10] * 6
|
||||
|
||||
mock_pl_df = MagicMock()
|
||||
mock_pl_df.to_pandas.return_value = pd.DataFrame({
|
||||
"datetime": [d.isoformat() for d in dates],
|
||||
"vt_symbol": ["AAPL"] * 6,
|
||||
"ma5": [0.5] * 6,
|
||||
})
|
||||
|
||||
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"), \
|
||||
patch("sanguo_factor.analyzer.factor_information_coefficient") as MIC, \
|
||||
patch("sanguo_factor.tears_data.build_tears_data",
|
||||
return_value={"factor_autocorr": 0.9, "periods": {"1D": {"ic_mean": 0.1}}}) as MB, \
|
||||
patch("sanguo_data.datareader.read_db_daily",
|
||||
return_value=_fake_bars("AAPL", range(1, 7))):
|
||||
MS.return_value.compute_factors.return_value = mock_pl_df
|
||||
MC.return_value = mock_factor_data
|
||||
MIC.return_value = pd.DataFrame(
|
||||
{"1D": [0.05, 0.04, 0.06, 0.05, 0.04, 0.06]}, index=dates)
|
||||
|
||||
report = run_factor_analysis(
|
||||
["AAPL"], ["ma5"], "2024-01-01", "2024-01-10",
|
||||
cfg=MagicMock(), output_dir=str(tmp_path)
|
||||
)
|
||||
MB.assert_called_once()
|
||||
assert "ma5" in report.tears_paths
|
||||
assert report.tears_paths["ma5"].endswith("ma5_tears.json")
|
||||
assert os.path.exists(report.tears_paths["ma5"])
|
||||
with open(report.tears_paths["ma5"], encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
assert data["factor"] == "ma5"
|
||||
assert data["periods"]["1D"]["ic_mean"] == 0.1
|
||||
assert data["generated_at"]
|
||||
|
||||
|
||||
def test_run_factor_analysis_tears_json_error_not_fatal(tmp_path):
|
||||
"""build_tears_data 抛错 → 只标注 tears_json_error,IC 结果保留."""
|
||||
import pytest
|
||||
pytest.importorskip("polars")
|
||||
pytest.importorskip("alphalens")
|
||||
|
||||
import pandas as pd
|
||||
from sanguo_factor.analyzer import run_factor_analysis
|
||||
|
||||
dates = pd.date_range("2024-01-01", periods=4, freq="D", tz="Asia/Shanghai")
|
||||
mock_factor_data = pd.DataFrame(index=pd.MultiIndex.from_product(
|
||||
[dates, ["AAPL"]], names=["datetime", "asset"]))
|
||||
for c in ("factor", "1D", "5D", "10D"):
|
||||
mock_factor_data[c] = [0.5] * 4
|
||||
|
||||
mock_pl_df = MagicMock()
|
||||
mock_pl_df.to_pandas.return_value = pd.DataFrame({
|
||||
"datetime": [d.isoformat() for d in dates],
|
||||
"vt_symbol": ["AAPL"] * 4,
|
||||
"ma5": [0.5] * 4,
|
||||
})
|
||||
|
||||
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"), \
|
||||
patch("sanguo_factor.analyzer.factor_information_coefficient") as MIC, \
|
||||
patch("sanguo_factor.tears_data.build_tears_data",
|
||||
side_effect=RuntimeError("boom")), \
|
||||
patch("sanguo_data.datareader.read_db_daily",
|
||||
return_value=_fake_bars("AAPL", range(1, 5))):
|
||||
MS.return_value.compute_factors.return_value = mock_pl_df
|
||||
MC.return_value = mock_factor_data
|
||||
MIC.return_value = pd.DataFrame({"1D": [0.05, 0.04, 0.06, 0.05]}, index=dates)
|
||||
|
||||
report = run_factor_analysis(
|
||||
["AAPL"], ["ma5"], "2024-01-01", "2024-01-10",
|
||||
cfg=MagicMock(), output_dir=str(tmp_path)
|
||||
)
|
||||
assert report.ic_summary["ma5"]["status"] == "success"
|
||||
assert "tears_json_error" in report.ic_summary["ma5"]
|
||||
assert report.tears_paths == {}
|
||||
|
||||
|
||||
def test_run_factor_analysis_ic_extraction_fails_gracefully(tmp_path):
|
||||
"""Test that IC extraction failures don't crash the pipeline.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user