Files
tick-stock-panel/backend/tests/test_screener_cache_api.py
T
shy3130 f03bc38a16 feat(screener): run_all 渐进式返回, 快策略先出、慢策略后台补算
策略页全量 run_all 需 ~2 分钟, 期间卡片全空。现在按历史耗时升序执行,
首返时限 (strategy_run_all_first_return_s, 默认 15s) 内算完的策略随
响应返回, 慢策略转后台继续算并逐个写入策略缓存, 前端轮询
cached-summary 逐个点亮卡片数字 (未出的显示脉冲占位)。

后端:
- services/strategy_run_queue: 单飞 daemon 工作线程 + handle 状态。
  相同 key (资产/周期/日期/策略集) 且未完成的请求搭车现有执行不重算;
  已完成的重跑即新执行。后端全局同时只跑一个 run_all, 补上前端
  防重入之外的第二道 Numba 并发防线
- run_all 渐进分支 (仅日线 + summary_only): 逐策略增量写缓存 (同日
  按 sid 合并), 收尾整体重写保持旧口径; 结果带 computed_at 时间戳;
  分钟周期与明细请求保持整段阻塞不变
- 历史耗时落盘 user_data/strategy_run_timings.json, 次日起快策略自动
  排前; cached-summary 透传 computed_at

前端:
- 请求通用 30s 超时 + 慢接口豁免清单 (run_all/run/backtest/factor 等
  300s), 避免一个挂起请求占满 HTTP/1.1 连接拖死全站
- Screener: 收到 pending 后 summaryQuery 每 2s 轮询, 以
  computed_at >= started_at 判新 (防同日旧缓存冒充), 8 分钟兜底;
  StrategyCard 三种尺寸新增 computing 脉冲占位

验证: 新增 13 测试 (排序/落盘/搭车/串行化/端点行为/旧路径兼容),
全量套件 1762 passed; pnpm build 通过。
2026-09-07 15:25:54 +08:00

98 lines
3.1 KiB
Python

from __future__ import annotations
from types import SimpleNamespace
from app.api import screener as screener_api
class _MonitorEngine:
def __init__(self, results=None):
self.results = results or {}
def latest_strategy_results(self):
return self.results
def _request(tmp_path, monitor_results=None):
repo = SimpleNamespace(store=SimpleNamespace(data_dir=tmp_path))
state = SimpleNamespace(repo=repo, monitor_engine=_MonitorEngine(monitor_results))
return SimpleNamespace(app=SimpleNamespace(state=state))
def test_cached_summary_omits_rows_and_counts_realtime_expirations(monkeypatch, tmp_path):
cached = {
"as_of": "2026-07-20",
"results": {
"strategy_a": {
"as_of": "2026-07-20",
"total": 2,
"rows": [{"symbol": "000001.SZ"}, {"symbol": "000002.SZ"}],
},
},
"today_ever_rows": {
"strategy_a": {
"000001.SZ": {"symbol": "000001.SZ"},
"600000.SH": {"symbol": "600000.SH"},
},
},
"updated_at": 1,
}
realtime = {
"strategy_a": {
"as_of": "2026-07-20",
"total": 2,
"rows": [{"symbol": "000002.SZ"}, {"symbol": "300001.SZ"}],
},
}
monkeypatch.setattr(screener_api.strategy_cache, "read_cache", lambda *_args: cached)
payload = screener_api.get_cached_summary(_request(tmp_path, realtime))
assert payload["results"] == {
"strategy_a": {"total": 2, "as_of": "2026-07-20", "computed_at": None}
}
assert payload["today_ever_counts"] == {"strategy_a": 4}
assert "rows" not in payload["results"]["strategy_a"]
def test_cached_result_returns_only_requested_rows_with_ext_and_strategy_membership(monkeypatch, tmp_path):
cached = {
"as_of": "2026-07-20",
"results": {
"strategy_a": {
"as_of": "2026-07-20",
"total": 1,
"rows": [{"symbol": "000001.SZ"}],
},
"strategy_b": {
"as_of": "2026-07-20",
"total": 2,
"rows": [{"symbol": "000001.SZ"}, {"symbol": "600000.SH"}],
},
},
"today_ever_rows": {
"strategy_a": {
"000001.SZ": {"symbol": "000001.SZ"},
"000002.SZ": {"symbol": "000002.SZ"},
},
},
"updated_at": 1,
}
monkeypatch.setattr(screener_api.strategy_cache, "read_cache", lambda *_args: cached)
monkeypatch.setattr(
screener_api,
"_load_ext_value_maps",
lambda *_args: {"concept.concept": {"000001.SZ": "银行", "000002.SZ": "科技"}},
)
payload = screener_api.get_cached_result(
"strategy_a",
_request(tmp_path),
ext_columns="concept.concept",
)
assert payload["result"]["strategy"] == "strategy_a"
assert payload["result"]["rows"] == [{"symbol": "000001.SZ", "concept.concept": "银行"}]
assert payload["today_ever_rows"]["000002.SZ"]["concept.concept"] == "科技"
assert payload["strategy_ids_by_symbol"] == {"000001.SZ": ["strategy_a", "strategy_b"]}