release: v1.32.6 — 两周改动深度审查全面修复(回测口径三件套/LLM 安全加固/涨停价舍入/时区统一/缓存与竞态等 58 处)

对 v1.21→v1.32.5 的 249 文件 4.2 万行改动做六路专项审查,本轮落地全部发现:

回测正确性:组合收益 fillna(0) 虚增、轮动停牌日过期价成交、单标的 WF 逐窗指标
被预热区稀释(三件套均带先红后绿回归);worst_drawdown 方向、grading 容错、
组合体检品种费率、寻优端点费率透传。

安全:LLM api_url 仅 http/https 且禁 userinfo(封死 file:// 读取与 Key 外送链)、
错误响应不回显原始 body、响应体 2MB 上限、配置原子写、坏配置字段级防御。

数据:涨跌停价整数分币舍入(67/318/90 个价位错 1 分漏判清零)、交易时段/采样/
provisional 统一沪时区、warehouse 增量缺口自动全量重拉、provisional 定点转正、
baostock 真故障抛错 + W/M 去 tradestatus(实测服务端报错,周月兜底此前从未工作)
+ 指数 vol 股→手(实测锚定)、ccpm 结构变更抛错。

Web API:缓存键补 count/vipdoc、NaN 清洗先于缓存、count>800 分页取全量、
submit 透传真实状态、pending 不再被淘汰成幽灵、watchlist/server 入参约束。

公式:FILTER 去副作用、0-1 值域误判收严、递归深度上限、REF 负移位显式禁止。

前端:4 处请求竞态序号守卫、Sparkline viewBox、北交所 market=2 映射、
空数据缓存死角、AI 弹窗卸载中止轮询、量能/资金日历口径修正。

CLI/CI:warehouse sync 失败 exit 1、参数校验干净报错、release 真实发布 SHA256、
CI 超时与缓存、spec 补 baostock 前提。

约 60 条回归测试先红后绿;pytest 1820 全过,ruff/mypy/vue-tsc/node --test 全绿。
This commit is contained in:
Justin Gu
2026-09-06 22:16:48 +08:00
parent d5cf9c14a9
commit e374a0da28
86 changed files with 4245 additions and 442 deletions
+100
View File
@@ -128,6 +128,67 @@ def test_limitup_empty_vipdoc(tmp_path):
assert eco.summary()["limit_up_count"] == 0
# ── 涨跌停价舍入(回归:浮点 floor(x*100+0.5) 在半分边界错 1 分)──────────────
def test_limit_price_matches_exchange_rounding_all_range():
"""_limit_price 与交易所 ROUND_HALF_UP 对 1.00~600.00 全价位零差异。
旧实现(float 乘后 floor)在 ±10% 档 67/318 个价位、±5% 档 90/884 个
价位算低 1 分(如 prev=1.15:涨停价应 1.27,旧算 1.26)。
"""
from decimal import ROUND_HALF_UP, Decimal
from easy_tdx.screen.limitup import _limit_price
for pct in (10, 5, 20, -10, -5, -20):
for cents in range(100, 60001):
prev = Decimal(cents).scaleb(-2)
expected = (
int(
(Decimal(cents) * (100 + pct) / 100).quantize(
Decimal("1"), rounding=ROUND_HALF_UP
)
)
/ 100
)
got = _limit_price(float(prev), pct)
assert got == expected, (prev, pct, got, expected)
def test_exchange_boundary_prices_detected(tmp_path):
"""半分边界价位的真实涨跌停不因浮点舍入漏判。
选点依据:.day 读回(raw×0.01)的浮点误差会抵消部分边界,33.05→36.36
与 2.65→2.39 是经读回仿真验证后旧实现(floor 浮点版)仍漏判的价位。
"""
from easy_tdx.screen.limitup import compute_limitup_ecology
# prev=33.05 → 交易所涨停价 36.36(旧实现误算 36.35 → 漏判涨停)
_write_stock(tmp_path, "sh", "600901", [33.05, 36.36])
# prev=2.65 → 交易所跌停价 2.39(旧实现误算 2.38 → 漏判跌停)
_write_stock(tmp_path, "sz", "000902", [2.65, 2.39])
eco = compute_limitup_ecology(tmp_path)
up = {e.code: e for e in eco.limit_up}
down = {e.code: e for e in eco.limit_down}
assert "600901" in up, f"33.05→36.36 应判涨停,实际 limit_up={up}"
assert up["600901"].streak == 1
assert "000902" in down, f"2.65→2.39 应判跌停,实际 limit_down={down}"
def test_history_boundary_prices_counted(tmp_path):
"""历史回补同样按交易所口径计涨跌停(33.05→36.36 / 2.65→2.39)。"""
from easy_tdx.screen.limitup import compute_limitup_history
_write_stock(tmp_path, "sh", "600901", [33.05, 36.36])
_write_stock(tmp_path, "sz", "000902", [2.65, 2.39])
rows = {r["date"]: r for r in compute_limitup_history(tmp_path, days=5)}
assert rows[20260802]["limit_up"] == 1
assert rows[20260802]["limit_down"] == 1
def test_limitup_endpoint_and_cache(vipdoc, monkeypatch):
"""端点返回 DictResponse 包装;60s 内命中缓存(扫描只跑一次)。"""
pytest.importorskip("fastapi")
@@ -164,3 +225,42 @@ def test_limitup_endpoint_and_cache(vipdoc, monkeypatch):
assert r2.json()["data"] == d1
assert calls["n"] == 1 # 第二次命中缓存
def test_limitup_endpoint_cache_key_includes_vipdoc(vipdoc, tmp_path, monkeypatch):
"""缓存键须含 vipdoc:不同 vipdoc 的请求在 TTL 内不互相命中。
旧实现 _limitup_cache 是单值缓存,先到的 vipdoc=A 结果会被 vipdoc=B
的请求在 60s TTL 内复用。
"""
pytest.importorskip("fastapi")
from fastapi import FastAPI
from fastapi.testclient import TestClient
from easy_tdx.web.errors import register_exception_handlers
from easy_tdx.web.routers import market as market_mod
other = tmp_path / "vipdoc_other"
(other / "sh" / "lday").mkdir(parents=True)
(other / "sh" / "lday" / "sh600100.day").write_bytes(
_day(20260801, 9.95, 11.0, 9.9, 11.0) + _day(20260802, 11.0, 12.1, 10.9, 12.1)
)
app = FastAPI()
register_exception_handlers(app)
app.include_router(market_mod.router, prefix="/api/v1")
app.state.tdx_client = object()
with TestClient(app) as client:
r1 = client.get("/api/v1/limitup-ecology", params={"vipdoc": str(vipdoc)})
assert r1.status_code == 200
r2 = client.get("/api/v1/limitup-ecology", params={"vipdoc": str(other)})
assert r2.status_code == 200
# 同 vipdoc 的第二次请求才命中缓存;不同 vipdoc 必须各自扫描
with TestClient(app) as client:
client.get("/api/v1/limitup-ecology", params={"vipdoc": str(vipdoc)})
client.get("/api/v1/limitup-ecology", params={"vipdoc": str(vipdoc)})
d = client.get("/api/v1/limitup-ecology", params={"vipdoc": str(other)}).json()["data"]
# other 目录只有 600100 一只 2 连板,不含 vipdoc 目录的 3 连板数据
assert d["summary"]["limit_up_count"] == 1