mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
- 日K/分时批量单请求上限改为系统设置可配 (默认 400), 后端守卫与前端分块 读同一偏好值, 不再发出必然被拒的请求; 两档独立 (分时成本约为日K 20 倍) - _gzip_payload 懒净化: 无 NaN/inf 常态跳过 _json_safe 整树深拷贝, 峰值 -65% (40→14MB/块), CPU -25% - minute-batch/daily-batch 全程持有 Polars 帧直出 JSON (write_json 行式), 不再构建 Python dict: 96k 根序列化 1222ms→26ms, 峰值 69→18MB; 字节级等价由 tests/test_kline_batch_frames_json.py 锁定 (含 inf/NaN/None) - 白盒测试改为解析 Response body (HTTP 真实契约) - 随 kline.py 同文件: minute-batch 门控在全量分钟服务健康时放行本地读, 补拉限速改走 resolve_limit (×0.8, 与盘后同步同 rpm 桶) 实测: 300 只分时(7.2万根)整请求 191ms; 20 并发尖峰 +728MB→+60MB Signed-off-by: shy3130 <shy3130@qq.com>
150 lines
5.2 KiB
Python
150 lines
5.2 KiB
Python
"""批量端点单请求上限: 可配置 + 与前端分块同源。
|
|
|
|
背景: daily-batch / minute-batch 的 symbols 上限原先是三处硬编码 (后端 400、
|
|
日K分块 300、分时块 300), 改任一处不一致就全站 400。现在上限是用户偏好,
|
|
后端校验与前端分块读同一个值。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from app.services import preferences
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolated(tmp_path, monkeypatch):
|
|
path = tmp_path / "preferences.json"
|
|
monkeypatch.setattr(preferences, "_path", lambda: path)
|
|
preferences._invalidate_cache()
|
|
yield path
|
|
preferences._invalidate_cache()
|
|
|
|
|
|
def _write(path, data):
|
|
path.write_text(json.dumps(data), encoding="utf-8")
|
|
preferences._invalidate_cache()
|
|
|
|
|
|
# ── 偏好读写 ─────────────────────────────────────────────
|
|
|
|
def test_defaults_when_unset():
|
|
assert preferences.get_daily_batch_symbols() == 400
|
|
assert preferences.get_minute_batch_symbols() == 400
|
|
|
|
|
|
def test_setter_roundtrip(_isolated):
|
|
assert preferences.set_daily_batch_symbols(1500) == 1500
|
|
assert preferences.get_daily_batch_symbols() == 1500
|
|
assert preferences.set_minute_batch_symbols(600) == 600
|
|
assert preferences.get_minute_batch_symbols() == 600
|
|
|
|
|
|
@pytest.mark.parametrize("raw,expected", [(0, 50), (1, 50), (-10, 50)])
|
|
def test_daily_lower_bound_clamps(_isolated, raw, expected):
|
|
assert preferences.set_daily_batch_symbols(raw) == expected
|
|
|
|
|
|
@pytest.mark.parametrize("raw,expected", [(99999, 5000), (5001, 5000)])
|
|
def test_daily_upper_bound_clamps(_isolated, raw, expected):
|
|
assert preferences.set_daily_batch_symbols(raw) == expected
|
|
|
|
|
|
@pytest.mark.parametrize("raw,expected", [(0, 50), (2001, 2000), (99999, 2000)])
|
|
def test_minute_bounds_clamp(_isolated, raw, expected):
|
|
assert preferences.set_minute_batch_symbols(raw) == expected
|
|
|
|
|
|
def test_minute_default_kept_below_daily_range(_isolated):
|
|
"""分时上限区间比日K窄: 单只 240 根, 同标的数成本高一个量级。"""
|
|
assert preferences.set_minute_batch_symbols(3000) == 2000
|
|
assert preferences.set_daily_batch_symbols(3000) == 3000
|
|
|
|
|
|
def test_malformed_stored_value_falls_back_to_default(_isolated):
|
|
_write(_isolated, {"daily_batch_symbols": "not-a-number", "minute_batch_symbols": None})
|
|
assert preferences.get_daily_batch_symbols() == 400
|
|
assert preferences.get_minute_batch_symbols() == 400
|
|
|
|
|
|
def test_out_of_range_stored_value_is_clamped_on_read(_isolated):
|
|
"""手改 preferences.json 越界时按区间收敛, 不把非法值直接用于校验。"""
|
|
_write(_isolated, {"daily_batch_symbols": 999999, "minute_batch_symbols": 1})
|
|
assert preferences.get_daily_batch_symbols() == 5000
|
|
assert preferences.get_minute_batch_symbols() == 50
|
|
|
|
|
|
def test_setter_keeps_other_preferences(_isolated):
|
|
_write(_isolated, {"realtime_quotes_enabled": True})
|
|
preferences.set_daily_batch_symbols(800)
|
|
data = preferences.load()
|
|
assert data["realtime_quotes_enabled"] is True
|
|
assert data["daily_batch_symbols"] == 800
|
|
|
|
|
|
# ── 端点校验 (与偏好联动) ─────────────────────────────────
|
|
|
|
def _fake_req(repo=None):
|
|
from types import SimpleNamespace
|
|
|
|
return SimpleNamespace(
|
|
app=SimpleNamespace(
|
|
state=SimpleNamespace(repo=repo, capabilities=None),
|
|
)
|
|
)
|
|
|
|
|
|
def test_daily_endpoint_rejects_over_configured_limit(_isolated, monkeypatch):
|
|
from fastapi import HTTPException
|
|
|
|
from app.api import kline as kline_api
|
|
|
|
preferences.set_daily_batch_symbols(100)
|
|
body = {"symbols": [f"6000{i:02d}.SH" for i in range(101)], "days": 12}
|
|
with pytest.raises(HTTPException) as exc:
|
|
kline_api.get_daily_batch(_fake_req(), body)
|
|
assert exc.value.status_code == 400
|
|
assert "100" in exc.value.detail
|
|
assert "系统设置" in exc.value.detail
|
|
|
|
|
|
def test_daily_endpoint_accepts_at_configured_limit(_isolated, monkeypatch):
|
|
import polars as pl
|
|
|
|
from app.api import kline as kline_api
|
|
|
|
preferences.set_daily_batch_symbols(120)
|
|
|
|
class _Repo:
|
|
def resolve_asset_type(self, _s):
|
|
return "stock"
|
|
|
|
def get_daily_batch(self, *_a, **_kw):
|
|
return pl.DataFrame()
|
|
|
|
body = {"symbols": [f"6000{i:02d}.SH" for i in range(120)], "days": 12}
|
|
out = json.loads(kline_api.get_daily_batch(_fake_req(_Repo()), body).body)
|
|
assert out["data"] == {}
|
|
|
|
|
|
def test_minute_endpoint_rejects_over_configured_limit(_isolated):
|
|
from fastapi import HTTPException
|
|
|
|
from app.api import kline as kline_api
|
|
|
|
preferences.set_minute_batch_symbols(80)
|
|
body = {"symbols": [f"6000{i:02d}.SH" for i in range(81)]}
|
|
with pytest.raises(HTTPException) as exc:
|
|
kline_api.get_minute_batch(_fake_req(), body)
|
|
assert exc.value.status_code == 400
|
|
assert "80" in exc.value.detail
|
|
|
|
|
|
def test_limits_are_independent(_isolated):
|
|
"""日K 调高不影响分时上限 (两档独立, 不共用同一个数)。"""
|
|
preferences.set_daily_batch_symbols(1800)
|
|
preferences.set_minute_batch_symbols(120)
|
|
assert preferences.get_daily_batch_symbols() == 1800
|
|
assert preferences.get_minute_batch_symbols() == 120
|