mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 14:34:18 +08:00
- web/e2e_mock.py:EASY_TDX_E2E_MOCK=1 时 lifespan 替换 TDX/MAC 客户端为 确定性合成数据(CRC32 播种随机游走,分页语义对齐真实 /bars);回测/WF/ 评估/自选/策略库仍走真实后端,SSE 由 QuoteStreamer 真轮询(mock 下 2s 一拍) - web-ui:@playwright/test + playwright.config.ts(webServer 自动起 serve, EASY_TDX_CONFIG_DIR 指向每轮临时目录);e2e/ 5 用例覆盖看板五指数/SSE、 自选增删、回测全流程、附加分析(WF 柱状图+一条龙评估卡)、策略库保存 - package.json 加 test:e2e;CI frontend job 追加 E2E;verify_ci.sh 补前端段 - tests/unit/test_e2e_mock.py(11 例)守护 mock 契约;本地 npx playwright test 全绿
171 lines
6.8 KiB
Python
171 lines
6.8 KiB
Python
"""E2E 合成数据源(web/e2e_mock.py)单元测试。
|
||
|
||
守护两件事:
|
||
1. mock 客户端与真实客户端的**契约**(方法签名可用、返回列覆盖前端字段);
|
||
2. 数据的**确定性与分页语义**(E2E 断言可复现的前提)。
|
||
|
||
另有一个 TestClient 端到端用例:EASY_TDX_E2E_MOCK=1 下 /bars、/market/stat、
|
||
/mac/quote-list 返回合成数据(不连真实服务器)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import pandas as pd
|
||
import pytest
|
||
|
||
from easy_tdx.models.enums import Market
|
||
from easy_tdx.web.e2e_mock import (
|
||
E2E_MOCK_ENV,
|
||
MockMacClient,
|
||
MockTdxClient,
|
||
_page_bars,
|
||
_synth_ohlcv,
|
||
)
|
||
|
||
pytest.importorskip("fastapi")
|
||
|
||
|
||
# ── 数据生成内核 ─────────────────────────────────────────────────────────────
|
||
|
||
|
||
def test_synth_ohlcv_deterministic() -> None:
|
||
"""同一 (market, code) 两次生成结果逐位一致(E2E 断言可复现的前提)。"""
|
||
a = _synth_ohlcv("SH", "600519", 300)
|
||
b = _synth_ohlcv("SH", "600519", 300)
|
||
pd.testing.assert_frame_equal(a, b)
|
||
assert list(a.columns) == ["datetime", "open", "high", "low", "close", "vol", "amount"]
|
||
|
||
|
||
def test_synth_ohlcv_differs_across_symbols() -> None:
|
||
"""不同标的应有不同行情(避免看板五指数全长得一样)。"""
|
||
a = _synth_ohlcv("SH", "000001", 100)["close"].iloc[-1]
|
||
b = _synth_ohlcv("SZ", "000001", 100)["close"].iloc[-1]
|
||
assert a != b
|
||
|
||
|
||
def test_page_bars_newest_first_page() -> None:
|
||
"""分页语义与真实 /bars 一致:start=0 取最新 count 根,页内升序。"""
|
||
df = _synth_ohlcv("SH", "600519", 1000)
|
||
page0 = _page_bars(df, 0, 800)
|
||
assert len(page0) == 800
|
||
assert page0["datetime"].iloc[-1] == df["datetime"].iloc[-1] # 最新一根在页尾
|
||
assert page0["datetime"].is_monotonic_increasing
|
||
|
||
page1 = _page_bars(df, 800, 800)
|
||
assert len(page1) == 200
|
||
assert page1["datetime"].iloc[-1] < page0["datetime"].iloc[0] # 更早一段
|
||
|
||
assert len(_page_bars(df, 5000, 800)) == 0 # 越界翻页返回空
|
||
|
||
|
||
# ── Mock 客户端契约 ──────────────────────────────────────────────────────────
|
||
|
||
|
||
async def test_mock_tdx_quotes_contract() -> None:
|
||
"""quotes df:market 列是 Market 枚举(QuoteStreamer._df_to_dicts 依赖)、
|
||
字段覆盖 SSE 白名单(前端行情表 + 看板指数卡)。"""
|
||
client = MockTdxClient()
|
||
df = await client.get_security_quotes([(Market.SH, "000001"), (Market.SZ, "000001")])
|
||
assert len(df) == 2
|
||
# pandas 会把 IntEnum 列统一为 int64(真实客户端同样如此);
|
||
# QuoteStreamer._df_to_dicts 依赖 IntEnum 哈希相等完成 int → "SH" 映射
|
||
assert df["market"].iloc[0] == Market.SH
|
||
from easy_tdx.web.quote_streamer import _MARKET_NAMES
|
||
|
||
assert _MARKET_NAMES.get(df["market"].iloc[0]) == "SH"
|
||
for col in ("price", "pre_close", "open", "high", "low", "vol", "amount", "bid1", "ask_vol5"):
|
||
assert col in df.columns
|
||
# 指数与个股同名代码(SH000001 上证指数 / SZ000001 平安银行)行情不同
|
||
assert df["price"].iloc[0] != df["price"].iloc[1]
|
||
|
||
|
||
async def test_mock_tdx_bars_daily_date_column() -> None:
|
||
"""日线返回 date 列(旧 /bars 契约),分页 start/count 生效。"""
|
||
client = MockTdxClient()
|
||
df = await client.get_security_bars(Market.SH, "600519", 4, 0, 50) # 4 = DAY
|
||
assert "date" in df.columns and "datetime" not in df.columns
|
||
assert len(df) == 50
|
||
assert df["date"].is_monotonic_increasing
|
||
|
||
|
||
async def test_mock_tdx_minute_and_stat() -> None:
|
||
"""分时 240 点 + 市场统计单行(看板两块数据源)。"""
|
||
client = MockTdxClient()
|
||
minute = await client.get_minute_time_data(Market.SH, "000001")
|
||
assert len(minute) == 240
|
||
assert {"datetime", "price", "vol"} <= set(minute.columns)
|
||
|
||
stat = await client.get_market_stat()
|
||
assert len(stat) == 1
|
||
for col in ("up_count", "down_count", "total_count", "total_amount"):
|
||
assert col in stat.columns
|
||
|
||
|
||
async def test_mock_mac_kline_contract() -> None:
|
||
"""MAC get_stock_kline:datetime 列 + float_shares(bars 路由规整依赖)。"""
|
||
client = MockMacClient()
|
||
df = await client.get_stock_kline(1, "600519", 4, 0, 30, 1, adjust=None)
|
||
assert "datetime" in df.columns
|
||
assert "float_shares" in df.columns
|
||
assert len(df) == 30
|
||
|
||
|
||
async def test_mock_mac_quote_list_sort_and_columns() -> None:
|
||
"""排行行情:涨跌幅排序生效、列覆盖前端 RankRow 渲染需求。"""
|
||
client = MockMacClient()
|
||
desc = await client.get_stock_quotes_list(count=20, sort_order="DESC")
|
||
asc = await client.get_stock_quotes_list(count=20, sort_order="ASC")
|
||
assert len(desc) == 20
|
||
for col in ("market", "code", "name", "close", "pre_close", "amount"):
|
||
assert col in desc.columns
|
||
d_pct = desc["close"] / desc["pre_close"]
|
||
a_pct = asc["close"] / asc["pre_close"]
|
||
assert d_pct.iloc[0] >= d_pct.iloc[-1]
|
||
assert a_pct.iloc[0] <= a_pct.iloc[-1]
|
||
|
||
|
||
async def test_mock_mac_board_and_unusual() -> None:
|
||
"""板块列表/异动流:非空、字段齐(看板热度榜与异动雷达)。"""
|
||
client = MockMacClient()
|
||
boards = await client.get_board_list(count=500)
|
||
assert len(boards) > 0
|
||
assert {"code", "name", "price", "pre_close"} <= set(boards.columns)
|
||
|
||
unusual = await client.get_unusual(market=1, count=60)
|
||
assert 0 < len(unusual) <= 12
|
||
assert {"time", "code", "name", "desc", "value"} <= set(unusual.columns)
|
||
|
||
|
||
async def test_mock_close_is_noop() -> None:
|
||
"""lifespan 关闭路径调用 close() 不抛异常。"""
|
||
await MockTdxClient().close()
|
||
await MockMacClient().close()
|
||
|
||
|
||
# ── TestClient 端到端(mock 模式 lifespan)───────────────────────────────────
|
||
|
||
|
||
def test_app_serves_synthetic_data_in_mock_mode(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
"""EASY_TDX_E2E_MOCK=1 时全应用 lifespan 用合成客户端,行情端点 200。"""
|
||
from fastapi.testclient import TestClient
|
||
|
||
from easy_tdx.web import create_app
|
||
|
||
monkeypatch.setenv(E2E_MOCK_ENV, "1")
|
||
app = create_app()
|
||
with TestClient(app) as client:
|
||
bars = client.get(
|
||
"/api/v1/bars",
|
||
params={"market": "SH", "code": "600519", "category": "DAY", "count": 30},
|
||
)
|
||
assert bars.status_code == 200
|
||
assert len(bars.json()["data"]) == 30
|
||
|
||
stat = client.get("/api/v1/market/stat")
|
||
assert stat.status_code == 200
|
||
assert stat.json()["data"][0]["up_count"] > 0
|
||
|
||
rank = client.get("/api/v1/mac/quote-list", params={"count": 10})
|
||
assert rank.status_code == 200
|
||
assert len(rank.json()["data"]) == 10
|