a
This commit is contained in:
parent
ed662b0092
commit
38ebbebd95
|
|
@ -3,7 +3,7 @@ FastAPI应用主入口
|
||||||
"""
|
"""
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from api.routes import config, trades, stats, dashboard, account, recommendations
|
from api.routes import config, trades, stats, dashboard, account, recommendations, system
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -151,6 +151,7 @@ app.include_router(stats.router, prefix="/api/stats", tags=["统计分析"])
|
||||||
app.include_router(dashboard.router, prefix="/api/dashboard", tags=["仪表板"])
|
app.include_router(dashboard.router, prefix="/api/dashboard", tags=["仪表板"])
|
||||||
app.include_router(account.router, prefix="/api/account", tags=["账户数据"])
|
app.include_router(account.router, prefix="/api/account", tags=["账户数据"])
|
||||||
app.include_router(recommendations.router, tags=["交易推荐"])
|
app.include_router(recommendations.router, tags=["交易推荐"])
|
||||||
|
app.include_router(system.router, tags=["系统控制"])
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
|
|
||||||
240
backend/api/routes/system.py
Normal file
240
backend/api/routes/system.py
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, Header
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 路由统一挂在 /api/system 下,前端直接调用 /api/system/...
|
||||||
|
router = APIRouter(prefix="/api/system")
|
||||||
|
|
||||||
|
|
||||||
|
def _require_admin(token: Optional[str], provided: Optional[str]) -> None:
|
||||||
|
"""
|
||||||
|
可选的简单保护:如果环境变量配置了 SYSTEM_CONTROL_TOKEN,则要求请求携带 X-Admin-Token。
|
||||||
|
生产环境强烈建议通过 Nginx 额外做鉴权 / IP 白名单。
|
||||||
|
"""
|
||||||
|
if not token:
|
||||||
|
return
|
||||||
|
if not provided or provided != token:
|
||||||
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
|
|
||||||
|
def _build_supervisorctl_cmd(args: list[str]) -> list[str]:
|
||||||
|
supervisorctl_path = os.getenv("SUPERVISORCTL_PATH", "supervisorctl")
|
||||||
|
supervisor_conf = os.getenv("SUPERVISOR_CONF", "").strip()
|
||||||
|
use_sudo = os.getenv("SUPERVISOR_USE_SUDO", "false").lower() == "true"
|
||||||
|
|
||||||
|
cmd: list[str] = []
|
||||||
|
if use_sudo:
|
||||||
|
# 需要你在 sudoers 配置 NOPASSWD(sudo -n 才不会卡住)
|
||||||
|
cmd += ["sudo", "-n"]
|
||||||
|
cmd += [supervisorctl_path]
|
||||||
|
if supervisor_conf:
|
||||||
|
cmd += ["-c", supervisor_conf]
|
||||||
|
cmd += args
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
def _run_supervisorctl(args: list[str]) -> str:
|
||||||
|
cmd = _build_supervisorctl_cmd(args)
|
||||||
|
try:
|
||||||
|
res = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise RuntimeError("supervisorctl 超时(10s)")
|
||||||
|
|
||||||
|
out = (res.stdout or "").strip()
|
||||||
|
err = (res.stderr or "").strip()
|
||||||
|
combined = "\n".join([s for s in [out, err] if s]).strip()
|
||||||
|
if res.returncode != 0:
|
||||||
|
raise RuntimeError(combined or f"supervisorctl failed (exit={res.returncode})")
|
||||||
|
return combined or out
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_supervisor_status(raw: str) -> Tuple[bool, Optional[int], str]:
|
||||||
|
"""
|
||||||
|
典型输出:
|
||||||
|
- auto_sys RUNNING pid 1234, uptime 0:10:00
|
||||||
|
- auto_sys STOPPED Not started
|
||||||
|
"""
|
||||||
|
if "RUNNING" in raw:
|
||||||
|
m = re.search(r"\bpid\s+(\d+)\b", raw)
|
||||||
|
pid = int(m.group(1)) if m else None
|
||||||
|
return True, pid, "RUNNING"
|
||||||
|
for state in ["STOPPED", "FATAL", "EXITED", "BACKOFF", "STARTING", "UNKNOWN"]:
|
||||||
|
if state in raw:
|
||||||
|
return False, None, state
|
||||||
|
return False, None, "UNKNOWN"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_program_name() -> str:
|
||||||
|
# 你给的宝塔配置是 [program:auto_sys]
|
||||||
|
return os.getenv("SUPERVISOR_TRADING_PROGRAM", "auto_sys").strip() or "auto_sys"
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/clear-cache")
|
||||||
|
async def clear_cache(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
清理配置缓存(Redis Hash: trading_config),并从数据库回灌到 Redis。
|
||||||
|
"""
|
||||||
|
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import config_manager
|
||||||
|
|
||||||
|
cm = getattr(config_manager, "config_manager", None)
|
||||||
|
if cm is None:
|
||||||
|
raise HTTPException(status_code=500, detail="config_manager 未初始化")
|
||||||
|
|
||||||
|
deleted_keys: list[str] = []
|
||||||
|
|
||||||
|
# 1) 清 backend 本地 cache
|
||||||
|
try:
|
||||||
|
cm._cache = {}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2) 清 Redis 缓存 key(Hash: trading_config)
|
||||||
|
try:
|
||||||
|
redis_client = getattr(cm, "_redis_client", None)
|
||||||
|
redis_connected = getattr(cm, "_redis_connected", False)
|
||||||
|
if redis_client is not None and redis_connected:
|
||||||
|
try:
|
||||||
|
redis_client.ping()
|
||||||
|
except Exception:
|
||||||
|
redis_connected = False
|
||||||
|
|
||||||
|
if redis_client is not None and redis_connected:
|
||||||
|
try:
|
||||||
|
redis_client.delete("trading_config")
|
||||||
|
deleted_keys.append("trading_config")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"删除 Redis key trading_config 失败: {e}")
|
||||||
|
|
||||||
|
# 可选:实时推荐缓存(如果存在)
|
||||||
|
try:
|
||||||
|
redis_client.delete("recommendations:realtime")
|
||||||
|
deleted_keys.append("recommendations:realtime")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"清 Redis 缓存失败: {e}")
|
||||||
|
|
||||||
|
# 3) 立刻从 DB 回灌到 Redis(避免 trading_system 读到空)
|
||||||
|
try:
|
||||||
|
cm.reload()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"回灌配置到 Redis 失败(仍可能使用DB/本地cache): {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "缓存已清理并回灌",
|
||||||
|
"deleted_keys": deleted_keys,
|
||||||
|
"note": "如果你使用 supervisor 管理交易系统,请点击“重启交易系统”让新 Key 立即生效。",
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"清理缓存失败: {e}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/trading/status")
|
||||||
|
async def trading_status(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
|
||||||
|
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
|
||||||
|
|
||||||
|
program = _get_program_name()
|
||||||
|
try:
|
||||||
|
raw = _run_supervisorctl(["status", program])
|
||||||
|
running, pid, state = _parse_supervisor_status(raw)
|
||||||
|
return {
|
||||||
|
"mode": "supervisor",
|
||||||
|
"program": program,
|
||||||
|
"running": running,
|
||||||
|
"pid": pid,
|
||||||
|
"state": state,
|
||||||
|
"raw": raw,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"supervisorctl status 失败: {e}. 你可能需要配置 SUPERVISOR_CONF / SUPERVISOR_TRADING_PROGRAM / SUPERVISOR_USE_SUDO",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/trading/start")
|
||||||
|
async def trading_start(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
|
||||||
|
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
|
||||||
|
|
||||||
|
program = _get_program_name()
|
||||||
|
try:
|
||||||
|
out = _run_supervisorctl(["start", program])
|
||||||
|
raw = _run_supervisorctl(["status", program])
|
||||||
|
running, pid, state = _parse_supervisor_status(raw)
|
||||||
|
return {
|
||||||
|
"message": "交易系统已启动(supervisor)",
|
||||||
|
"output": out,
|
||||||
|
"status": {
|
||||||
|
"mode": "supervisor",
|
||||||
|
"program": program,
|
||||||
|
"running": running,
|
||||||
|
"pid": pid,
|
||||||
|
"state": state,
|
||||||
|
"raw": raw,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"supervisorctl start 失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/trading/stop")
|
||||||
|
async def trading_stop(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
|
||||||
|
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
|
||||||
|
|
||||||
|
program = _get_program_name()
|
||||||
|
try:
|
||||||
|
out = _run_supervisorctl(["stop", program])
|
||||||
|
raw = _run_supervisorctl(["status", program])
|
||||||
|
running, pid, state = _parse_supervisor_status(raw)
|
||||||
|
return {
|
||||||
|
"message": "交易系统已停止(supervisor)",
|
||||||
|
"output": out,
|
||||||
|
"status": {
|
||||||
|
"mode": "supervisor",
|
||||||
|
"program": program,
|
||||||
|
"running": running,
|
||||||
|
"pid": pid,
|
||||||
|
"state": state,
|
||||||
|
"raw": raw,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"supervisorctl stop 失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/trading/restart")
|
||||||
|
async def trading_restart(x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token")) -> Dict[str, Any]:
|
||||||
|
_require_admin(os.getenv("SYSTEM_CONTROL_TOKEN", "").strip(), x_admin_token)
|
||||||
|
|
||||||
|
program = _get_program_name()
|
||||||
|
try:
|
||||||
|
out = _run_supervisorctl(["restart", program])
|
||||||
|
raw = _run_supervisorctl(["status", program])
|
||||||
|
running, pid, state = _parse_supervisor_status(raw)
|
||||||
|
return {
|
||||||
|
"message": "交易系统已重启(supervisor)",
|
||||||
|
"output": out,
|
||||||
|
"status": {
|
||||||
|
"mode": "supervisor",
|
||||||
|
"program": program,
|
||||||
|
"running": running,
|
||||||
|
"pid": pid,
|
||||||
|
"state": state,
|
||||||
|
"raw": raw,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"supervisorctl restart 失败: {e}")
|
||||||
|
|
||||||
|
|
@ -209,6 +209,111 @@
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.system-section {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.system-header {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-section h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #34495e;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-status-badge {
|
||||||
|
padding: 0.3rem 0.7rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-status-badge.running {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
border: 1px solid #c8e6c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-status-badge.stopped {
|
||||||
|
background: #fff3e0;
|
||||||
|
color: #e65100;
|
||||||
|
border: 1px solid #ffe0b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-status-meta {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-btn {
|
||||||
|
padding: 0.5rem 0.9rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
background: #f8f9fa;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-btn:hover:not(:disabled) {
|
||||||
|
border-color: #2196F3;
|
||||||
|
background: #e7f3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-btn.primary {
|
||||||
|
border-color: #2196F3;
|
||||||
|
background: #2196F3;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-btn.primary:hover:not(:disabled) {
|
||||||
|
background: #1976D2;
|
||||||
|
border-color: #1976D2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-hint {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ const ConfigPanel = () => {
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
const [feasibilityCheck, setFeasibilityCheck] = useState(null)
|
const [feasibilityCheck, setFeasibilityCheck] = useState(null)
|
||||||
const [checkingFeasibility, setCheckingFeasibility] = useState(false)
|
const [checkingFeasibility, setCheckingFeasibility] = useState(false)
|
||||||
|
const [systemStatus, setSystemStatus] = useState(null)
|
||||||
|
const [systemBusy, setSystemBusy] = useState(false)
|
||||||
|
|
||||||
// 预设方案配置
|
// 预设方案配置
|
||||||
// 注意:百分比配置使用整数形式(如8.0表示8%),在应用时会转换为小数(0.08)
|
// 注意:百分比配置使用整数形式(如8.0表示8%),在应用时会转换为小数(0.08)
|
||||||
|
|
@ -64,9 +66,82 @@ const ConfigPanel = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadSystemStatus = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.getTradingSystemStatus()
|
||||||
|
setSystemStatus(res)
|
||||||
|
} catch (error) {
|
||||||
|
// 静默失败:不影响配置页使用
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClearCache = async () => {
|
||||||
|
setSystemBusy(true)
|
||||||
|
setMessage('')
|
||||||
|
try {
|
||||||
|
const res = await api.clearSystemCache()
|
||||||
|
setMessage(res.message || '缓存已清理')
|
||||||
|
await loadConfigs()
|
||||||
|
await loadSystemStatus()
|
||||||
|
} catch (error) {
|
||||||
|
setMessage('清理缓存失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setSystemBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStartTrading = async () => {
|
||||||
|
setSystemBusy(true)
|
||||||
|
setMessage('')
|
||||||
|
try {
|
||||||
|
const res = await api.startTradingSystem()
|
||||||
|
setMessage(res.message || '交易系统已启动')
|
||||||
|
await loadSystemStatus()
|
||||||
|
} catch (error) {
|
||||||
|
setMessage('启动失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setSystemBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStopTrading = async () => {
|
||||||
|
setSystemBusy(true)
|
||||||
|
setMessage('')
|
||||||
|
try {
|
||||||
|
const res = await api.stopTradingSystem()
|
||||||
|
setMessage(res.message || '交易系统已停止')
|
||||||
|
await loadSystemStatus()
|
||||||
|
} catch (error) {
|
||||||
|
setMessage('停止失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setSystemBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRestartTrading = async () => {
|
||||||
|
setSystemBusy(true)
|
||||||
|
setMessage('')
|
||||||
|
try {
|
||||||
|
const res = await api.restartTradingSystem()
|
||||||
|
setMessage(res.message || '交易系统已重启')
|
||||||
|
await loadSystemStatus()
|
||||||
|
} catch (error) {
|
||||||
|
setMessage('重启失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setSystemBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadConfigs()
|
loadConfigs()
|
||||||
checkFeasibility()
|
checkFeasibility()
|
||||||
|
loadSystemStatus()
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
loadSystemStatus()
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
return () => clearInterval(timer)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const checkFeasibility = async () => {
|
const checkFeasibility = async () => {
|
||||||
|
|
@ -247,6 +322,61 @@ const ConfigPanel = () => {
|
||||||
<p>修改配置后,交易系统将在下次扫描时自动使用新配置</p>
|
<p>修改配置后,交易系统将在下次扫描时自动使用新配置</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 系统控制:清缓存 / 启停 / 重启(supervisor) */}
|
||||||
|
<div className="system-section">
|
||||||
|
<div className="system-header">
|
||||||
|
<h3>系统控制</h3>
|
||||||
|
<div className="system-status">
|
||||||
|
<span className={`system-status-badge ${systemStatus?.running ? 'running' : 'stopped'}`}>
|
||||||
|
{systemStatus?.running ? '运行中' : '未运行'}
|
||||||
|
</span>
|
||||||
|
{systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null}
|
||||||
|
{systemStatus?.program ? <span className="system-status-meta">程序: {systemStatus.program}</span> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="system-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="system-btn"
|
||||||
|
onClick={handleClearCache}
|
||||||
|
disabled={systemBusy}
|
||||||
|
title="清理Redis配置缓存并从数据库回灌。切换API Key后建议先点这里,再重启交易系统。"
|
||||||
|
>
|
||||||
|
清除缓存
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="system-btn"
|
||||||
|
onClick={handleStopTrading}
|
||||||
|
disabled={systemBusy || systemStatus?.running === false}
|
||||||
|
title="通过 supervisorctl 停止交易系统"
|
||||||
|
>
|
||||||
|
停止
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="system-btn"
|
||||||
|
onClick={handleStartTrading}
|
||||||
|
disabled={systemBusy || systemStatus?.running === true}
|
||||||
|
title="通过 supervisorctl 启动交易系统"
|
||||||
|
>
|
||||||
|
启动
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="system-btn primary"
|
||||||
|
onClick={handleRestartTrading}
|
||||||
|
disabled={systemBusy}
|
||||||
|
title="通过 supervisorctl 重启交易系统(建议切换API Key后使用)"
|
||||||
|
>
|
||||||
|
重启交易系统
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="system-hint">
|
||||||
|
建议流程:先更新配置里的 Key → 点击“清除缓存” → 点击“重启交易系统”,确保不再使用旧账号下单。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 预设方案快速切换 */}
|
{/* 预设方案快速切换 */}
|
||||||
<div className="preset-section">
|
<div className="preset-section">
|
||||||
<div className="preset-header">
|
<div className="preset-header">
|
||||||
|
|
|
||||||
|
|
@ -239,4 +239,62 @@ export const api = {
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 系统控制(supervisor)
|
||||||
|
clearSystemCache: async () => {
|
||||||
|
const response = await fetch(buildUrl('/api/system/clear-cache'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: '清理缓存失败' }));
|
||||||
|
throw new Error(error.detail || '清理缓存失败');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
getTradingSystemStatus: async () => {
|
||||||
|
const response = await fetch(buildUrl('/api/system/trading/status'));
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: '获取交易系统状态失败' }));
|
||||||
|
throw new Error(error.detail || '获取交易系统状态失败');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
startTradingSystem: async () => {
|
||||||
|
const response = await fetch(buildUrl('/api/system/trading/start'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: '启动交易系统失败' }));
|
||||||
|
throw new Error(error.detail || '启动交易系统失败');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
stopTradingSystem: async () => {
|
||||||
|
const response = await fetch(buildUrl('/api/system/trading/stop'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: '停止交易系统失败' }));
|
||||||
|
throw new Error(error.detail || '停止交易系统失败');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
restartTradingSystem: async () => {
|
||||||
|
const response = await fetch(buildUrl('/api/system/trading/restart'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: '重启交易系统失败' }));
|
||||||
|
throw new Error(error.detail || '重启交易系统失败');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user