# 群晖NAS部署sanguo_vnpy回测服务方案 **编制人**:姜维 伯约 **日期**:2026-04-28(第3轮修订) **状态**:调研完成,待实施 --- ## 一、问题复现与根因分析 ### 1.1 现象 从局域网任意机器访问: - `curl http://192.168.2.154:8888/` → ✅ 302(Jupyter正常) - `curl http://192.168.2.154:8088/api/backtest/health` → ❌ Connection refused ### 1.2 根因(5重问题) | # | 问题 | 严重度 | 影响 | |---|------|--------|------| | **1** | entrypoint.sh 未启动 backtest-service | 🔴 阻断 | 容器内8088端口根本无进程监听 | | **2** | 目录名 `backtest-service` 含连字符,不能做Python包 | 🔴 阻断 | 无法用 `python -m` 或 `uvicorn backtest_service.main:app` 启动 | | **3** | models.py 使用 `ApiResponse[T](BaseModel)` 语法 | 🔴 阻断 | Python 3.12+语法,容器内Python 3.10不支持 | | **4** | executor.py 使用 vnpy 3.x API,与容器内vnpy 4.3.0不兼容 | 🔴 阻断 | 3个import失败 + Interval枚举值缺失 + API签名变化 | | **5** | main.py 的 `uvicorn.run("main:app")` 路径错误 | 🟡 修复 | uvicorn重导入时相对导入失败 | ### 1.3 根因4详情:vnpy 3.x → 4.x API变更 executor.py 是vnpy深度集成代码,以下import在vnpy 4.3.0下全部失败: | executor.py (3.x写法) | 4.x正确写法 | 状态 | |---|---|---| | `from vnpy.trader.event import EventEngine` | `from vnpy.event import EventEngine` | ✅ 可修复 | | `from vnpy.trader.backtesting import BacktestingEngine` | `from vnpy_ctastrategy.backtesting import BacktestingEngine` | ✅ 可修复(需安装包) | | `from vnpy.trader.database import database_manager` | `from vnpy.trader.database import get_database` | ✅ 可修复(需安装vnpy_sqlite) | | `from vnpy.trader.strategy import Strategy` | `from vnpy_ctastrategy import CtaStrategyApp` | ⚠️ 需确认 | | `Interval.FIVE_MINUTE` 等细分枚举 | 4.x只有 `MINUTE/HOUR/DAILY/WEEKLY/TICK` | ⚠️ API设计需调整 | | `BacktestingEngine.set_parameters(data=...)` | 4.x API签名可能变化 | ⚠️ 需逐个验证 | **结论**:executor.py与vnpy 4.x的API差异过大,不适合逐行修补,建议**重写executor.py适配vnpy 4.x**。 ### 1.4 已排除项 - ❌ 端口映射问题:iptables DNAT 8088→172.17.0.2:8088 正确 - ❌ DSM防火墙问题:Jupyter使用同模式可访问 - ❌ Docker网络问题:bridge模式正常工作 - ❌ 进程崩溃问题:服务从未启动过,不存在崩溃 --- ## 二、现有资源盘点 ### 2.1 NAS硬件 | 项目 | 规格 | |------|------| | 型号 | DS216+II | | CPU | Intel Celeron N3060 @ 1.60GHz (2核) | | 内存 | 8GB (可用约6.4GB) | | 磁盘 | 3.5TB总量, 1.6TB可用 | | Docker | 20.10.3 (需sudo) | ### 2.2 容器内环境 ``` 容器ID: 8fc55af3d27d 镜像: sanguo_vnpy:with-scripts Python: 3.10.20 vnpy: 4.3.0(核心包) vnpy相关: vnpy_webtrader 1.1.0(已安装) 缺失包: vnpy_ctastrategy, vnpy_sqlite(已手动验证可安装) 运行服务: Jupyter Lab(8888), SSH(22) ✅ 未运行: backtest-service(8088) ❌ ``` ### 2.3 数据资源(NAS SMB共享) ``` /Volumes/stock/A股数据/日线数据/ — 日线历史行情 /Volumes/stock/A股数据/分钟线数据/ — 分钟线数据 /Volumes/stock/A股数据/财务数据/ — 财务数据 ``` --- ## 三、部署方案 ### 3.1 架构图 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 群晖 NAS 192.168.2.154 │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Docker Container: sanguo_vnpy │ │ │ │ │ │ │ │ entrypoint.sh │ │ │ │ ├── sshd -D (22) ──→ 2222 ✅已有 │ │ │ │ ├── jupyter-lab (8888) ──→ 8888 ✅已有 │ │ │ │ └── uvicorn backtest_service (8088) ──→ 8088 🆕新增 │ │ │ │ │ │ │ │ /app/scripts/backtest_service/ (下划线目录名) │ │ │ │ /app/backtest_jobs/ (回测结果) │ │ │ │ /app/data/ (volume → /volume1/stock/A股数据) │ │ │ └─────────────────────────────────────────────────────────┘ │ │ ↑ Docker bridge (172.17.0.0/16) │ │ ↑ iptables DNAT: 8088→172.17.0.2:8088 │ └─────────────────────────────────────────────────────────────────┘ ↑ 局域网 192.168.2.0/24 ↑ ┌─────────────────┐ ┌─────────────────┐ │ Mac mini │ │ Windows Node │ │ 192.168.2.153 │ │ 192.168.2.33 │ │ curl :8088 │ │ 回测任务提交 │ └─────────────────┘ └─────────────────┘ ``` ### 3.2 修复清单(6处改动) | # | 文件 | 改动 | 说明 | |---|------|------|------| | 1 | **Dockerfile** | COPY后 `mv backtest-service backtest_service` | 目录名含连字符不能做Python包 | | 2 | **entrypoint.sh** | 添加 uvicorn 启动命令 | `PYTHONPATH=/app/scripts uvicorn backtest_service.main:app` | | 3 | **models.py** | `ApiResponse[T](BaseModel)` → `ApiResponse(BaseModel, Generic[T])` | Python 3.10兼容 | | 4 | **main.py** | `uvicorn.run("main:app")` → `uvicorn.run("backtest_service.main:app")` | 包路径启动 | | 5 | **executor.py** | 重写,适配vnpy 4.x API | 最核心的改动 | | 6 | **requirements-base.txt** | 添加 `vnpy_ctastrategy` 和 `vnpy_sqlite` | 缺失的vnpy插件 | **关键决策:18处相对导入零改动**(司马懿方案)。通过`PYTHONPATH=/app/scripts uvicorn backtest_service.main:app`包方式启动,所有`from .xxx import yyy`自然工作。 ### 3.3 executor.py重写要点 executor.py是与vnpy深度集成的核心文件,需要适配4.x API变更: ```python # === vnpy 4.x import 路径 === from vnpy.event import EventEngine # 原: vnpy.trader.event from vnpy_ctastrategy.backtesting import BacktestingEngine # 原: vnpy.trader.backtesting from vnpy.trader.constant import Interval # 不变 from vnpy.trader.database import get_database # 原: database_manager # === Interval 枚举变更 === # 3.x: FIVE_MINUTE, FIFTEEN_MINUTE, THIRTY_MINUTE, FOUR_HOUR # 4.x: 只有 MINUTE, HOUR, DAILY, WEEKLY, TICK INTERVAL_MAP = { "1m": Interval.MINUTE, "5m": Interval.MINUTE, # 4.x不再细分,统一用MINUTE "15m": Interval.MINUTE, "30m": Interval.MINUTE, "1h": Interval.HOUR, "4h": Interval.HOUR, "1d": Interval.DAILY, "1w": Interval.WEEKLY, } # === 数据加载方式变更 === # 3.x: database_manager.query_history(req) # 4.x: db = get_database(); data = db.query_bar_data(symbol, exchange, interval, start, end) # 或者从CSV文件直接加载: engine.load_data(file_path) ``` **重写策略**:保持API接口不变(submit/status/result/health),只改内部vnpy调用逻辑。回测引擎调用改用 `engine.load_data()` 从CSV文件加载,绕过数据库兼容问题(NAS上数据本来就在CSV文件里)。 ### 3.4 修改后的entrypoint.sh ```bash #!/bin/bash set -e echo "==========================================" echo " sanguo_vnpy Docker 容器启动中..." echo "==========================================" # SSH服务 sudo /usr/sbin/sshd -D & # Jupyter Lab jupyter lab --ip=0.0.0.0 --port=8888 --no-browser \ --NotebookApp.token='sanguo123' \ --NotebookApp.password='' \ --NotebookApp.allow_origin='*' & # 自动化回测服务(uvicorn包方式启动) mkdir -p /app/logs /app/backtest_jobs # 兼容旧镜像:如果目录名含连字符,创建符号链接 if [ -d /app/scripts/backtest-service ] && [ ! -d /app/scripts/backtest_service ]; then ln -sf /app/scripts/backtest-service /app/scripts/backtest_service fi PYTHONPATH=/app/scripts uvicorn backtest_service.main:app \ --host 0.0.0.0 --port 8088 \ >> /app/logs/backtest-service.log 2>&1 & BT_PID=$! echo "回测服务已启动 (PID=$BT_PID, 端口8088)" # code-server code-server & sleep 5 # 健康检查 if curl -sf http://localhost:8088/api/backtest/health > /dev/null 2>&1; then echo "✅ 回测服务健康检查通过" else echo "⚠️ 回测服务尚未就绪,检查日志: /app/logs/backtest-service.log" fi echo "" echo "✅ sanguo_vnpy 环境启动成功!" echo "" echo "访问地址:" echo " Jupyter Lab: http://localhost:8888 (token: sanguo123)" echo " 回测服务: http://localhost:8088/api/backtest/health" echo " 回测API文档: http://localhost:8088/docs" echo " SSH: ssh -p 2222 vnpy@localhost (password: sanguo123)" echo "" tail -f /dev/null ``` ### 3.5 Dockerfile增量修改 ```dockerfile # 在 COPY scripts 行之后添加: COPY --chown=vnpy:vnpy scripts /app/scripts RUN find /app/scripts -name "*.sh" -type f -exec chmod +x {} \; # 目录名含连字符不能做Python包,重命名为下划线 RUN if [ -d /app/scripts/backtest-service ]; then \ mv /app/scripts/backtest-service /app/scripts/backtest_service; \ fi # 创建日志和回测结果目录 RUN mkdir -p /app/logs /app/backtest_jobs # 在 requirements-base.txt 中添加: # vnpy_ctastrategy>=1.0.0 # vnpy_sqlite>=1.0.0 ``` ### 3.6 docker run命令 ```bash sudo /var/packages/Docker/target/usr/bin/docker run -d \ --name sanguo_vnpy \ --restart unless-stopped \ -m 4g --cpus=1.5 \ -p 8888:8888 \ -p 8088:8088 \ -p 8000:8000 \ -p 2222:22 \ -v /volume1/stock/A股数据:/app/data:ro \ -v /volume1/stock/回测结果:/app/backtest_jobs \ sanguo_vnpy:latest ``` --- ## 四、验证步骤 ### 4.1 交付标准验证 ```bash # 标准一:Health端点返回200 curl -s -o /dev/null -w "%{http_code}" http://192.168.2.154:8088/api/backtest/health # 预期: 200 # 标准二:提交回测任务并获取结果 TASK_ID=$(curl -s -X POST http://192.168.2.154:8088/api/backtest/submit \ -H "Content-Type: application/json" \ -d '{ "strategy_name": "test_ma_cross", "strategy_code": "class TestStrategy(CtaTemplate): ...", "parameters": {"fast_window": 5, "slow_window": 20}, "start_date": "2024-01-01", "end_date": "2024-12-31", "symbol": "000001.SZ", "interval": "d" }' | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['task_id'])") # 查询状态 curl http://192.168.2.154:8088/api/backtest/status/$TASK_ID # 获取结果 curl http://192.168.2.154:8088/api/backtest/result/$TASK_ID # 标准三:自动恢复 sudo /var/packages/Docker/target/usr/bin/docker kill sanguo_vnpy sleep 15 curl -s -o /dev/null -w "%{http_code}" http://192.168.2.154:8088/api/backtest/health # 预期: 200 ``` ### 4.2 回滚方案 | 场景 | 操作 | |------|------| | 回测服务启动失败 | 容器仍可用(Jupyter/SSH正常),只影响8088 | | 需要完全回滚 | `docker stop && docker rm`,用旧镜像 `sanguo_vnpy:with-scripts` 重建 | | executor.py有bug | `docker cp` 修复的文件进容器,不重启容器 | | 代码改坏 | git checkout恢复,重新docker cp | --- ## 五、实施计划 | Phase | 操作 | 耗时 | 前置条件 | |-------|------|------|---------| | **1. 快速验证** | 容器内手动修复+启动backtest-service | 10min | 无 | | **2. 代码修复** | 修复models.py/main.py/entrypoint.sh(4处) | 15min | Phase 1通过 | | **3. executor重写** | 适配vnpy 4.x API | 30min | Phase 2通过 | | **4. 重建镜像** | 修改Dockerfile + docker build + 部署 | 30min | Phase 3通过 | | **5. 交付验证** | 执行4.1全部验证 | 10min | Phase 4完成 | **Phase 1验证命令**(在当前运行容器内,不重建镜像): ```bash # SSH到NAS ssh admin@192.168.2.154 # 进入容器 sudo /var/packages/Docker/target/usr/bin/docker exec -it sanguo_vnpy bash # 容器内操作: # 1. 安装缺失包 pip3 install vnpy_ctastrategy vnpy_sqlite # 2. 创建修复后的目录 rm -rf /tmp/backtest_service cp -r /app/scripts/backtest-service /tmp/backtest_service # 3. 修复models.py泛型语法(sed替换,见3.2节#3) # 4. 修复main.py的uvicorn路径(sed替换,见3.2节#4) # 5. 用uvicorn启动 PYTHONPATH=/tmp uvicorn backtest_service.main:app --host 0.0.0.0 --port 8088 # 6. 另一个终端验证 curl http://localhost:8088/api/backtest/health ``` --- ## 六、关键技术决策 | 决策点 | 选择 | 理由 | |--------|------|------| | 启动方式 | `PYTHONPATH=... uvicorn backtest_service.main:app` | 18处相对导入零改动(司马懿方案) | | 目录名 | `backtest-service` → `backtest_service` | Python包名不允许连字符 | | 数据加载 | 从CSV文件直接加载 | 绕过vnpy 4.x数据库API变更,NAS数据本来在CSV里 | | 网络模式 | bridge(默认) | Jupyter已验证可用 | | 重启策略 | `--restart unless-stopped` | 异常崩溃自动恢复 | | 资源限制 | 4GB内存/1.5CPU | 给NAS留足余量 | --- ## 附录:容器内实际验证日志 ``` [2026-04-28 08:50] 容器内 pip list | grep vnpy → vnpy 4.3.0, vnpy_webtrader 1.1.0 [2026-04-28 08:51] vnpy 4.3.0 无 BacktestingEngine(拆分到vnpy_ctastrategy包) [2026-04-28 08:52] pip install vnpy_ctastrategy vnpy_sqlite → 成功 [2026-04-28 08:53] from vnpy_ctastrategy.backtesting import BacktestingEngine → OK [2026-04-28 08:53] from vnpy.event import EventEngine → OK [2026-04-28 08:53] Interval枚举: 4.x只有MINUTE/HOUR/DAILY/WEEKLY/TICK [2026-04-28 08:54] ApiResponse[T]语法 → Python 3.10不支持,需改Generic[T] ```