mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 16:54:20 +08:00
- ZIG 右侧突破回补策略:MyTT 新增 ZIG 之字转向(未来函数,含前视偏差警示); 波谷启动建仓挂硬止损(OCO)→ 见顶清仓记前高 → 右侧突破回补;路径依赖不实现 entry_exit_masks(向量化守护测试白名单);含寻优预设网格与 --strategy-file 独立文件 - 交易时段感知刷新:realtime/session.py(09:15~11:30:30 / 13:00~15:05)+ GET /market/session;看板 30/60/120s 轮询休市自动暂停(三态状态栏 + 开关持久化 + 手动刷新不受限);SSE/WS 既有会话语义不动 - 120 分钟 K 线:/bars?category=MIN_120(MAC 原生 Period.MINS×120 优先, 2×60M 相邻聚合兜底,标准客户端上限 400 根);前端周期选择器同步 - 逐 bar 衍生字段:/bars 与 /bars/index 附带 pre_close/change/change_pct/ amplitude_pct(pre_close≤0.01 兜底防除零) - 159 只核心龙头池:数据资产取自 Fork(东财全行业龙头名单,四组分层); universe=core 接入 screen scan / SignalScanner / StrengthRanker / market strength; GET /market/core-leaders + WebUI「龙头池」页(搜索/个股详情) - 多 Provider LLM 直连:easy_tdx.ai + /llm/*(DeepSeek/通义/智谱/Kimi/MiniMax/ OpenAI/Claude/Ollama/自定义,openai 兼容 + anthropic 原生双协议); 配置落盘 ~/.easy_tdx/llm.json(WebUI「AI 设置」页 ⇆ 手工编辑双向兼容, 文件>环境变量>预设;key 脱敏回显/CLEAR 清除);「AI 解读」后台任务化 (复用 task_runner,提交+轮询,不占 HTTP 连接);思考型模型空白正文防御 (reasoning_content 耗尽 max_tokens → 可操作报错;默认 16000); AI 解读历史页(自动归档 Prompt/正文/策略上下文 + 去回测带参引导) - WebUI 加固:SPA fallback 对未知 /api/* 返回 JSON 404(不再 200 HTML 伪装解析错); index.html 一律 Cache-Control: no-store(防缓存旧资源引用);路由兜底重定向; 全局风险提示常驻底栏 + 龙头池/AI 解读针对性免责声明 - 测试:新增 9 个单测文件共 59 例;黄金基线仅新增 zig_breakout 条目(其余零漂移); 全量 1448 例通过
85 lines
3.0 KiB
Python
85 lines
3.0 KiB
Python
"""SPA fallback 的 /api 守卫测试(v1.29)。
|
||
|
||
背景(实测踩坑):未注册的 ``/api/*`` 路径会掉进 StaticFiles 的 SPA
|
||
fallback 返回 200 + index.html——前端 ``resp.ok`` 为 true、``resp.json()``
|
||
抛 ``Unexpected token '<'``,把"服务是旧版本/端点不存在"伪装成前端解析
|
||
错误。守护:未知 /api 路径必须返回 JSON 404,前端路由路径仍回 index.html。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
fastapi_testclient = pytest.importorskip("fastapi.testclient")
|
||
|
||
|
||
def _make_app_with_ui(tmp_path: Path):
|
||
"""带假前端 dist 的 app(index.html + 一个资产文件)。"""
|
||
from easy_tdx.web.app import _create_app
|
||
|
||
dist = tmp_path / "dist"
|
||
dist.mkdir()
|
||
(dist / "index.html").write_text("<!doctype html><title>spa</title>", encoding="utf-8")
|
||
(dist / "test-asset.txt").write_text("asset", encoding="utf-8")
|
||
|
||
import easy_tdx.web.app as app_mod
|
||
|
||
original = app_mod._resolve_web_dist_dir
|
||
app_mod._resolve_web_dist_dir = lambda: dist # type: ignore[assignment]
|
||
try:
|
||
return _create_app(enable_mac=False, enable_ui=True)
|
||
finally:
|
||
app_mod._resolve_web_dist_dir = original # type: ignore[assignment]
|
||
|
||
|
||
@pytest.fixture()
|
||
def client(tmp_path):
|
||
app = _make_app_with_ui(tmp_path)
|
||
with fastapi_testclient.TestClient(app) as c:
|
||
yield c
|
||
|
||
|
||
def test_unknown_api_path_returns_json_404(client):
|
||
"""未注册的 /api 路径:JSON 404,绝不能是 200 HTML(SPA fallback)。"""
|
||
resp = client.get("/api/v1/llm/config-not-exist")
|
||
assert resp.status_code == 404
|
||
assert resp.headers["content-type"].startswith("application/json")
|
||
assert "<!doctype" not in resp.text.lower()
|
||
|
||
|
||
def test_unknown_api_post_returns_404_not_html(client):
|
||
resp = client.post("/api/v1/no-such-endpoint", json={})
|
||
assert resp.status_code in (404, 405)
|
||
assert "<!doctype" not in resp.text.lower()
|
||
|
||
|
||
def test_spa_route_still_serves_index(client):
|
||
"""前端路由路径(如 /llm)仍回 index.html(SPA 刷新场景)。"""
|
||
resp = client.get("/llm")
|
||
assert resp.status_code == 200
|
||
assert "spa" in resp.text
|
||
|
||
|
||
def test_static_asset_served(client):
|
||
assert client.get("/test-asset.txt").text == "asset"
|
||
|
||
|
||
def test_registered_api_route_unaffected(client):
|
||
"""已注册端点正常返回 JSON(守卫只拦未匹配路径)。"""
|
||
resp = client.get("/api/v1/market/session")
|
||
assert resp.status_code == 200
|
||
assert resp.json()["session_desc"]
|
||
|
||
|
||
def test_index_html_no_store(client):
|
||
"""入口 index.html 永远 no-store:防浏览器缓存旧资源引用(强刷仍见旧版)。"""
|
||
for path in ("/", "/llm", "/ai-history"):
|
||
resp = client.get(path)
|
||
assert resp.status_code == 200
|
||
assert resp.headers.get("cache-control") == "no-store", path
|
||
# 哈希文件名的静态资产不受影响(默认缓存语义)
|
||
asset = client.get("/test-asset.txt")
|
||
assert asset.headers.get("cache-control") != "no-store"
|