mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
- 扩展数据: 每日/每周/每月调度 + 仅交易日, 拉取设置双层对话框, 创建/编辑弹窗统一 - 量比: 盘中按已交易分钟折算当日成交量, 修复自动/手动数值撕裂 - 策略执行: 分钟路径接入后台单飞队列, 线程池 40→100, 渐进式结果隔离 - 性能: 策略矩阵请求内缓存、策略缓存内存镜像(mtime 指纹失效)、坏策略向量化 - 前端: 分时缩略图着色改为过昨收基准线判定、数字输入草稿态、RunningHub 折扣动态化 - RunningHub: 透传忙闲时定价并展示当前时段
156 lines
5.8 KiB
Python
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, "删除策略应清空共享矩阵缓存"
|