mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 14:34:18 +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 全绿。
131 lines
5.3 KiB
RPMSpec
131 lines
5.3 KiB
RPMSpec
# -*- 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 API,Pillow 的
|
||
# 图像格式插件用 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", # 暂无图标资源;后续补
|
||
)
|