perf(strategy): 渐进式 run_all 只构建一次共享矩阵

_run_all_progressive 逐策略调用 engine.run_all(strategy_ids=[sid]),
run_all 不回写 context.market → 每个 matrix_native 策略都重建一次
全市场矩阵 (39 万行, 小服务器单次数秒到十余秒), 6 策略一轮多耗约
1 分钟。

从 run_all 抽出 build_shared_matrix (字段并集语义不变, 非渐进路径
行为一致), 渐进式循环外一次建好放入 context.market 复用。
This commit is contained in:
shy3130
2026-09-09 15:30:10 +08:00
parent b531475ca8
commit 01347b72bc
3 changed files with 118 additions and 32 deletions
+15 -1
View File
@@ -8,7 +8,7 @@ import math
import os
import re
import time
from dataclasses import asdict
from dataclasses import asdict, replace
from datetime import date, datetime
from typing import Any, Optional
@@ -536,6 +536,20 @@ def _run_all_progressive(
params_map=params_map,
overrides_map=overrides_map,
)
# 逐策略 run_all 不会把矩阵回写 context.market → 每个矩阵策略都会重建
# 全市场矩阵 (小服务器上单次数秒到十余秒)。这里按字段并集一次建好复用;
# FakeEngine 等无该方法的实现跳过 (保持旧行为)。
if getattr(context, "market", None) is None:
build_matrix = getattr(engine, "build_shared_matrix", None)
if callable(build_matrix):
matrix = build_matrix(
context,
[(sid, engine.get(sid)) for sid in ordered_ids],
params_map,
overrides_map,
)
if matrix is not None:
context = replace(context, market=matrix)
all_results: dict[str, dict] = {}
elapsed_map: dict[str, float] = {}
for sid in ordered_ids:
+46 -31
View File
@@ -1151,37 +1151,9 @@ class StrategyEngine:
raise ValueError("selected strategies require history data")
shared_matrix = context.market
matrix_strats = [
(sid, strategy)
for sid, strategy in selected
if strategy.execution_backend == "matrix_native"
]
if (
shared_matrix is None
and matrix_strats
and shared_history is not None
and not shared_history.is_empty()
):
from app.backtest.matrix import build_market_data_matrix
field_columns: set[str] = set()
for sid, strategy in matrix_strats:
field_columns.update(
self._matrix_field_columns(
strategy,
overrides_map.get(sid),
params_map.get(sid),
)
)
matrix_t0 = time.perf_counter()
shared_matrix = build_market_data_matrix(
shared_history,
field_columns=field_columns,
)
logger.info(
"run_all: shared matrix built in %.0fms (fields=%d)",
(time.perf_counter() - matrix_t0) * 1000,
len(field_columns),
if shared_matrix is None:
shared_matrix = self.build_shared_matrix(
context, selected, params_map, overrides_map
)
results: dict[str, StrategyResult] = {}
@@ -1227,6 +1199,49 @@ class StrategyEngine:
return results
def build_shared_matrix(
self,
context: StrategyDataContext,
selected: list[tuple[str, StrategyDef]],
params_map: dict | None = None,
overrides_map: dict | None = None,
):
"""按所选策略的字段并集构建市场数据矩阵; 无矩阵策略或无历史时返回 None。
渐进式 run_all (逐策略执行) 也用它一次建好并集矩阵后放入 context.market,
避免每个 matrix_native 策略重复构建同一份大矩阵 (全市场历史, 秒级)。
"""
params_map = params_map or {}
overrides_map = overrides_map or {}
matrix_strats = [
(sid, strategy)
for sid, strategy in selected
if strategy.execution_backend == "matrix_native"
]
history = context.history
if not matrix_strats or history is None or history.is_empty():
return None
from app.backtest.matrix import build_market_data_matrix
field_columns: set[str] = set()
for sid, strategy in matrix_strats:
field_columns.update(
self._matrix_field_columns(
strategy,
overrides_map.get(sid),
params_map.get(sid),
)
)
matrix_t0 = time.perf_counter()
matrix = build_market_data_matrix(history, field_columns=field_columns)
logger.info(
"run_all: shared matrix built in %.0fms (fields=%d)",
(time.perf_counter() - matrix_t0) * 1000,
len(field_columns),
)
return matrix
@staticmethod
def _matrix_field_columns(
strategy: StrategyDef,
@@ -234,3 +234,60 @@ def test_run_all_full_detail_stays_blocking(monkeypatch, tmp_path, fast_first_re
# 非 summary 请求: 保持整段阻塞并返回明细
assert resp["results"]["d1"]["rows"][0]["symbol"] == "000001.SZ"
assert "pending" not in resp
def test_run_all_progressive_builds_matrix_once_and_shares_it(
monkeypatch, tmp_path, fast_first_return
):
"""渐进式逐策略执行前, 并集矩阵只建一次并放进 context.market 复用。
旧行为: 每个策略单独 run_all, context.market 不回写 → 每个矩阵策略都
重建全市场矩阵 (小服务器上单次数秒到十余秒)。
"""
@dataclass
class _Context:
market: object = None
class _MatrixEngine(_FakeEngine):
def __init__(self, delays):
super().__init__(delays)
self.matrix_builds = 0
self.seen_markets: list[object] = []
def build_shared_matrix(self, context, selected, params_map=None, overrides_map=None):
self.matrix_builds += 1
return {"fields": len(selected)}
def run_all(self, context, params_map=None, overrides_map=None, *, strategy_ids=None, parallel=True):
self.seen_markets.append(context.market)
return super().run_all(
context, params_map=params_map, overrides_map=overrides_map,
strategy_ids=strategy_ids, parallel=parallel,
)
class _CtxService(_FakeService):
def build_strategy_context(self, *args, **kwargs):
return _Context()
engine = _MatrixEngine({"s1": 0.01, "s2": 0.01, "s3": 0.01})
monkeypatch.setattr(screener_api, "ScreenerService", _CtxService)
resp = screener_api.run_all(
_request(tmp_path, engine),
{
"as_of": AS_OF,
"strategy_ids": ["s1", "s2", "s3"],
"asset_type": "stock",
"timeframe": "1d",
"summary_only": True,
},
)
results = _wait_cache_results(tmp_path, ["s1", "s2", "s3"])
assert set(results) == {"s1", "s2", "s3"}
assert resp["complete"] is True or resp["pending"] == []
# 矩阵只构建一次; 每个策略拿到的都是同一个 market 对象
assert engine.matrix_builds == 1
assert engine.seen_markets and all(m == {"fields": 3} for m in engine.seen_markets)
assert len(engine.seen_markets) == 3