Files
tick-stock-panel/backend/tests/test_minute_range_api.py
T
shy3130 697c27bb02 feat(v0.2): 市场阶段与主线识别 + 因子挖掘全链路 + 数据层完善
- 市场环境: 新增情绪周期6阶段(冰点/启动/主升/高潮/退潮/修复, 连板梯队驱动,
  EMA平滑+2日确认+弱档否决, 平均段长9.7天)与概念/行业主线排名(涨停梯队聚合,
  可配置宽基/风格标签过滤); 市场环境页重构, regime 透明加列, 与5档state并存
- 挖掘: 因子与策略挖掘全链路(API/worker/进程锁/候选库/前端工作台/文档),
  周度调度默认关闭且永不自动发布
- 回测: 财务快照因子(点时口径), 批量回测预计算共享下期收益,
  信号路径矩阵列依赖展开修复(consecutive_limit_ups 缺列报错)
- 数据/性能: enriched 生成与预热治理, 重任务限流, 行情/K线缓存复用, 时区修复
- 测试: 后端全量 914 通过; GUI 黑盒验证截图存证 gui-test-screenshots/
2026-08-16 23:39:07 +08:00

114 lines
3.5 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"
# _get_stock_info 走 instruments 内存缓存 (不再走 execute_one DuckDB 查询)
repo.get_instruments.return_value = pl.DataFrame({
"symbol": ["600000.SH"],
"name": ["浦发银行"],
"total_shares": [1.0],
"float_shares": [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},
))