Files
easy_tdx_max/easy_tdx.spec
T
Justin Gu e374a0da28 release: v1.32.6 — 两周改动深度审查全面修复(回测口径三件套/LLM 安全加固/涨停价舍入/时区统一/缓存与竞态等 58 处)
对 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 全绿。
2026-09-06 22:16:48 +08:00

131 lines
5.3 KiB
RPMSpec
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- mode: python ; coding: utf-8 -*-
"""PyInstaller spec — 把 easy-tdx + Vue 前端打包成单一 Windows EXE。
构建前提(CI 会自动完成,本地手动构建需自行执行)::
1. pip install -e ".[web,packaging,baostock]" pyinstaller
# baostock 为 EXE 内置兜底数据源(见下方 hiddenimports),缺它会
# 被 spec 顶部的显式检查拦下——这是有意设计,防止静默产出无兜底的 EXE
2. cd web-ui && npm ci && npm run build # 产出 web-ui/dist/
3. pyinstaller easy_tdx.spec # 产出 dist/easy-tdx.exe
设计要点:
- ``--onefile``:单 EXE,老人双击即用。首次启动解压到临时目录需 2-5 秒。
- ``console=False``:无黑窗(Windows GUI 子系统)。
- ``collect_submodules('uvicorn')``uvicorn importlib 动态加载协议
实现,PyInstaller 静态分析漏掉会导致启动报 ModuleNotFoundError。
- ``collect_submodules('easy_tdx')``routers/backtest.strategies 等大量
延迟 import,全量收集避免遗漏。
- ``web-ui/dist web_dist``app.py ``_resolve_web_dist_dir`` 会从
``sys._MEIPASS/web_dist`` 读取前端资源。
- ``~/.easy_tdx`` SQLite 不在打包范围(用户运行时写入家目录),无需处理。
"""
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
hiddenimports: list[str] = []
hiddenimports += collect_submodules("uvicorn")
hiddenimports += collect_submodules("easy_tdx")
# baostock 兜底数据源(/bars 与 warehouse 的最后一级回退)在
# sources/baostock.py 里经 importlib 懒加载,静态分析扫不到,需显式声明。
# 显式检查而非依赖 collect_submodules 的失败形态(不同 PyInstaller 版本下
# 可能返回空列表静默跳过)——缺 baostock 时立即报错,防止静默产出
# 「兜底源缺失」的 EXE(运行期 /bars 自动回退静默失效,极难排查)。
import importlib.util
if importlib.util.find_spec("baostock") is None:
raise SystemExit(
"打包错误: 未找到 baostock——EXE 内置兜底数据源需要它。"
'请先执行 pip install baostock(或 pip install -e ".[web,packaging,baostock]"'
"再运行 pyinstaller easy_tdx.spec"
)
hiddenimports += collect_submodules("baostock")
# pandas / numpy / scipy 由 PyInstaller 自带 hook 处理(见
# PyInstaller/hooks/hook-pandas.* 等),无需手动 collect_submodules——
# 手动全量收集会把 numpy.typing.tests / pandas._numba.kernels 等可选/测试
# 模块也拖进来,既增加体积又制造噪音 WARNING。
# 系统托盘(打包态专用):pystray 在 Windows 用 win32 APIPillow 的
# 图像格式插件用 importlib 动态加载,两者都需要显式声明。
hiddenimports += collect_submodules("pystray")
datas: list[tuple[str, str]] = []
datas += collect_data_files("tzdata")
# Pillow 的图像格式插件(PngImagePlugin 等)随数据文件一起收集
datas += collect_data_files("PIL")
# 前端 dist 整目录收集到运行时 sys._MEIPASS/web_dist。
# 关键坑(v1.19.1 的 bug):PyInstaller 的 datas=[(src, dst)] 里 src 必须是
# **文件路径**而非目录——写成 ('web-ui/dist', 'web_dist') 会被当成不存在的
# 文件静默跳过(EXE 内 0 个前端文件 → 页面纯黑)。正确做法是用 glob 展开
# dist 下每个文件为独立的 (src_file, dest_dir) tuple。spec 在仓库根执行,
# 所以相对路径基于仓库根。
from pathlib import Path
_dist_root = Path("web-ui/dist")
if _dist_root.is_dir():
for f in _dist_root.rglob("*"):
if f.is_file():
# 目标目录:保留 dist 内的相对结构(如 assets/),
# 整体放到 web_dist/ 下。例如 web-ui/dist/assets/x.js
# → web_dist/assets/,文件名 x.js 由 PyInstaller 自动拼。
rel_parent = f.parent.relative_to(_dist_root)
dest_dir = f"web_dist/{rel_parent.as_posix()}" if str(rel_parent) != "." else "web_dist"
datas.append((str(f), dest_dir))
else:
print("WARNING: web-ui/dist 不存在,EXE 将无前端界面!请先 npm run build")
block_cipher = None
a = Analysis(
["src/easy_tdx/__main__.py"],
pathex=["src"],
binaries=[],
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
# matplotlib 仅 CLI 表格输出用,EXE 形态不需要
"matplotlib",
"tkinter",
"PyQt5",
"PyQt6",
"PySide2",
"PySide6",
# 测试框架不打进生产 EXE
"pytest",
"IPython",
"jupyter",
"notebook",
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name="easy-tdx",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
# GUI 子系统:双击不弹控制台黑窗。排查问题时改 console=True 重新打包。
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
# icon="assets/easy-tdx.ico", # 暂无图标资源;后续补
)