Files
tick-stock-panel/backend/tests/test_shared_matrix_cache.py
T
shy3130 9d3ac6381d feat: 扩展数据调度与交互优化、缓存与策略性能优化
- 扩展数据: 每日/每周/每月调度 + 仅交易日, 拉取设置双层对话框, 创建/编辑弹窗统一
- 量比: 盘中按已交易分钟折算当日成交量, 修复自动/手动数值撕裂
- 策略执行: 分钟路径接入后台单飞队列, 线程池 40→100, 渐进式结果隔离
- 性能: 策略矩阵请求内缓存、策略缓存内存镜像(mtime 指纹失效)、坏策略向量化
- 前端: 分时缩略图着色改为过昨收基准线判定、数字输入草稿态、RunningHub 折扣动态化
- RunningHub: 透传忙闲时定价并展示当前时段
2026-10-19 16:45:13 +08:00

156 lines
5.8 KiB
Python

"""run_all 共享矩阵的进程级复用 (engine.build_shared_matrix 缓存)。
矩阵 = 纯函数(history 窗口, 字段并集), 数组构建后只读 — 同指纹直接复用免重建
(全市场矩阵构建数秒)。指纹含 enriched generation + 窗口范围, 数据/窗口变化
自动失效; generation 未知 (分钟/ETF/测试数据) 时 fail-closed 不缓存。
"""
from __future__ import annotations
from datetime import date
from pathlib import Path
import numpy as np
import polars as pl
from app.strategy.engine import StrategyDataContext, StrategyDef, StrategyEngine
class _FakeMatrixStrategy:
"""最小 matrix_strategy: 只暴露字段依赖接口。"""
def __init__(self, fields: set[str]) -> None:
self._fields = fields
def required_fields(self) -> set[str]:
return set(self._fields)
def required_fields_for_params(self, params: dict | None = None) -> set[str]:
return set()
def _matrix_def(sid: str, fields: set[str]) -> StrategyDef:
return StrategyDef(
meta={"id": sid, "name": sid, "timeframes": ["1d"], "asset_types": ["stock"]},
basic_filter={},
entry_signals=[],
exit_signals=[],
stop_loss=None,
trailing_stop=None,
trailing_take_profit_activate=None,
trailing_take_profit_drawdown=None,
max_hold_days=None,
filter_fn=None,
filter_history_fn=None,
lookback_days=5,
source="custom",
execution_backend="matrix_native",
matrix_strategy=_FakeMatrixStrategy(fields),
)
def _history(days: int = 6) -> pl.DataFrame:
syms = ["600000.SH", "000001.SZ"]
rows = []
for d in range(days):
for s in syms:
rows.append({
"symbol": s, "name": s, "date": date(2026, 3, 2 + d),
"open": 10.0, "high": 11.0, "low": 9.0,
"close": 10.0 + d, "volume": 1000.0 + d,
})
return pl.DataFrame(rows)
def _engine_with(sid: str, fields: set[str]) -> StrategyEngine:
engine = StrategyEngine(strategy_dirs=[Path("/nonexistent")])
engine._strategies[sid] = _matrix_def(sid, fields)
return engine
def _ctx(history: pl.DataFrame, generation: str | None) -> StrategyDataContext:
return StrategyDataContext(
asset_type="stock", timeframe="1d", as_of=date(2026, 3, 7),
current=None, history=history, data_generation=generation,
)
def test_same_fingerprint_returns_cached_matrix():
engine = _engine_with("m1", {"close", "volume"})
hist = _history()
m1 = engine.build_shared_matrix(_ctx(hist, "g1"), [("m1", engine.get("m1"))])
m2 = engine.build_shared_matrix(_ctx(hist, "g1"), [("m1", engine.get("m1"))])
assert m1 is not None and m2 is not None
assert m1 is m2, "同指纹应命中缓存返回同一对象"
assert engine._shared_matrix_cache, "缓存应有一条记录"
def test_cached_matrix_equals_freshly_built():
"""对拍: 缓存命中对象与绕过缓存现建的对象逐元素相等 (纯优化承诺)。"""
engine = _engine_with("m1", {"close", "volume"})
hist = _history()
cached = engine.build_shared_matrix(_ctx(hist, "g1"), [("m1", engine.get("m1"))])
with engine._realtime_matrix_lock:
engine._shared_matrix_cache.clear()
fresh = engine.build_shared_matrix(_ctx(hist, "g1"), [("m1", engine.get("m1"))])
assert cached is not fresh
np.testing.assert_array_equal(cached.close, fresh.close)
np.testing.assert_array_equal(cached.volume, fresh.volume)
assert cached.symbols == fresh.symbols
assert cached.timestamp_labels == fresh.timestamp_labels
def test_field_union_change_rebuilds():
engine = _engine_with("m1", {"close"})
engine._strategies["m2"] = _matrix_def("m2", {"momentum_20d"})
hist = _history()
small = engine.build_shared_matrix(
_ctx(hist, "g1"), [("m1", engine.get("m1"))]
)
union = engine.build_shared_matrix(
_ctx(hist, "g1"),
[("m1", engine.get("m1")), ("m2", engine.get("m2"))],
)
assert small is not union, "字段并集变化必须重建"
assert "momentum_20d" not in small.fields
# 回到小子集: 指纹不同于并集 → 又一次重建 (缓存只保留最近一份)
again = engine.build_shared_matrix(_ctx(hist, "g1"), [("m1", engine.get("m1"))])
assert again is not union
def test_generation_change_rebuilds():
engine = _engine_with("m1", {"close"})
hist = _history()
m1 = engine.build_shared_matrix(_ctx(hist, "g1"), [("m1", engine.get("m1"))])
m2 = engine.build_shared_matrix(_ctx(hist, "g2"), [("m1", engine.get("m1"))])
assert m1 is not m2, "enriched 换代后旧矩阵必须失效"
def test_window_change_rebuilds():
"""不同 lookback 的 history 窗口不可混用同一矩阵。"""
engine = _engine_with("m1", {"close"})
m1 = engine.build_shared_matrix(_ctx(_history(6), "g1"), [("m1", engine.get("m1"))])
m2 = engine.build_shared_matrix(_ctx(_history(4), "g1"), [("m1", engine.get("m1"))])
assert m1 is not m2
assert len(m1.timestamp_labels) == 6
assert len(m2.timestamp_labels) == 4
def test_no_generation_disables_cache():
"""generation 未知 (分钟/ETF/测试): fail-closed 不缓存, 保持旧行为。"""
engine = _engine_with("m1", {"close"})
hist = _history()
m1 = engine.build_shared_matrix(_ctx(hist, None), [("m1", engine.get("m1"))])
m2 = engine.build_shared_matrix(_ctx(hist, None), [("m1", engine.get("m1"))])
assert m1 is not None and m2 is not None
assert m1 is not m2
assert not engine._shared_matrix_cache
def test_remove_strategy_clears_cache():
engine = _engine_with("m1", {"close"})
hist = _history()
engine.build_shared_matrix(_ctx(hist, "g1"), [("m1", engine.get("m1"))])
assert engine._shared_matrix_cache
engine.unregister("m1")
assert not engine._shared_matrix_cache, "删除策略应清空共享矩阵缓存"