This commit is contained in:
薇薇安 2026-02-01 22:04:43 +08:00
parent 0a0bcd941b
commit c01f681dec
4 changed files with 473 additions and 110 deletions

View File

@ -895,6 +895,79 @@ async def trading_restart(_admin: Dict[str, Any] = Depends(require_system_admin)
raise HTTPException(status_code=500, detail=f"supervisorctl restart 失败: {e}") raise HTTPException(status_code=500, detail=f"supervisorctl restart 失败: {e}")
@router.post("/trading/stop-all")
async def trading_stop_all(
_admin: Dict[str, Any] = Depends(require_system_admin),
prefix: str = "auto_sys_acc",
include_default: bool = False,
) -> Dict[str, Any]:
"""
一键停止所有账号交易进程supervisor
"""
try:
prefix = (prefix or "auto_sys_acc").strip()
if not prefix:
prefix = "auto_sys_acc"
# 先读取全量 status拿到有哪些进程
status_all = _run_supervisorctl(["status"])
names = _list_supervisor_process_names(status_all)
targets: list[str] = []
for n in names:
if n.startswith(prefix):
targets.append(n)
if include_default:
default_prog = _get_program_name()
if default_prog and default_prog not in targets and default_prog in names:
targets.append(default_prog)
if not targets:
return {
"message": "未找到可停止的交易进程",
"prefix": prefix,
"include_default": include_default,
"count": 0,
"targets": [],
"status_all": status_all,
}
results: list[Dict[str, Any]] = []
ok = 0
failed = 0
for prog in targets:
try:
out = _run_supervisorctl(["stop", prog])
raw = _run_supervisorctl(["status", prog])
running, pid, state = _parse_supervisor_status(raw)
results.append(
{
"program": prog,
"ok": True,
"output": out,
"status": {"running": running, "pid": pid, "state": state, "raw": raw},
}
)
ok += 1
except Exception as e:
failed += 1
results.append({"program": prog, "ok": False, "error": str(e)})
return {
"message": "已发起批量停止",
"prefix": prefix,
"include_default": include_default,
"count": len(targets),
"ok": ok,
"failed": failed,
"targets": targets,
"results": results,
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"批量停止失败: {e}")
@router.post("/trading/restart-all") @router.post("/trading/restart-all")
async def trading_restart_all( async def trading_restart_all(
_admin: Dict[str, Any] = Depends(require_system_admin), _admin: Dict[str, Any] = Depends(require_system_admin),

View File

@ -165,3 +165,125 @@
background: #ffebee; background: #ffebee;
color: #c62828; color: #c62828;
} }
/* System Control Section */
.system-section {
border-top: 4px solid #1976d2;
}
.system-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.system-header h3 {
margin: 0;
}
.system-status {
display: flex;
align-items: center;
gap: 12px;
}
.system-status-badge {
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.system-status-badge.running {
background: #e8f5e9;
color: #2e7d32;
border: 1px solid #c8e6c9;
}
.system-status-badge.stopped {
background: #ffebee;
color: #c62828;
border: 1px solid #ffcdd2;
}
.system-status-meta {
font-size: 12px;
color: #666;
font-family: monospace;
}
.system-control-group {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid #eee;
}
.control-group-title {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: #555;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.system-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.system-btn {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
color: #333;
}
.system-btn:hover:not(:disabled) {
background: #f5f5f5;
border-color: #bbb;
}
.system-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.system-btn.primary {
background: #1976d2;
color: white;
border-color: #1565c0;
}
.system-btn.primary:hover:not(:disabled) {
background: #1565c0;
}
.system-btn.danger {
background: #dc3545;
color: white;
border-color: #dc3545;
}
.system-btn.danger:hover:not(:disabled) {
background: #c82333;
border-color: #bd2130;
}
.system-hint {
font-size: 13px;
color: #666;
margin-top: 12px;
padding: 8px 12px;
background: #fff3e0;
border-radius: 4px;
border: 1px solid #ffe0b2;
}

View File

@ -212,6 +212,10 @@ const GlobalConfig = () => {
const [snapshotIncludeSecrets, setSnapshotIncludeSecrets] = useState(false) const [snapshotIncludeSecrets, setSnapshotIncludeSecrets] = useState(false)
const [snapshotBusy, setSnapshotBusy] = useState(false) const [snapshotBusy, setSnapshotBusy] = useState(false)
// Tab
const [searchTerm, setSearchTerm] = useState('')
const [activeTab, setActiveTab] = useState('all')
const PCT_LIKE_KEYS = new Set([ const PCT_LIKE_KEYS = new Set([
'LIMIT_ORDER_OFFSET_PCT', 'LIMIT_ORDER_OFFSET_PCT',
'ENTRY_MAX_DRIFT_PCT_TRENDING', 'ENTRY_MAX_DRIFT_PCT_TRENDING',
@ -679,6 +683,21 @@ const GlobalConfig = () => {
} }
} }
const handleStopAllTrading = async () => {
if (!window.confirm('确定要停止【所有账号】的交易进程吗?所有账号将停止自动交易!')) return
setSystemBusy(true)
setMessage('')
try {
const res = await api.stopAllTradingSystems({ prefix: 'auto_sys_acc', include_default: true })
setMessage(`已发起批量停止:共 ${res.count} 个,成功 ${res.ok},失败 ${res.failed}`)
await loadSystemStatus()
} catch (e) {
setMessage('批量停止失败: ' + (e?.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
const applyPreset = async (presetKey) => { const applyPreset = async (presetKey) => {
const preset = presets[presetKey] const preset = presets[presetKey]
@ -761,6 +780,57 @@ const GlobalConfig = () => {
} }
} }
//
const handleConfigUpdate = async (key, value, config) => {
//
if (config.value === value) return
//
let displayOld = config.value
let displayNew = value
//
const isPercent = key.includes('PERCENT') || key.includes('PCT')
if (isPercent && typeof value === 'number' && Math.abs(value) <= 1) {
// key PERCENT
//
//
displayOld = `${config.value} (${(config.value * 100).toFixed(2)}%)`
displayNew = `${value} (${(value * 100).toFixed(2)}%)`
}
const confirmMsg = `确定修改配置项【${key}】吗?\n\n原值: ${config.value}\n新值: ${value}\n\n修改将立即生效。`
if (!window.confirm(confirmMsg)) {
// UI
// ConfigItem onBlur
// UI
await loadConfigs()
return
}
try {
setSaving(true)
setMessage('')
if (!isAdmin) {
setMessage('只有管理员可以修改全局配置')
return
}
await api.updateGlobalConfigsBatch([{
key,
value,
type: config.type,
category: config.category,
description: config.description
}])
setMessage(`已更新 ${key}`)
await loadConfigs()
} catch (error) {
setMessage('更新配置失败: ' + error.message)
} finally {
setSaving(false)
}
}
// //
const isSecretKey = (key) => { const isSecretKey = (key) => {
return key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET' return key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET'
@ -1110,14 +1180,29 @@ const GlobalConfig = () => {
<h3>系统控制</h3> <h3>系统控制</h3>
<div className="system-status"> <div className="system-status">
<span className={`system-status-badge ${systemStatus?.running ? 'running' : 'stopped'}`}> <span className={`system-status-badge ${systemStatus?.running ? 'running' : 'stopped'}`}>
{systemStatus?.running ? '运行中' : '未运行'} 交易系统 {systemStatus?.running ? '运行中' : '未运行'}
</span> </span>
{systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null} {systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null}
{systemStatus?.program ? <span className="system-status-meta">程序: {systemStatus.program}</span> : null}
{systemStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {systemStatus.meta.requested_at}</span> : null} <span className={`system-status-badge ${backendStatus?.running ? 'running' : 'stopped'}`} style={{ marginLeft: '10px' }}>
后端服务 {backendStatus?.running ? '运行中' : '未知'}
</span>
{backendStatus?.pid ? <span className="system-status-meta">PID: {backendStatus.pid}</span> : null}
</div> </div>
</div> </div>
<div className="system-control-group">
<h4 className="control-group-title" style={{ margin: '10px 0 8px', fontSize: '14px', color: '#666' }}>后端服务管理</h4>
<div className="system-actions"> <div className="system-actions">
<button
type="button"
className="system-btn primary"
onClick={handleRestartBackend}
disabled={systemBusy}
title="通过 backend/restart.sh 重启后端uvicorn。重启期间接口会短暂不可用。"
>
重启后端服务
</button>
<button <button
type="button" type="button"
className="system-btn" className="system-btn"
@ -1127,6 +1212,21 @@ const GlobalConfig = () => {
> >
清除缓存 清除缓存
</button> </button>
</div>
</div>
<div className="system-control-group" style={{ marginTop: '15px' }}>
<h4 className="control-group-title" style={{ margin: '10px 0 8px', fontSize: '14px', color: '#666' }}>交易服务管理</h4>
<div className="system-actions">
<button
type="button"
className="system-btn"
onClick={handleStartTrading}
disabled={systemBusy || systemStatus?.running === true}
title="通过 supervisorctl 启动交易系统"
>
启动
</button>
<button <button
type="button" type="button"
className="system-btn" className="system-btn"
@ -1136,24 +1236,6 @@ const GlobalConfig = () => {
> >
停止 停止
</button> </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>
<button <button
type="button" type="button"
className="system-btn primary" className="system-btn primary"
@ -1165,23 +1247,18 @@ const GlobalConfig = () => {
</button> </button>
<button <button
type="button" type="button"
className="system-btn primary" className="system-btn danger"
onClick={handleRestartBackend} onClick={handleStopAllTrading}
disabled={systemBusy} disabled={systemBusy}
title="通过 backend/restart.sh 重启后端uvicorn。重启期间接口会短暂不可用。" title="批量停止所有账号交易进程auto_sys_acc*"
> >
重启后端服务 停止所有账号交易
</button> </button>
</div> </div>
<div className="system-status" style={{ marginTop: '10px' }}>
<span className={`system-status-badge ${backendStatus?.running ? 'running' : 'stopped'}`}>
后端 {backendStatus?.running ? '运行中' : '未知'}
</span>
{backendStatus?.pid ? <span className="system-status-meta">PID: {backendStatus.pid}</span> : null}
{backendStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {backendStatus.meta.requested_at}</span> : null}
</div> </div>
<div className="system-hint"> <div className="system-hint">
建议流程先更新配置里的 Key 点击"清除缓存" 点击"重启交易系统"确保不再使用旧账号下单 建议流程先更新配置里的 Key 点击"清除缓存" 点击"重启所有账号交易"确保不再使用旧账号下单
</div> </div>
</section> </section>
)} )}
@ -1269,64 +1346,137 @@ const GlobalConfig = () => {
修改全局策略配置所有普通用户账号将使用这些配置风险旋钮除外 修改全局策略配置所有普通用户账号将使用这些配置风险旋钮除外
</p> </p>
</div> </div>
{/* 搜索和筛选栏 */}
<div className="config-toolbar" style={{ marginBottom: '20px', display: 'flex', flexDirection: 'column', gap: '15px' }}>
{/* 搜索框 */}
<div className="search-bar" style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
<input
type="text"
placeholder="🔍 搜索配置项 Key、描述或中文名..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{
width: '100%',
padding: '10px 12px',
borderRadius: '6px',
border: '1px solid #ddd',
fontSize: '14px'
}}
/>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
style={{
position: 'absolute',
right: '10px',
border: 'none',
background: 'none',
cursor: 'pointer',
color: '#999',
fontSize: '16px'
}}
>
</button>
)}
</div>
{/* Tabs */}
<div className="config-tabs" style={{ display: 'flex', gap: '10px', overflowX: 'auto', paddingBottom: '5px' }}>
{[
{ key: 'all', label: '全部' },
{ key: 'risk', label: '风险控制' },
{ key: 'strategy', label: '策略参数' },
{ key: 'scan', label: '市场扫描' },
{ key: 'position', label: '仓位控制' },
].map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`tab-btn ${activeTab === tab.key ? 'active' : ''}`}
style={{
padding: '8px 16px',
borderRadius: '20px',
border: activeTab === tab.key ? '1px solid #007bff' : '1px solid #eee',
background: activeTab === tab.key ? '#e7f1ff' : '#f9f9f9',
color: activeTab === tab.key ? '#007bff' : '#666',
cursor: 'pointer',
fontWeight: activeTab === tab.key ? '600' : 'normal',
whiteSpace: 'nowrap',
transition: 'all 0.2s'
}}
>
{tab.label}
</button>
))}
</div>
</div>
{Object.keys(configs).length > 0 ? ( {Object.keys(configs).length > 0 ? (
(() => { (() => {
const configCategories = { const configCategories = {
'scan': '市场扫描',
'position': '仓位控制',
'risk': '风险控制', 'risk': '风险控制',
'strategy': '策略参数', 'strategy': '策略参数',
'scan': '市场扫描',
'position': '仓位控制',
} }
return Object.entries(configCategories).map(([category, label]) => {
const categoryConfigs = Object.entries(configs).filter(([key, config]) => { //
// configcategory const filteredConfigs = Object.entries(configs).filter(([key, config]) => {
if (!config || typeof config !== 'object') { // 1. API Key
console.warn(`Config ${key} is not an object:`, config) if (!config || typeof config !== 'object') return false
return false
}
if (!config.category || config.category !== category) return false
//
const RISK_KNOBS_KEYS = ['MIN_MARGIN_USDT', 'MIN_POSITION_PERCENT', 'MAX_POSITION_PERCENT', const RISK_KNOBS_KEYS = ['MIN_MARGIN_USDT', 'MIN_POSITION_PERCENT', 'MAX_POSITION_PERCENT',
'MAX_TOTAL_POSITION_PERCENT', 'AUTO_TRADE_ENABLED', 'MAX_OPEN_POSITIONS', 'MAX_DAILY_ENTRIES'] 'MAX_TOTAL_POSITION_PERCENT', 'AUTO_TRADE_ENABLED', 'MAX_OPEN_POSITIONS', 'MAX_DAILY_ENTRIES']
if (RISK_KNOBS_KEYS.includes(key)) return false if (RISK_KNOBS_KEYS.includes(key)) return false
// API
if (key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET' || key === 'USE_TESTNET') return false if (key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET' || key === 'USE_TESTNET') return false
// 2. Tab
if (activeTab !== 'all' && config.category !== activeTab) return false
// 3.
if (searchTerm) {
const lowerTerm = searchTerm.toLowerCase()
const matchKey = key.toLowerCase().includes(lowerTerm)
const matchDesc = (config.description || '').toLowerCase().includes(lowerTerm)
const matchLabel = (KEY_LABELS[key] || '').toLowerCase().includes(lowerTerm)
if (!matchKey && !matchDesc && !matchLabel) return false
}
return true return true
}) })
if (categoryConfigs.length === 0) return null
console.log(`Category ${category} (${label}): ${categoryConfigs.length} configs`, categoryConfigs.map(([k]) => k)) if (filteredConfigs.length === 0) {
return <div style={{ textAlign: 'center', color: '#999', padding: '30px', background: '#f5f5f5', borderRadius: '8px' }}>未找到匹配的配置项</div>
}
//
// 'all' Tab Category
// Tab
const groupsToRender = activeTab === 'all'
? Object.keys(configCategories)
: [activeTab]
// filteredConfigs Map 便 filter
return groupsToRender.map(category => {
const categoryLabel = configCategories[category] || category
const groupConfigs = filteredConfigs.filter(([_, cfg]) => cfg.category === category)
if (groupConfigs.length === 0) return null
return ( return (
<div key={category} style={{ marginBottom: '24px' }}> <div key={category} style={{ marginBottom: '24px' }}>
<h4 style={{ marginBottom: '12px', fontSize: '16px', fontWeight: '600' }}>{label}</h4> <h4 style={{ marginBottom: '12px', fontSize: '16px', fontWeight: '600', borderLeft: '4px solid #007bff', paddingLeft: '10px', color: '#333' }}>
{categoryLabel}
</h4>
<div className="config-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '16px' }}> <div className="config-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '16px' }}>
{categoryConfigs.map(([key, config]) => ( {groupConfigs.map(([key, config]) => (
<ConfigItem <ConfigItem
key={key} key={key}
label={key} label={key}
config={config} config={config}
onUpdate={async (value) => { onUpdate={(val) => handleConfigUpdate(key, val, config)}
try {
setSaving(true)
setMessage('')
if (!isAdmin) {
setMessage('只有管理员可以修改全局配置')
return
}
await api.updateGlobalConfigsBatch([{
key,
value,
type: config.type,
category: config.category,
description: config.description
}])
setMessage(`已更新 ${key}`)
await loadConfigs()
} catch (error) {
setMessage('更新配置失败: ' + error.message)
} finally {
setSaving(false)
}
}}
disabled={saving} disabled={saving}
/> />
))} ))}

View File

@ -612,6 +612,24 @@ export const api = {
return response.json(); return response.json();
}, },
stopAllTradingSystems: async (opts = {}) => {
const params = new URLSearchParams()
if (opts?.prefix) params.set('prefix', String(opts.prefix))
if (opts?.include_default) params.set('include_default', 'true')
const url = params.toString()
? `${buildUrl('/api/system/trading/stop-all')}?${params.toString()}`
: buildUrl('/api/system/trading/stop-all')
const response = await fetch(url, {
method: 'POST',
headers: withAccountHeaders({ 'Content-Type': 'application/json' }),
})
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '批量停止失败' }))
throw new Error(error.detail || '批量停止失败')
}
return response.json()
},
restartAllTradingSystems: async (opts = {}) => { restartAllTradingSystems: async (opts = {}) => {
const params = new URLSearchParams() const params = new URLSearchParams()
if (opts?.prefix) params.set('prefix', String(opts.prefix)) if (opts?.prefix) params.set('prefix', String(opts.prefix))