0dd81d709f
①后端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绿
105 lines
3.4 KiB
Python
105 lines
3.4 KiB
Python
"""Tests for factor (投研) endpoints (S2): /factor/list, /ic-summary, /report."""
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from sanguo_api.app import create_app
|
|
from sanguo_api.routes import set_orchestrator
|
|
from sanguo_api.auth import hash_password
|
|
|
|
|
|
class FakeReport:
|
|
"""Stand-in for FactorReport."""
|
|
|
|
def __init__(self, ic_summary: dict, report_paths: dict, tears_paths: dict | None = None):
|
|
self.ic_summary = ic_summary
|
|
self.report_paths = report_paths
|
|
self.tears_paths = tears_paths or {}
|
|
|
|
|
|
class FakeOrch:
|
|
def __init__(self, raw):
|
|
self._raw = raw
|
|
|
|
def get_raw_result(self, task_id):
|
|
return self._raw
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def client() -> TestClient:
|
|
app = create_app(
|
|
db_path="/tmp/test_fc_routes.db",
|
|
auth_config={
|
|
"username": "admin",
|
|
"password_hash": hash_password("admin"),
|
|
"jwt_secret": "test-secret",
|
|
"expire_minutes": 60,
|
|
},
|
|
max_workers=1,
|
|
)
|
|
set_orchestrator(FakeOrch(FakeReport(
|
|
ic_summary={"ma5": {"status": "success", "ic": {
|
|
"1D": {"mean": -0.12, "std": 0.5, "icir": -0.24, "t_stat": -1.1, "count": 49},
|
|
}}},
|
|
report_paths={"ma5": "/tmp/__definitely_absent_ma5.html"},
|
|
)))
|
|
return TestClient(app)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def token(client) -> str:
|
|
return client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin"}).json()["token"]
|
|
|
|
|
|
def test_factor_list_shape(client, token):
|
|
r = client.get("/api/v1/factor/list", headers={"Authorization": f"Bearer {token}"})
|
|
assert r.status_code == 200
|
|
assert isinstance(r.json()["factors"], list)
|
|
|
|
|
|
def test_ic_summary(client, token):
|
|
r = client.get("/api/v1/task/t/ic-summary", headers={"Authorization": f"Bearer {token}"})
|
|
assert r.status_code == 200
|
|
ic = r.json()["ic_summary"]
|
|
assert "ma5" in ic
|
|
assert ic["ma5"]["ic"]["1D"]["mean"] == -0.12
|
|
|
|
|
|
def test_report_bad_token_401(client):
|
|
r = client.get("/api/v1/task/t/report/ma5?token=bad")
|
|
assert r.status_code == 401
|
|
|
|
|
|
def test_report_file_absent_404(client, token):
|
|
r = client.get(f"/api/v1/task/t/report/ma5?token={token}")
|
|
assert r.status_code == 404
|
|
|
|
|
|
# —— tears JSON 端点(方案A) ——
|
|
|
|
def test_tears_json_no_token_401(client):
|
|
r = client.get("/api/v1/task/t/tears/ma5")
|
|
assert r.status_code == 401
|
|
|
|
|
|
def test_tears_json_absent_404(client, token):
|
|
"""fixture 的 FakeReport 无 tears_paths → 404(历史任务语义)."""
|
|
r = client.get("/api/v1/task/t/tears/ma5", headers={"Authorization": f"Bearer {token}"})
|
|
assert r.status_code == 404
|
|
|
|
|
|
def test_tears_json_served(client, token, tmp_path):
|
|
"""tears_paths 指向真实 JSON 文件 → 200 + application/json."""
|
|
import json
|
|
p = tmp_path / "ma5_tears.json"
|
|
p.write_text(json.dumps({"factor": "ma5", "periods": {"1D": {"ic_mean": 0.06}}}),
|
|
encoding="utf-8")
|
|
set_orchestrator(FakeOrch(FakeReport(
|
|
ic_summary={"ma5": {"status": "success", "ic": {}}},
|
|
report_paths={"ma5": "/tmp/__definitely_absent_ma5.html"},
|
|
tears_paths={"ma5": str(p)},
|
|
)))
|
|
r = client.get("/api/v1/task/t/tears/ma5", headers={"Authorization": f"Bearer {token}"})
|
|
assert r.status_code == 200
|
|
assert r.headers["content-type"].startswith("application/json")
|
|
assert r.json()["factor"] == "ma5"
|