Files
tick-stock-panel/backend/tests/test_minute_range_api.py
T
shy3130 8c00f7a3fd feat(v0.2): 双击价格创建点位监控 + 分钟K补齐修复 + 超时设置独立
点位监控(双击图表):
- 日K/单日分时/多日分时价格主图支持双击, 自动预填目标价并按最新价判断上穿/下穿
- 成交量区与指标子图不触发; 自动生成通知文案, 用户手改后不再覆盖
- 已启用点位监控以横虚线显示 (上穿红/下穿绿), 并纳入三图 Y 轴范围
- 复用现有 PriceAlertDialog, 未改变后端 close>=/<= 规则语义与冷却机制

分钟K数据:
- 个股补齐强制回溯请求天数 (force_full_days), 修复切换日期范围只显示3天
- 单股并发补齐复用仓库写锁, 修复 Windows Parquet 临时文件替换失败
- DuckDB 读路径改用短生命周期 cursor, 释放对 Parquet 文件的占用

设置页:
- 数据任务超时从 DataSources 抽出为独立"超时设置"页签
- 设置侧栏垂直居中

验证: pytest 38 passed, pnpm build ok, git diff --check ok, 浏览器端到端 GUI 验证通过
2026-08-10 23:32:44 +08:00

108 lines
3.3 KiB
Python

"""多日分时 API 契约。"""
import asyncio
from datetime import date, datetime
from types import SimpleNamespace
from unittest.mock import MagicMock
import polars as pl
import pytest
from fastapi import HTTPException
from app.api import kline as kline_api
def _request(repo=None, capset=None):
return SimpleNamespace(
app=SimpleNamespace(
state=SimpleNamespace(
repo=repo or MagicMock(),
capabilities=capset or MagicMock(),
)
)
)
def test_minute_range_returns_latest_sessions_with_previous_closes():
repo = MagicMock()
repo.resolve_asset_type.return_value = "stock"
repo.execute_one.return_value = ("浦发银行", 1.0, 1.0)
repo.get_minute_range.return_value = pl.DataFrame({
"symbol": ["600000.SH"] * 3,
"datetime": [
datetime(2026, 8, 5, 1, 30),
datetime(2026, 8, 6, 1, 30),
datetime(2026, 8, 7, 1, 30),
],
"open": [10.0, 11.0, 12.0],
"high": [10.2, 11.2, 12.2],
"low": [9.8, 10.8, 11.8],
"close": [10.1, 11.1, 12.1],
"volume": [100.0, 110.0, 120.0],
"amount": [101_000.0, 122_100.0, 145_200.0],
})
repo.get_daily_asset.return_value = pl.DataFrame({
"date": [
date(2026, 8, 4),
date(2026, 8, 5),
date(2026, 8, 6),
date(2026, 8, 7),
],
"close": [9.9, 10.1, 11.1, 12.1],
})
result = kline_api.get_minute_range(_request(repo), "600000.SH", 2)
assert result["name"] == "浦发银行"
assert result["requested_days"] == 2
assert result["source"] == "local"
assert [session["date"] for session in result["sessions"]] == [
"2026-08-06",
"2026-08-07",
]
assert [session["prev_close"] for session in result["sessions"]] == [
10.1,
11.1,
]
assert result["sessions"][0]["rows"][0]["close"] == 11.1
def test_minute_range_does_not_read_stock_store_for_index():
repo = MagicMock()
repo.resolve_asset_type.return_value = "index"
repo.get_instruments_asset.return_value = pl.DataFrame()
result = kline_api.get_minute_range(_request(repo), "000001.SH", 10)
assert result["asset_type"] == "index"
assert result["sessions"] == []
repo.get_minute_range.assert_not_called()
def test_sync_minute_single_uses_requested_days(monkeypatch):
repo = MagicMock()
repo.resolve_asset_type.return_value = "stock"
capset = MagicMock()
sync = MagicMock(return_value=2400)
refresh = MagicMock()
monkeypatch.setattr(kline_api, "_minute_allowed", lambda _: True)
monkeypatch.setattr(kline_api.kline_sync, "sync_and_persist_minute", sync)
monkeypatch.setattr("app.jobs.daily_pipeline._refresh_single_view", refresh)
result = asyncio.run(kline_api.sync_minute_single(
_request(repo, capset),
{"symbol": "600000.SH", "days": 10},
))
assert result["rows"] == 2400
sync.assert_called_once_with(["600000.SH"], repo, capset, days=10, force_full_days=True)
refresh.assert_called_once_with(repo, "kline_minute")
def test_sync_minute_single_rejects_invalid_days():
with pytest.raises(HTTPException, match="days 必须在 1 到 30 之间"):
asyncio.run(kline_api.sync_minute_single(
_request(),
{"symbol": "600000.SH", "days": 0},
))