mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 22:34:18 +08:00
- 市场环境: 新增情绪周期6阶段(冰点/启动/主升/高潮/退潮/修复, 连板梯队驱动, EMA平滑+2日确认+弱档否决, 平均段长9.7天)与概念/行业主线排名(涨停梯队聚合, 可配置宽基/风格标签过滤); 市场环境页重构, regime 透明加列, 与5档state并存 - 挖掘: 因子与策略挖掘全链路(API/worker/进程锁/候选库/前端工作台/文档), 周度调度默认关闭且永不自动发布 - 回测: 财务快照因子(点时口径), 批量回测预计算共享下期收益, 信号路径矩阵列依赖展开修复(consecutive_limit_ups 缺列报错) - 数据/性能: enriched 生成与预热治理, 重任务限流, 行情/K线缓存复用, 时区修复 - 测试: 后端全量 914 通过; GUI 黑盒验证截图存证 gui-test-screenshots/
107 lines
3.9 KiB
Python
107 lines
3.9 KiB
Python
"""get_daily 复用 enriched 历史缓存的等价性测试。
|
|
|
|
个股对话框打开时 /api/kline/daily 每个行情 tick 调用一次; 旧路径每次
|
|
150 天扫描 + 全套指标重算, 新路径优先从预计算历史缓存裁剪。
|
|
本测试证明: 同一份数据下两条路径输出逐列一致, 且缓存命中时不触发扫描。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
|
|
import polars as pl
|
|
from polars.testing import assert_frame_equal
|
|
|
|
from app.tickflow.repository import KlineRepository
|
|
|
|
SYM = "600001.SH"
|
|
|
|
|
|
def _raw_frame(days: int = 80) -> pl.DataFrame:
|
|
"""构造 ~57 个交易日的 14 列形态数据 (复权价与原始价一致, 无除权)。"""
|
|
base = date(2026, 4, 1)
|
|
rows = []
|
|
price = 10.0
|
|
d = base
|
|
while len(rows) < days:
|
|
if d.weekday() < 5:
|
|
open_ = price * (1 + ((len(rows) % 7) - 3) * 0.004)
|
|
close = price * (1 + ((len(rows) % 5) - 2) * 0.006)
|
|
high = max(open_, close) * 1.01
|
|
low = min(open_, close) * 0.99
|
|
price = close
|
|
rows.append({
|
|
"symbol": SYM, "date": d,
|
|
"open": round(open_, 4), "high": round(high, 4),
|
|
"low": round(low, 4), "close": round(close, 4),
|
|
"volume": 10000.0 + (len(rows) % 10) * 500.0,
|
|
"amount": 1.0e7,
|
|
"raw_close": round(close, 4), "raw_high": round(high, 4),
|
|
"raw_low": round(low, 4),
|
|
})
|
|
d += timedelta(days=1)
|
|
return pl.DataFrame(rows).sort(["symbol", "date"])
|
|
|
|
|
|
def _bare_repo(raw: pl.DataFrame) -> tuple[KlineRepository, dict]:
|
|
repo = KlineRepository.__new__(KlineRepository)
|
|
repo._enriched_history_cache = None
|
|
repo._enriched_history_start = None
|
|
repo._enriched_cache = None
|
|
repo._enriched_cache_date = None
|
|
repo.get_instruments = lambda: pl.DataFrame() # type: ignore[method-assign]
|
|
repo.get_historical_shares = lambda: pl.DataFrame() # type: ignore[method-assign]
|
|
repo.get_enriched_latest = lambda: (pl.DataFrame(), None) # type: ignore[method-assign]
|
|
calls = {"scan": 0}
|
|
|
|
def _scan(symbol, start, end, columns):
|
|
calls["scan"] += 1
|
|
return raw.filter((pl.col("date") >= start) & (pl.col("date") <= end))
|
|
|
|
repo._scan_daily_symbol = _scan # type: ignore[method-assign]
|
|
return repo, calls
|
|
|
|
|
|
def test_get_daily_cache_path_matches_scan_path():
|
|
raw = _raw_frame()
|
|
dates = raw["date"].to_list()
|
|
start, end = dates[30], dates[-1]
|
|
|
|
# 旧路径: 无历史缓存 → 扫描 + 即时计算
|
|
repo_old, calls_old = _bare_repo(raw)
|
|
result_old = repo_old.get_daily(SYM, start, end)
|
|
assert calls_old["scan"] == 1
|
|
|
|
# 新路径: 预计算历史缓存 (同一真实计算栈 _compute_enriched_range 构建)
|
|
repo_new, calls_new = _bare_repo(raw)
|
|
hist = repo_new._compute_enriched_range(raw)
|
|
repo_new._enriched_history_cache = hist
|
|
repo_new._enriched_history_start = hist["date"].min()
|
|
result_new = repo_new.get_daily(SYM, start, end)
|
|
|
|
assert calls_new["scan"] == 0, "缓存命中时不得回退到扫描路径"
|
|
assert result_new.height == result_old.height
|
|
|
|
common = sorted(set(result_old.columns) & set(result_new.columns))
|
|
assert {"open", "close", "ma5", "ma20", "ma60", "rsi_14"} <= set(common)
|
|
assert_frame_equal(
|
|
result_old.sort("date").select(common),
|
|
result_new.sort("date").select(common),
|
|
check_exact=False,
|
|
rel_tol=1e-9,
|
|
)
|
|
|
|
|
|
def test_get_daily_falls_back_to_scan_when_cache_does_not_cover_start():
|
|
raw = _raw_frame()
|
|
dates = raw["date"].to_list()
|
|
|
|
repo, calls = _bare_repo(raw)
|
|
hist = repo._compute_enriched_range(raw)
|
|
repo._enriched_history_cache = hist
|
|
repo._enriched_history_start = hist["date"].min()
|
|
|
|
# 请求起点早于缓存覆盖 → 必须回退扫描路径
|
|
result = repo.get_daily(SYM, dates[0] - timedelta(days=5), dates[-1])
|
|
assert calls["scan"] == 1
|
|
assert not result.is_empty()
|