fix(portfolio): routes_portfolio 本地/SSH 自动检测(VPS后端405修复)

VPS 后端(schtasks sanguo-api → run_web.py)独立部署,之前没同步 routes_portfolio
致前端调 /api/v1/portfolio/backtest 报 405 Method Not Allowed。
- routes_portfolio 加本地/SSH 自动检测:os.path.isdir(_VPS_WORKDIR) 判定
  (VPS 有 C:\sanguo_vnpy_v2 → 本地直接 subprocess 跑 runner;Mac 无 → SSH 到 VPS)
  避免 VPS 后端 SSH 自连外网 IP 绕路,无需 env 配置同源两端自适应
- VPS 端同步 routes_portfolio.py + app.py 注册 portfolio_router + 重启 schtasks
- 验证:POST /api/v1/portfolio/backtest 从 405 → 401(路由注册成功,verify_token 生效)
This commit is contained in:
2026-07-19 08:18:53 +08:00
parent f074a6b420
commit c4042a50d1
+48 -33
View File
@@ -8,7 +8,9 @@ POST /portfolio/backtest: SSH 触发 VPS 跑 BulletTrade + all_weather,
from __future__ import annotations
import logging
import os
import subprocess
import sys
from typing import Any, Optional
from fastapi import APIRouter, Depends, Header, HTTPException
@@ -50,40 +52,53 @@ def run_portfolio_backtest(req: PortfolioBacktestRequest):
- 没有 tail/head/grep:用 python 后处理(本函数在 Mac 端直接解析 stdout)
- 中文路径:VPS_WORKDIR / userdata_mini 走 env(DEFAULT_DATA_PROVIDER=miniqmt)
"""
# 远端命令手动拼接(不用 shlex.quote:它产 POSIX 单引号,Windows cmd 不认单引号致 cd 失败;
# 且不用 set X=Y:runner_backtest 顶部 os.environ.setdefault 已自处理 env,早于 bullet_trade import,
# 避免 cmd "set X=Y &&" 尾空格进 value(变 "miniqmt ")致 bullet_trade provider 名匹配失败)。
remote_cmd = (
f"cd {_VPS_WORKDIR} && "
f"{_VPS_PYTHON} -X utf8 -m sanguo_portfolio.runner_backtest --json "
f"--start {req.start_date} --end {req.end_date} "
f"--cash {req.initial_cash} --benchmark {req.benchmark} "
f"--max-pool 30"
)
ssh_argv = [
"ssh",
"-o", "ConnectTimeout=15",
"-o", "StrictHostKeyChecking=no",
_VPS_HOST,
remote_cmd,
]
logger.info("[portfolio] SSH 触发: %s", ssh_argv[-1])
try:
proc = subprocess.run(
ssh_argv,
capture_output=True,
text=True,
timeout=_VPS_TIMEOUT,
check=False,
# 本地模式(VPS 后端直接跑 runner,避免 SSH 自连外网 IP 绕路);
# SSH 模式(Mac 后端 → VPS)。env SANGUO_PORTFOLIO_LOCAL=1 切本地。
# 自动检测:VPS 上 _VPS_WORKDIR(C:\sanguo_vnpy_v2) 存在 → 本地直接跑 runner;
# Mac 上无该目录 → SSH 到 VPS。无需 env 配置,同源代码两端自适应。
if os.path.isdir(_VPS_WORKDIR):
argv = [
sys.executable, "-X", "utf8", "-m", "sanguo_portfolio.runner_backtest",
"--json", "--start", req.start_date, "--end", req.end_date,
"--cash", str(req.initial_cash), "--benchmark", req.benchmark, "--max-pool", "30",
]
logger.info("[portfolio] 本地跑 runner: %s", " ".join(argv[3:]))
try:
proc = subprocess.run(
argv, cwd=_VPS_WORKDIR, capture_output=True, text=True,
timeout=_VPS_TIMEOUT, check=False,
)
except subprocess.TimeoutExpired:
raise HTTPException(504, f"回测超时(>{_VPS_TIMEOUT}s)")
except Exception as exc:
logger.exception("[portfolio] 本地 runner 调用失败")
raise HTTPException(500, f"本地 runner 调用失败: {exc}")
else:
# Mac 端 SSH 到 VPS(远端命令手动拼,不用 shlex.quote:产 POSIX 单引号 cmd 不认)
remote_cmd = (
f"cd {_VPS_WORKDIR} && "
f"{_VPS_PYTHON} -X utf8 -m sanguo_portfolio.runner_backtest --json "
f"--start {req.start_date} --end {req.end_date} "
f"--cash {req.initial_cash} --benchmark {req.benchmark} "
f"--max-pool 30"
)
except subprocess.TimeoutExpired:
raise HTTPException(504, f"VPS 回测超时(>{_VPS_TIMEOUT}s)")
except FileNotFoundError:
raise HTTPException(500, "本机未找到 ssh 命令")
except Exception as exc:
logger.exception("[portfolio] SSH 调用失败")
raise HTTPException(500, f"SSH 调用失败: {exc}")
ssh_argv = [
"ssh", "-o", "ConnectTimeout=15", "-o", "StrictHostKeyChecking=no",
_VPS_HOST, remote_cmd,
]
logger.info("[portfolio] SSH 触发: %s", remote_cmd)
try:
proc = subprocess.run(
ssh_argv, capture_output=True, text=True,
timeout=_VPS_TIMEOUT, check=False,
)
except subprocess.TimeoutExpired:
raise HTTPException(504, f"VPS 回测超时(>{_VPS_TIMEOUT}s)")
except FileNotFoundError:
raise HTTPException(500, "本机未找到 ssh 命令")
except Exception as exc:
logger.exception("[portfolio] SSH 调用失败")
raise HTTPException(500, f"SSH 调用失败: {exc}")
if proc.returncode != 0:
stderr_tail = (proc.stderr or "")[-2000:]