mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 22:34:18 +08:00
- fuyao dragon-tiger-list 三榜接入 (非路由数据集, tickflow 无对应能力) - services.dragon_tiger: 本地日K分区做交易日回退(规避接口非交易日报错), 历史日 JSON 按日缓存(不可变), 当日未发布自动回退上一期 - 卡片视觉对齐市场摘要条: 奖杯徽章/排名药丸/分段式tab/净买额比例条/席位奖牌, 收起两行摘要展开三榜明细, 点股票开日K预览 - AI 复盘 prompt 注入龙虎榜摘要段 (净买/净卖/机构/游资 Top), 未配置 fuyao 时空串不占段 - displayDate 联动: 历史复盘显示当期榜单
158 lines
6.0 KiB
Python
158 lines
6.0 KiB
Python
"""龙虎榜服务测试 (不依赖真实网络)。
|
|
|
|
覆盖: 交易日回退 (非交易日/目标日解析)、历史日 JSON 缓存命中与落盘、
|
|
fuyao 未配置降级、当日未发布 fallback_prev、彻底失败 no_data、
|
|
AI 复盘摘要段构建。
|
|
日期用 2026-08-27/28 (写作时为过去交易日), 与仓库既有绝对日期测试风格一致。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import date
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from app.plugins.fuyao.client import FuyaoError
|
|
from app.services import dragon_tiger as dt
|
|
|
|
|
|
def _mk_days(data_dir: Path, *days: str) -> None:
|
|
for d in days:
|
|
(data_dir / "kline_daily" / f"date={d}").mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
@pytest.fixture()
|
|
def data_dir(tmp_path: Path) -> Path:
|
|
_mk_days(tmp_path, "2026-08-26", "2026-08-27", "2026-08-28")
|
|
return tmp_path
|
|
|
|
|
|
class _FakeProvider:
|
|
"""按 (board, date) 记录调用; fail_dates 中的日期抛 FuyaoError。"""
|
|
|
|
def __init__(self, fail_dates: set[str] | None = None):
|
|
self.calls: list[tuple] = []
|
|
self.fail_dates = fail_dates or set()
|
|
|
|
def dragon_tiger(self, board_type: str, date: str | None) -> dict:
|
|
self.calls.append((board_type, date))
|
|
if date in self.fail_dates:
|
|
raise FuyaoError(f"code=3002: {date} 未就绪")
|
|
iso = date or "2026-08-28"
|
|
return {
|
|
"trade_date": iso,
|
|
"count": 2,
|
|
"stock_count": 2,
|
|
"stock_items": [
|
|
{"thscode": "600519.SH", "ticker": "600519", "name": "贵州茅台",
|
|
"change": 0.05, "net_value": 1.5e8, "net_rate": 0.01,
|
|
"buy_value": 2e8, "sell_value": 0.5e8, "hot_rank": 3, "range_days": 1,
|
|
"org_net_value": 0.8e8},
|
|
{"thscode": "000858.SZ", "ticker": "000858", "name": "五粮液",
|
|
"change": -0.03, "net_value": -0.6e8, "net_rate": -0.004,
|
|
"buy_value": 0.4e8, "sell_value": 1e8, "range_days": 3},
|
|
],
|
|
"hot_money_items": [
|
|
{"name": "宁波桑田路", "buying": 1.9e8,
|
|
"rows": [{"thscode": "600519.SH", "name": "贵州茅台",
|
|
"hot_money_item_net_value": 1.2e8}]},
|
|
],
|
|
}
|
|
|
|
|
|
def _use_provider(monkeypatch, provider) -> _FakeProvider:
|
|
monkeypatch.setattr(dt, "_provider", lambda: provider)
|
|
return provider
|
|
|
|
|
|
# ---- 交易日解析 ----
|
|
|
|
def test_resolve_rolls_back_non_trading_day(data_dir):
|
|
# 周日 08-30 → 最近分区 08-28
|
|
assert dt.resolve_trade_date(data_dir, date(2026, 8, 30)) == date(2026, 8, 28)
|
|
# 08-27 是交易日 → 原样
|
|
assert dt.resolve_trade_date(data_dir, date(2026, 8, 27)) == date(2026, 8, 27)
|
|
|
|
|
|
def test_resolve_older_than_all_partitions_returns_target(data_dir):
|
|
# 早于全部本地分区 → 原样返回, 由调用方显式尝试 (失败如实 no_data)
|
|
assert dt.resolve_trade_date(data_dir, date(2020, 1, 1)) == date(2020, 1, 1)
|
|
|
|
|
|
# ---- 状态与缓存 ----
|
|
|
|
def test_source_unavailable_without_fuyao(data_dir, monkeypatch):
|
|
monkeypatch.setattr(dt, "_provider", lambda: None)
|
|
out = dt.get_dragon_tiger(data_dir, None)
|
|
assert out["state"] == "source_unavailable"
|
|
|
|
|
|
def test_fetch_stores_cache_then_hits_cache(data_dir, monkeypatch):
|
|
provider = _use_provider(monkeypatch, _FakeProvider())
|
|
out = dt.get_dragon_tiger(data_dir, None) # 默认 → 最近分区 08-28
|
|
assert out["state"] == "ok"
|
|
assert out["trade_date"] == "2026-08-28"
|
|
assert len(provider.calls) == 3 # 三榜一次取齐
|
|
cache = data_dir / "dragon_tiger" / "date=2026-08-28.json"
|
|
assert cache.exists()
|
|
|
|
provider.calls.clear()
|
|
out2 = dt.get_dragon_tiger(data_dir, date(2026, 8, 30)) # 周日 → 08-28 → 命中缓存
|
|
assert out2["state"] == "ok" and out2["trade_date"] == "2026-08-28"
|
|
assert provider.calls == [] # 纯本地, 不打网络
|
|
|
|
|
|
def test_explicit_history_date_uses_cache(data_dir, monkeypatch):
|
|
provider = _use_provider(monkeypatch, _FakeProvider())
|
|
dt.get_dragon_tiger(data_dir, date(2026, 8, 27))
|
|
assert provider.calls[0] == ("all", "2026-08-27")
|
|
provider.calls.clear()
|
|
dt.get_dragon_tiger(data_dir, date(2026, 8, 27))
|
|
assert provider.calls == []
|
|
|
|
|
|
def test_unpublished_today_falls_back_to_prev(data_dir, monkeypatch):
|
|
# 08-28 拉取失败 (未发布/未就绪) → 回退 08-27, state=fallback_prev
|
|
provider = _use_provider(monkeypatch, _FakeProvider(fail_dates={"2026-08-28"}))
|
|
out = dt.get_dragon_tiger(data_dir, date(2026, 8, 28))
|
|
assert out["state"] == "fallback_prev"
|
|
assert out["trade_date"] == "2026-08-27"
|
|
assert out["requested_date"] == "2026-08-28"
|
|
# 回退日缓存以 ok 状态落盘 (不污染直查)
|
|
cached = json.loads((data_dir / "dragon_tiger" / "date=2026-08-27.json").read_text(encoding="utf-8"))
|
|
assert cached["state"] == "ok"
|
|
|
|
|
|
def test_total_failure_returns_no_data(data_dir, monkeypatch):
|
|
provider = _use_provider(monkeypatch, _FakeProvider(fail_dates={"2026-08-28", "2026-08-27"}))
|
|
out = dt.get_dragon_tiger(data_dir, date(2026, 8, 28))
|
|
assert out["state"] == "no_data"
|
|
assert "2026-08-28" in out.get("message", "")
|
|
|
|
|
|
def test_corrupt_cache_refetches(data_dir, monkeypatch):
|
|
provider = _use_provider(monkeypatch, _FakeProvider())
|
|
cache = data_dir / "dragon_tiger" / "date=2026-08-28.json"
|
|
cache.parent.mkdir(parents=True, exist_ok=True)
|
|
cache.write_text("{broken json", encoding="utf-8")
|
|
out = dt.get_dragon_tiger(data_dir, date(2026, 8, 28))
|
|
assert out["state"] == "ok"
|
|
assert provider.calls # 缓存损坏 → 重新拉取
|
|
|
|
|
|
# ---- AI 复盘摘要 ----
|
|
|
|
def test_build_recap_context_contains_summary(data_dir, monkeypatch):
|
|
_use_provider(monkeypatch, _FakeProvider())
|
|
ctx = dt.build_recap_context(data_dir)
|
|
assert "净买入居前" in ctx and "贵州茅台" in ctx
|
|
assert "机构净买居前" in ctx
|
|
assert "宁波桑田路" in ctx
|
|
|
|
|
|
def test_build_recap_context_empty_without_source(data_dir, monkeypatch):
|
|
monkeypatch.setattr(dt, "_provider", lambda: None)
|
|
assert dt.build_recap_context(data_dir) == ""
|