mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 20:14:16 +08:00
点位监控(双击图表): - 日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 验证通过
108 lines
3.3 KiB
Python
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},
|
|
))
|