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
+30 -15
View File
@@ -8,7 +8,9 @@ POST /portfolio/backtest: SSH 触发 VPS 跑 BulletTrade + all_weather,
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
import subprocess import subprocess
import sys
from typing import Any, Optional from typing import Any, Optional
from fastapi import APIRouter, Depends, Header, HTTPException from fastapi import APIRouter, Depends, Header, HTTPException
@@ -50,9 +52,29 @@ def run_portfolio_backtest(req: PortfolioBacktestRequest):
- 没有 tail/head/grep:用 python 后处理(本函数在 Mac 端直接解析 stdout) - 没有 tail/head/grep:用 python 后处理(本函数在 Mac 端直接解析 stdout)
- 中文路径:VPS_WORKDIR / userdata_mini 走 env(DEFAULT_DATA_PROVIDER=miniqmt) - 中文路径:VPS_WORKDIR / userdata_mini 走 env(DEFAULT_DATA_PROVIDER=miniqmt)
""" """
# 远端命令手动拼接(不用 shlex.quote:它产 POSIX 单引号,Windows cmd 不认单引号致 cd 失败; # 本地模式(VPS 后端直接跑 runner,避免 SSH 自连外网 IP 绕路);
# 且不用 set X=Y:runner_backtest 顶部 os.environ.setdefault 已自处理 env,早于 bullet_trade import, # SSH 模式(Mac 后端 → VPS)。env SANGUO_PORTFOLIO_LOCAL=1 切本地。
# 避免 cmd "set X=Y &&" 尾空格进 value(变 "miniqmt ")致 bullet_trade provider 名匹配失败)。 # 自动检测: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 = ( remote_cmd = (
f"cd {_VPS_WORKDIR} && " f"cd {_VPS_WORKDIR} && "
f"{_VPS_PYTHON} -X utf8 -m sanguo_portfolio.runner_backtest --json " f"{_VPS_PYTHON} -X utf8 -m sanguo_portfolio.runner_backtest --json "
@@ -61,21 +83,14 @@ def run_portfolio_backtest(req: PortfolioBacktestRequest):
f"--max-pool 30" f"--max-pool 30"
) )
ssh_argv = [ ssh_argv = [
"ssh", "ssh", "-o", "ConnectTimeout=15", "-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=15", _VPS_HOST, remote_cmd,
"-o", "StrictHostKeyChecking=no",
_VPS_HOST,
remote_cmd,
] ]
logger.info("[portfolio] SSH 触发: %s", remote_cmd)
logger.info("[portfolio] SSH 触发: %s", ssh_argv[-1])
try: try:
proc = subprocess.run( proc = subprocess.run(
ssh_argv, ssh_argv, capture_output=True, text=True,
capture_output=True, timeout=_VPS_TIMEOUT, check=False,
text=True,
timeout=_VPS_TIMEOUT,
check=False,
) )
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
raise HTTPException(504, f"VPS 回测超时(>{_VPS_TIMEOUT}s)") raise HTTPException(504, f"VPS 回测超时(>{_VPS_TIMEOUT}s)")