mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 17:54:15 +08:00
策略页全量 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 通过。
166 lines
5.7 KiB
Python
166 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import types
|
|
from datetime import date
|
|
from typing import ClassVar
|
|
|
|
from app.api import screener as screener_api
|
|
from app.services.screener import ScreenerResult
|
|
|
|
|
|
class _CapturingScreenerService:
|
|
calls: ClassVar[list[dict]] = []
|
|
|
|
def __init__(self, repo, asset_type="stock"):
|
|
self.repo = repo
|
|
self.asset_type = asset_type
|
|
|
|
def latest_date(self):
|
|
return date(2026, 7, 15)
|
|
|
|
def build_strategy_context(
|
|
self,
|
|
engine,
|
|
as_of,
|
|
strategy_ids,
|
|
*,
|
|
timeframe="1d",
|
|
params_map=None,
|
|
overrides_map=None,
|
|
):
|
|
self.calls.append({
|
|
"kind": "context",
|
|
"strategy_ids": strategy_ids,
|
|
"timeframe": timeframe,
|
|
"params_map": params_map,
|
|
"overrides_map": overrides_map,
|
|
})
|
|
return types.SimpleNamespace(as_of=as_of)
|
|
|
|
|
|
class _CapturingStrategyEngine:
|
|
calls: ClassVar[list[dict]] = []
|
|
|
|
def has(self, strategy_id):
|
|
return strategy_id == "builtin_strategy"
|
|
|
|
def get(self, strategy_id):
|
|
if not self.has(strategy_id):
|
|
raise ValueError(f"unknown strategy: {strategy_id}")
|
|
return types.SimpleNamespace(meta={"id": strategy_id})
|
|
|
|
def run(self, strategy_id, context, *, pool=None, params=None, overrides=None):
|
|
self.calls.append({
|
|
"kind": "run",
|
|
"strategy_id": strategy_id,
|
|
"pool": pool,
|
|
"params": params,
|
|
"overrides": overrides,
|
|
})
|
|
return ScreenerResult(as_of=context.as_of, strategy=strategy_id)
|
|
|
|
def run_all(self, context, *, params_map=None, overrides_map=None, strategy_ids=None, parallel=True):
|
|
self.calls.append({
|
|
"kind": "run_all",
|
|
"params_map": params_map,
|
|
"overrides_map": overrides_map,
|
|
"strategy_ids": strategy_ids,
|
|
})
|
|
return {
|
|
strategy_id: ScreenerResult(as_of=context.as_of, strategy=strategy_id)
|
|
for strategy_id in strategy_ids or []
|
|
}
|
|
|
|
|
|
def _api_request(tmp_path, engine):
|
|
repo = types.SimpleNamespace(store=types.SimpleNamespace(data_dir=tmp_path))
|
|
state = types.SimpleNamespace(repo=repo, strategy_engine=engine)
|
|
return types.SimpleNamespace(app=types.SimpleNamespace(state=state))
|
|
|
|
|
|
def _install_api_fakes(monkeypatch):
|
|
_CapturingScreenerService.calls = []
|
|
_CapturingStrategyEngine.calls = []
|
|
monkeypatch.setattr(screener_api, "ScreenerService", _CapturingScreenerService)
|
|
monkeypatch.setattr(screener_api, "_load_ext_value_maps", lambda *_args: {})
|
|
monkeypatch.setattr(screener_api, "_update_cache_strategy", lambda *_args: None)
|
|
monkeypatch.setattr(screener_api.strategy_cache, "write_cache", lambda *_args: None)
|
|
|
|
|
|
def test_single_run_passes_saved_params_to_strategy_engine(monkeypatch, tmp_path):
|
|
engine = _CapturingStrategyEngine()
|
|
request = _api_request(tmp_path, engine)
|
|
_install_api_fakes(monkeypatch)
|
|
saved = {"params": {"threshold": 3.0, "enabled": False}}
|
|
monkeypatch.setattr(screener_api.strategy_config, "load_override", lambda *_args: saved)
|
|
|
|
screener_api.run_preset(
|
|
screener_api.PresetRequest(
|
|
strategy_id="builtin_strategy",
|
|
as_of=date(2026, 7, 15),
|
|
),
|
|
request,
|
|
)
|
|
|
|
context_call = _CapturingScreenerService.calls[0]
|
|
run_call = _CapturingStrategyEngine.calls[0]
|
|
assert context_call["params_map"] == {"builtin_strategy": saved["params"]}
|
|
assert context_call["overrides_map"] == {"builtin_strategy": saved}
|
|
assert run_call["params"] == saved["params"]
|
|
assert run_call["overrides"] == saved
|
|
|
|
|
|
def test_batch_run_passes_saved_params_to_strategy_engine(monkeypatch, tmp_path):
|
|
engine = _CapturingStrategyEngine()
|
|
request = _api_request(tmp_path, engine)
|
|
_install_api_fakes(monkeypatch)
|
|
saved = {"params": {"threshold": 3.0, "enabled": False}}
|
|
monkeypatch.setattr(
|
|
screener_api.strategy_config,
|
|
"list_overrides",
|
|
lambda *_args: {"builtin_strategy": saved},
|
|
)
|
|
|
|
screener_api.run_all(
|
|
request,
|
|
body={"as_of": "2026-07-15", "strategy_ids": ["builtin_strategy"]},
|
|
)
|
|
|
|
context_call = _CapturingScreenerService.calls[0]
|
|
run_all_call = _CapturingStrategyEngine.calls[0]
|
|
expected_params = {"builtin_strategy": saved["params"]}
|
|
expected_overrides = {"builtin_strategy": saved}
|
|
assert context_call["params_map"] == expected_params
|
|
assert context_call["overrides_map"] == expected_overrides
|
|
assert run_all_call["params_map"] == expected_params
|
|
assert run_all_call["overrides_map"] == expected_overrides
|
|
|
|
|
|
def test_batch_summary_response_still_writes_full_cache(monkeypatch, tmp_path):
|
|
engine = _CapturingStrategyEngine()
|
|
request = _api_request(tmp_path, engine)
|
|
_install_api_fakes(monkeypatch)
|
|
written = []
|
|
monkeypatch.setattr(screener_api.strategy_config, "list_overrides", lambda *_args: {})
|
|
monkeypatch.setattr(screener_api.strategy_cache, "write_cache", lambda *args: written.append(args))
|
|
|
|
payload = screener_api.run_all(
|
|
request,
|
|
body={
|
|
"as_of": "2026-07-15",
|
|
"strategy_ids": ["builtin_strategy"],
|
|
"summary_only": True,
|
|
},
|
|
)
|
|
|
|
assert payload["as_of"] == "2026-07-15"
|
|
assert payload["results"]["builtin_strategy"]["total"] == 0
|
|
assert payload["results"]["builtin_strategy"]["as_of"] == "2026-07-15"
|
|
# 渐进式路径: 全部算完 → complete 且无 pending
|
|
assert payload["pending"] == []
|
|
assert payload["complete"] is True
|
|
assert payload["error"] is None
|
|
# 渐进式增量写 + 收尾全量写都带 rows, 缓存口径不变
|
|
assert written[0][2]["builtin_strategy"]["rows"] == []
|
|
assert written[-1][2]["builtin_strategy"]["rows"] == []
|