# 群晖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]
```
