mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 23:54:21 +08:00
对 v1.21→v1.32.5 的 249 文件 4.2 万行改动做六路专项审查,本轮落地全部发现: 回测正确性:组合收益 fillna(0) 虚增、轮动停牌日过期价成交、单标的 WF 逐窗指标 被预热区稀释(三件套均带先红后绿回归);worst_drawdown 方向、grading 容错、 组合体检品种费率、寻优端点费率透传。 安全:LLM api_url 仅 http/https 且禁 userinfo(封死 file:// 读取与 Key 外送链)、 错误响应不回显原始 body、响应体 2MB 上限、配置原子写、坏配置字段级防御。 数据:涨跌停价整数分币舍入(67/318/90 个价位错 1 分漏判清零)、交易时段/采样/ provisional 统一沪时区、warehouse 增量缺口自动全量重拉、provisional 定点转正、 baostock 真故障抛错 + W/M 去 tradestatus(实测服务端报错,周月兜底此前从未工作) + 指数 vol 股→手(实测锚定)、ccpm 结构变更抛错。 Web API:缓存键补 count/vipdoc、NaN 清洗先于缓存、count>800 分页取全量、 submit 透传真实状态、pending 不再被淘汰成幽灵、watchlist/server 入参约束。 公式:FILTER 去副作用、0-1 值域误判收严、递归深度上限、REF 负移位显式禁止。 前端:4 处请求竞态序号守卫、Sparkline viewBox、北交所 market=2 映射、 空数据缓存死角、AI 弹窗卸载中止轮询、量能/资金日历口径修正。 CLI/CI:warehouse sync 失败 exit 1、参数校验干净报错、release 真实发布 SHA256、 CI 超时与缓存、spec 补 baostock 前提。 约 60 条回归测试先红后绿;pytest 1820 全过,ruff/mypy/vue-tsc/node --test 全绿。
173 lines
6.1 KiB
Python
173 lines
6.1 KiB
Python
"""count>800 的分页取数测试(离线)。
|
||
|
||
TDX 协议单次 get_security_bars 最多返回 800 根:旧实现里 multiseed /
|
||
rotation / formula 的单次调用在 count>800 时被服务器静默截断。本文件钉死
|
||
"分页取全量 + 页序正确 + 数据起点提前停止"行为。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
|
||
import pandas as pd
|
||
import pytest
|
||
|
||
pytest.importorskip("fastapi")
|
||
|
||
_PAGE_CAP = 800
|
||
|
||
|
||
class _CappedBarsClient:
|
||
"""模拟 TDX 服务器:单次最多返回 _PAGE_CAP 根,start 为回看偏移。"""
|
||
|
||
def __init__(self, total_bars: int = 3000):
|
||
self.total_bars = total_bars
|
||
self.calls: list[tuple[int, int]] = [] # (start, count)
|
||
|
||
def _make_page(self, start: int, n: int) -> pd.DataFrame:
|
||
"""start 偏移处往前 n 根(升序页);越过数据起点则截断为 0 根。"""
|
||
hi = self.total_bars - start # 本页最旧一根的全局序号(0 起)
|
||
lo = max(0, hi - n)
|
||
if hi <= 0:
|
||
return pd.DataFrame()
|
||
dates = pd.date_range("2020-01-01", periods=self.total_bars, freq="B")
|
||
idx = dates[lo:hi]
|
||
return pd.DataFrame(
|
||
{
|
||
"date": idx,
|
||
"open": 10.0,
|
||
"high": 11.0,
|
||
"low": 9.0,
|
||
"close": 10.5,
|
||
"vol": 1000.0,
|
||
"amount": 10000.0,
|
||
}
|
||
)
|
||
|
||
async def get_security_bars(self, market, code, category, start, count, **kw):
|
||
self.calls.append((int(start), int(count)))
|
||
return self._make_page(int(start), int(count))
|
||
|
||
|
||
# ── 共享分页辅助 ───────────────────────────────────────────────────────────────
|
||
|
||
|
||
def test_fetch_bars_paged_requests_multiple_pages():
|
||
"""count=2000 → 3 次请求(800/800/400),拼齐 2000 根且时间升序。"""
|
||
from easy_tdx.web.routers.backtest import _fetch_bars_paged
|
||
|
||
fake = _CappedBarsClient(total_bars=3000)
|
||
df = asyncio.run(_fetch_bars_paged(fake, "SZ:000001", "DAY", 2000))
|
||
|
||
assert fake.calls == [(0, 800), (800, 800), (1600, 400)]
|
||
assert len(df) == 2000
|
||
dates = pd.to_datetime(df["date"])
|
||
assert dates.is_monotonic_increasing # 页序拼接后必须升序
|
||
|
||
|
||
def test_fetch_bars_paged_stops_at_data_start():
|
||
"""数据起点不足一页时提前停止,不多发请求。"""
|
||
from easy_tdx.web.routers.backtest import _fetch_bars_paged
|
||
|
||
fake = _CappedBarsClient(total_bars=1000)
|
||
df = asyncio.run(_fetch_bars_paged(fake, "SH:600519", "DAY", 2000))
|
||
|
||
# 第二页只回 200 根(不足一页)= 数据起点,循环不再发第三笔请求
|
||
assert fake.calls == [(0, 800), (800, 800)]
|
||
assert len(df) == 1000
|
||
|
||
|
||
def test_fetch_bars_paged_small_count_single_call():
|
||
"""count≤800 仍单页取齐(不多打请求)。"""
|
||
from easy_tdx.web.routers.backtest import _fetch_bars_paged
|
||
|
||
fake = _CappedBarsClient(total_bars=3000)
|
||
df = asyncio.run(_fetch_bars_paged(fake, "SZ:000001", "DAY", 250))
|
||
assert fake.calls == [(0, 250)]
|
||
assert len(df) == 250
|
||
|
||
|
||
def test_fetch_bars_paged_empty_returns_empty_df():
|
||
from easy_tdx.web.routers.backtest import _fetch_bars_paged
|
||
|
||
fake = _CappedBarsClient(total_bars=0)
|
||
df = asyncio.run(_fetch_bars_paged(fake, "SZ:000001", "DAY", 800))
|
||
assert df.empty
|
||
|
||
|
||
# ── multiseed / rotation 端点(取数在 handler 内完成,POST 返回即可断言)──────
|
||
|
||
|
||
def _app_with(fake_client):
|
||
from fastapi import FastAPI
|
||
|
||
from easy_tdx.web.errors import register_exception_handlers
|
||
from easy_tdx.web.routers import backtest as backtest_mod
|
||
|
||
app = FastAPI()
|
||
register_exception_handlers(app)
|
||
app.include_router(backtest_mod.router, prefix="/api/v1")
|
||
app.state.tdx_client = fake_client
|
||
app.state.mac_client = None
|
||
app.state.ex_client = None
|
||
return app
|
||
|
||
|
||
def test_multiseed_fetches_full_count_via_paging():
|
||
"""multiseed count=900(>800)→ 每标的 2 次请求,不再被 800 截断。"""
|
||
pytest.importorskip("fastapi")
|
||
from fastapi.testclient import TestClient
|
||
|
||
fake = _CappedBarsClient(total_bars=3000)
|
||
with TestClient(_app_with(fake)) as client:
|
||
resp = client.post(
|
||
"/api/v1/backtest/multiseed/run/async",
|
||
json={
|
||
"strategy": "ma_cross",
|
||
"params": {"fast": 3, "slow": 6},
|
||
"stocks": ["SZ:000001", "SH:600519"],
|
||
"count": 900,
|
||
},
|
||
)
|
||
assert resp.status_code == 202, resp.text
|
||
|
||
# 2 标的 × 2 页
|
||
assert fake.calls == [(0, 800), (800, 100), (0, 800), (800, 100)]
|
||
|
||
|
||
def test_rotation_fetches_full_count_via_paging():
|
||
"""rotation count=900(>800)→ 每标的 2 次请求。"""
|
||
pytest.importorskip("fastapi")
|
||
from fastapi.testclient import TestClient
|
||
|
||
fake = _CappedBarsClient(total_bars=3000)
|
||
with TestClient(_app_with(fake)) as client:
|
||
resp = client.post(
|
||
"/api/v1/backtest/rotation/run/async",
|
||
json={
|
||
"stocks": ["SZ:000001", "SH:600519"],
|
||
"count": 900,
|
||
},
|
||
)
|
||
assert resp.status_code == 202, resp.text
|
||
|
||
assert fake.calls == [(0, 800), (800, 100), (0, 800), (800, 100)]
|
||
|
||
|
||
# ── formula 取数路径 ──────────────────────────────────────────────────────────
|
||
|
||
|
||
def test_formula_resolve_df_pages_full_count():
|
||
"""formula _resolve_df symbol 路径 count=2000 → 3 页拼齐且升序。"""
|
||
from easy_tdx.web.routers.backtest import _fetch_bars_paged # noqa: F401 需已存在
|
||
from easy_tdx.web.routers.formula import FormulaComputeRequest, _resolve_df
|
||
|
||
fake = _CappedBarsClient(total_bars=3000)
|
||
df = asyncio.run(
|
||
_resolve_df(fake, FormulaComputeRequest(text="C", symbol="SZ:000001", count=2000))
|
||
)
|
||
assert fake.calls == [(0, 800), (800, 800), (1600, 400)]
|
||
assert len(df) == 2000
|
||
dates = pd.to_datetime(df["date"])
|
||
assert dates.is_monotonic_increasing
|