mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
- 扩展数据: 每日/每周/每月调度 + 仅交易日, 拉取设置双层对话框, 创建/编辑弹窗统一 - 量比: 盘中按已交易分钟折算当日成交量, 修复自动/手动数值撕裂 - 策略执行: 分钟路径接入后台单飞队列, 线程池 40→100, 渐进式结果隔离 - 性能: 策略矩阵请求内缓存、策略缓存内存镜像(mtime 指纹失效)、坏策略向量化 - 前端: 分时缩略图着色改为过昨收基准线判定、数字输入草稿态、RunningHub 折扣动态化 - RunningHub: 透传忙闲时定价并展示当前时段
98 lines
3.6 KiB
Python
98 lines
3.6 KiB
Python
"""strategy_cache 的进程内内存镜像 (轮询瘦身)。
|
|
|
|
/cached-summary 前端每 2s 轮询一次, 无镜像时每次都读 13MB JSON 再解析 (50-100ms)。
|
|
镜像契约:
|
|
- read_cache 在 (st_mtime_ns, st_size) 指纹未变时返回共享对象 (is 同一对象), 调用方只读;
|
|
- 外部改写文件 → 指纹变 → 重读新内容, 不返回陈旧镜像;
|
|
- write_cache 写入成功直填镜像: 写后读不触发重解析 (read-modify-write 也走镜像);
|
|
- clear_cache / 文件消失 → 镜像条目一并清除。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
|
|
from app.services import strategy_cache
|
|
|
|
|
|
def _result(*symbols: str) -> dict:
|
|
return {
|
|
"total": len(symbols),
|
|
"as_of": "2026-10-19",
|
|
"rows": [{"symbol": s} for s in symbols],
|
|
}
|
|
|
|
|
|
def _spy_json_loads(monkeypatch) -> dict:
|
|
"""统计 JSON 解析次数 — 镜像命中时不应发生解析。"""
|
|
calls = {"n": 0}
|
|
real = strategy_cache.json.loads
|
|
|
|
def counting(text):
|
|
calls["n"] += 1
|
|
return real(text)
|
|
|
|
monkeypatch.setattr(strategy_cache.json, "loads", counting)
|
|
return calls
|
|
|
|
|
|
def test_read_returns_shared_mirror_object_without_reparse(tmp_path, monkeypatch):
|
|
strategy_cache.write_cache(tmp_path, "2026-10-19", {"s1": _result("000001.SZ")})
|
|
calls = _spy_json_loads(monkeypatch)
|
|
|
|
first = strategy_cache.read_cache(tmp_path)
|
|
second = strategy_cache.read_cache(tmp_path)
|
|
|
|
assert first is second # 镜像命中: 同一对象
|
|
assert first["results"]["s1"]["rows"][0]["symbol"] == "000001.SZ"
|
|
assert calls["n"] == 0 # write_cache 已直填镜像, 读零解析
|
|
|
|
|
|
def test_second_write_merges_via_mirror_then_reads_fresh(tmp_path, monkeypatch):
|
|
strategy_cache.write_cache(tmp_path, "2026-10-19", {"s1": _result("000001.SZ")})
|
|
calls = _spy_json_loads(monkeypatch)
|
|
|
|
strategy_cache.write_cache(tmp_path, "2026-10-19", {"s2": _result("600000.SH")})
|
|
cached = strategy_cache.read_cache(tmp_path)
|
|
|
|
assert set(cached["results"]) == {"s1", "s2"}
|
|
assert calls["n"] == 0 # read-modify-write 的读与随后的读全走镜像
|
|
|
|
|
|
def test_external_rewrite_invalidates_mirror(tmp_path):
|
|
strategy_cache.write_cache(tmp_path, "2026-10-19", {"s1": _result("000001.SZ")})
|
|
assert set(strategy_cache.read_cache(tmp_path)["results"]) == {"s1"}
|
|
|
|
# 模拟外部进程改写缓存文件 (绕过 write_cache), 并显式改 mtime 规避
|
|
# 文件系统时间戳分辨率导致的指纹碰撞
|
|
path = tmp_path / "user_data" / "strategy_cache.json"
|
|
external = {
|
|
"as_of": "2026-10-19",
|
|
"results": {"ext": _result("300750.SZ")},
|
|
"today_ever_matched": {},
|
|
"today_ever_rows": {},
|
|
"updated_at": 1,
|
|
}
|
|
path.write_text(json.dumps(external), encoding="utf-8")
|
|
os.utime(path, ns=(10**9, 10**9))
|
|
|
|
cached = strategy_cache.read_cache(tmp_path)
|
|
assert set(cached["results"]) == {"ext"} # 读到磁盘新内容, 非陈旧镜像
|
|
|
|
|
|
def test_clear_cache_drops_mirror(tmp_path):
|
|
strategy_cache.write_cache(tmp_path, "2026-10-19", {"s1": _result("000001.SZ")})
|
|
assert strategy_cache.read_cache(tmp_path) is not None
|
|
|
|
strategy_cache.clear_cache(tmp_path)
|
|
assert strategy_cache.read_cache(tmp_path) is None
|
|
|
|
|
|
def test_file_deleted_externally_mirror_not_leaked(tmp_path):
|
|
strategy_cache.write_cache(tmp_path, "2026-10-19", {"s1": _result("000001.SZ")})
|
|
assert strategy_cache.read_cache(tmp_path) is not None
|
|
|
|
# 外部直接删文件 (非 clear_cache): stat 失败路径须清镜像并返回 None
|
|
(tmp_path / "user_data" / "strategy_cache.json").unlink()
|
|
assert strategy_cache.read_cache(tmp_path) is None
|