import React, { useState, useEffect, useRef } from 'react' import { Link } from 'react-router-dom' import { useSelector } from 'react-redux' import { api } from '../services/api' import { selectCurrentUser, selectIsAdmin } from '../store/appSlice' import './GlobalConfig.css' import './ConfigPanel.css' // 复用 ConfigPanel 的样式 // 复用 ConfigPanel 的 ConfigItem 组件 const ConfigItem = ({ label, config, onUpdate, disabled }) => { const isPercentKey = label.includes('PERCENT') || label.includes('PCT') const PCT_LIKE_KEYS = new Set([ 'LIMIT_ORDER_OFFSET_PCT', 'ENTRY_MAX_DRIFT_PCT_TRENDING', 'ENTRY_MAX_DRIFT_PCT_RANGING', 'FIXED_RISK_PERCENT', // 固定风险百分比,已经是小数形式(0.02 = 2%) ]) const isPctLike = PCT_LIKE_KEYS.has(label) const isRatioPercentKey = isPercentKey && !isPctLike const formatPercent = (n) => { if (typeof n !== 'number' || isNaN(n)) return '' return n.toFixed(4).replace(/\.?0+$/, '') } const getInitialDisplayValue = (val) => { if (config.type === 'number' && isPercentKey) { if (val === null || val === undefined || val === '') { return '' } const numVal = typeof val === 'string' ? parseFloat(val) : val if (isNaN(numVal)) { return '' } if (isPctLike) { const pctNum = numVal <= 0.05 ? numVal * 100 : numVal return formatPercent(pctNum) } const percent = numVal <= 1 ? numVal * 100 : numVal return formatPercent(percent) } return val === null || val === undefined ? '' : val } const [value, setValue] = useState(config.value) const [localValue, setLocalValue] = useState(getInitialDisplayValue(config.value)) const [isEditing, setIsEditing] = useState(false) useEffect(() => { setValue(config.value) setIsEditing(false) setLocalValue(getInitialDisplayValue(config.value)) }, [config.value]) const handleChange = (newValue) => { setLocalValue(newValue) setIsEditing(true) } const handleBlur = () => { if (!isEditing) return let finalValue = localValue if (config.type === 'number') { if (isPercentKey) { const numVal = parseFloat(localValue) if (isNaN(numVal)) { setLocalValue(getInitialDisplayValue(config.value)) setIsEditing(false) return } if (isPctLike) { finalValue = numVal <= 1 ? numVal / 100 : numVal / 100 } else { finalValue = numVal <= 100 ? numVal / 100 : numVal / 100 } } else { finalValue = parseFloat(localValue) if (isNaN(finalValue)) { setLocalValue(getInitialDisplayValue(config.value)) setIsEditing(false) return } } } else if (config.type === 'boolean') { finalValue = localValue === 'true' || localValue === true } else { finalValue = localValue } onUpdate(finalValue) setIsEditing(false) } const displayValue = isEditing ? localValue : getInitialDisplayValue(config.value) return (
{config.type === 'boolean' ? ( ) : ( { let newValue = e.target.value if (config.type === 'number') { if (isPercentKey) { const numValue = parseFloat(newValue) const maxAllowed = isPctLike ? 1 : 100 if (newValue !== '' && !isNaN(numValue) && (numValue < 0 || numValue > maxAllowed)) { return } } } handleChange(newValue) }} onBlur={handleBlur} onKeyPress={(e) => { if (e.key === 'Enter') { handleBlur() } }} disabled={disabled} style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }} /> )} {isPercentKey && %}
{config.description && (
{config.description}
)}
) } const GlobalConfig = () => { const currentUser = useSelector(selectCurrentUser) const isAdmin = useSelector(selectIsAdmin) const [users, setUsers] = useState([]) const [accounts, setAccounts] = useState([]) const [loading, setLoading] = useState(true) const [message, setMessage] = useState('') const [busy, setBusy] = useState(false) const [selectedUserId, setSelectedUserId] = useState(null) const [showUserForm, setShowUserForm] = useState(false) const [newUser, setNewUser] = useState({ username: '', password: '', role: 'user', status: 'active' }) const [editingUserId, setEditingUserId] = useState(null) // 系统控制相关 const [systemStatus, setSystemStatus] = useState(null) const [backendStatus, setBackendStatus] = useState(null) const [systemBusy, setSystemBusy] = useState(false) // 预设方案相关 const [configs, setConfigs] = useState({}) const [saving, setSaving] = useState(false) const [configMeta, setConfigMeta] = useState(null) // 配置快照相关 const [showSnapshot, setShowSnapshot] = useState(false) const [snapshotText, setSnapshotText] = useState('') const [snapshotIncludeSecrets, setSnapshotIncludeSecrets] = useState(false) const [snapshotBusy, setSnapshotBusy] = useState(false) const PCT_LIKE_KEYS = new Set([ 'LIMIT_ORDER_OFFSET_PCT', 'ENTRY_MAX_DRIFT_PCT_TRENDING', 'ENTRY_MAX_DRIFT_PCT_RANGING', ]) // isAdmin 已从 Redux 获取,无需重复定义 // 预设方案配置(必须在函数定义之前,常量定义) const presets = { swing: { name: '波段回归(推荐)', desc: '根治高频与追价:关闭智能入场,回归"纯限价 + 30分钟扫描 + 更高信号门槛"的低频波段。建议先跑20-30单再评估。', configs: { SCAN_INTERVAL: 1800, TOP_N_SYMBOLS: 8, MAX_POSITION_PERCENT: 2.0, MAX_TOTAL_POSITION_PERCENT: 20.0, MIN_POSITION_PERCENT: 0.0, MIN_SIGNAL_STRENGTH: 8, USE_TRAILING_STOP: false, ATR_STOP_LOSS_MULTIPLIER: 2.5, // 放宽止损至2.5倍ATR,提升胜率 ATR_TAKE_PROFIT_MULTIPLIER: 1.5, RISK_REWARD_RATIO: 1.5, // 配合止盈倍数 TAKE_PROFIT_PERCENT: 25.0, MIN_HOLD_TIME_SEC: 1800, SMART_ENTRY_ENABLED: false, USE_DYNAMIC_ATR_MULTIPLIER: false, // 关闭动态调整,使用固定2.5倍 }, }, fill: { name: '成交优先(更少漏单)', desc: '优先解决"挂单NEW→超时撤单→没成交"的问题:解锁自动交易过滤 + 保守智能入场(限制追价步数与追价上限),在趋势强时允许可控的市价兜底。', configs: { SCAN_INTERVAL: 1800, TOP_N_SYMBOLS: 6, MIN_SIGNAL_STRENGTH: 7, AUTO_TRADE_ONLY_TRENDING: false, AUTO_TRADE_ALLOW_4H_NEUTRAL: true, SMART_ENTRY_ENABLED: true, LIMIT_ORDER_OFFSET_PCT: 0.1, ENTRY_CONFIRM_TIMEOUT_SEC: 120, ENTRY_CHASE_MAX_STEPS: 2, ENTRY_STEP_WAIT_SEC: 20, ENTRY_MARKET_FALLBACK_AFTER_SEC: 60, ENTRY_MAX_DRIFT_PCT_TRENDING: 0.3, ENTRY_MAX_DRIFT_PCT_RANGING: 0.15, USE_TRAILING_STOP: false, ATR_STOP_LOSS_MULTIPLIER: 2.5, // 放宽止损至2.5倍ATR,提升胜率 ATR_TAKE_PROFIT_MULTIPLIER: 1.5, RISK_REWARD_RATIO: 1.5, // 配合止盈倍数 TAKE_PROFIT_PERCENT: 25.0, MIN_HOLD_TIME_SEC: 1800, USE_DYNAMIC_ATR_MULTIPLIER: false, // 关闭动态调整,使用固定2.5倍 }, }, strict: { name: '精选低频(高胜率倾向)', desc: '更偏"少单、质量优先":仅趋势行情自动交易 + 4H中性不自动下单 + 更高信号门槛。仍保持较贴近的限价偏移,减少"完全成交不了"。', configs: { SCAN_INTERVAL: 1800, TOP_N_SYMBOLS: 6, MIN_SIGNAL_STRENGTH: 8, AUTO_TRADE_ONLY_TRENDING: true, AUTO_TRADE_ALLOW_4H_NEUTRAL: false, SMART_ENTRY_ENABLED: false, LIMIT_ORDER_OFFSET_PCT: 0.1, ENTRY_CONFIRM_TIMEOUT_SEC: 180, USE_TRAILING_STOP: false, ATR_STOP_LOSS_MULTIPLIER: 2.5, // 放宽止损至2.5倍ATR,提升胜率 ATR_TAKE_PROFIT_MULTIPLIER: 1.5, RISK_REWARD_RATIO: 1.5, // 配合止盈倍数 TAKE_PROFIT_PERCENT: 25.0, MIN_HOLD_TIME_SEC: 1800, USE_DYNAMIC_ATR_MULTIPLIER: false, // 关闭动态调整,使用固定2.5倍 }, }, steady: { name: '稳定出单(均衡收益/频率)', desc: '在"会下单"的基础上略提高出单频率:更短扫描间隔 + 更宽松门槛 + 保守智能入场(追价受限),适合想要稳定有单但不想回到高频。', configs: { SCAN_INTERVAL: 900, TOP_N_SYMBOLS: 8, MIN_SIGNAL_STRENGTH: 6, AUTO_TRADE_ONLY_TRENDING: false, AUTO_TRADE_ALLOW_4H_NEUTRAL: true, SMART_ENTRY_ENABLED: true, LIMIT_ORDER_OFFSET_PCT: 0.12, ENTRY_CONFIRM_TIMEOUT_SEC: 120, ENTRY_CHASE_MAX_STEPS: 3, ENTRY_STEP_WAIT_SEC: 15, ENTRY_MARKET_FALLBACK_AFTER_SEC: 45, ENTRY_MAX_DRIFT_PCT_TRENDING: 0.4, ENTRY_MAX_DRIFT_PCT_RANGING: 0.2, USE_TRAILING_STOP: false, ATR_STOP_LOSS_MULTIPLIER: 2.5, // 放宽止损至2.5倍ATR,提升胜率 ATR_TAKE_PROFIT_MULTIPLIER: 1.5, RISK_REWARD_RATIO: 1.5, // 配合止盈倍数 TAKE_PROFIT_PERCENT: 25.0, MIN_HOLD_TIME_SEC: 1800, USE_DYNAMIC_ATR_MULTIPLIER: false, // 关闭动态调整,使用固定2.5倍 }, }, conservative: { name: '保守配置', desc: '适合新手,风险较低,止损止盈较宽松,避免被正常波动触发', configs: { SCAN_INTERVAL: 900, MIN_CHANGE_PERCENT: 2.0, MIN_SIGNAL_STRENGTH: 5, TOP_N_SYMBOLS: 10, MAX_SCAN_SYMBOLS: 150, MIN_VOLATILITY: 0.02, STOP_LOSS_PERCENT: 10.0, TAKE_PROFIT_PERCENT: 25.0, MIN_STOP_LOSS_PRICE_PCT: 2.0, MIN_TAKE_PROFIT_PRICE_PCT: 3.0, USE_TRAILING_STOP: false, ATR_STOP_LOSS_MULTIPLIER: 2.5, // 放宽止损至2.5倍ATR,提升胜率 ATR_TAKE_PROFIT_MULTIPLIER: 1.5, RISK_REWARD_RATIO: 1.5, // 配合止盈倍数 MIN_HOLD_TIME_SEC: 1800, USE_DYNAMIC_ATR_MULTIPLIER: false, // 关闭动态调整,使用固定2.5倍 } }, balanced: { name: '平衡配置', desc: '推荐使用,平衡频率和质量,止损止盈适中(盈亏比1.5:1)', configs: { SCAN_INTERVAL: 600, MIN_CHANGE_PERCENT: 1.5, MIN_SIGNAL_STRENGTH: 4, TOP_N_SYMBOLS: 12, MAX_SCAN_SYMBOLS: 250, MIN_VOLATILITY: 0.018, STOP_LOSS_PERCENT: 8.0, TAKE_PROFIT_PERCENT: 25.0, MIN_STOP_LOSS_PRICE_PCT: 2.0, MIN_TAKE_PROFIT_PRICE_PCT: 3.0, USE_TRAILING_STOP: false, ATR_STOP_LOSS_MULTIPLIER: 2.5, // 放宽止损至2.5倍ATR,提升胜率 ATR_TAKE_PROFIT_MULTIPLIER: 1.5, RISK_REWARD_RATIO: 1.5, // 配合止盈倍数 MIN_HOLD_TIME_SEC: 1800, USE_DYNAMIC_ATR_MULTIPLIER: false, // 关闭动态调整,使用固定2.5倍 } }, aggressive: { name: '激进高频', desc: '晚间波动大时使用,交易频率高,止损较紧但止盈合理(盈亏比1.5:1)', configs: { SCAN_INTERVAL: 300, MIN_CHANGE_PERCENT: 1.0, MIN_SIGNAL_STRENGTH: 3, TOP_N_SYMBOLS: 18, MAX_SCAN_SYMBOLS: 350, MIN_VOLATILITY: 0.015, STOP_LOSS_PERCENT: 5.0, TAKE_PROFIT_PERCENT: 25.0, MIN_STOP_LOSS_PRICE_PCT: 1.5, MIN_TAKE_PROFIT_PRICE_PCT: 2.0, USE_TRAILING_STOP: false, ATR_STOP_LOSS_MULTIPLIER: 2.5, // 放宽止损至2.5倍ATR,提升胜率 ATR_TAKE_PROFIT_MULTIPLIER: 1.5, RISK_REWARD_RATIO: 1.5, // 配合止盈倍数 MIN_HOLD_TIME_SEC: 1800, USE_DYNAMIC_ATR_MULTIPLIER: false, // 关闭动态调整,使用固定2.5倍 } } } // 所有函数定义(必须在 useEffect 之前) const loadUsers = async () => { try { const list = await api.getUsers() setUsers(Array.isArray(list) ? list : []) } catch (error) { setMessage('加载用户列表失败: ' + (error.message || '未知错误')) } finally { setLoading(false) } } const loadAccounts = async () => { try { const list = await api.getAccounts() setAccounts(Array.isArray(list) ? list : []) } catch (error) { console.error('加载账号列表失败:', error) } } const loadConfigMeta = async () => { try { const m = await api.getConfigMeta() setConfigMeta(m || null) } catch (e) { // 静默失败,可能是权限问题 console.error('loadConfigMeta failed:', e) setConfigMeta(null) } } const loadConfigs = async () => { try { // 管理员全局配置:从独立的全局配置表读取,不依赖任何 account if (isAdmin) { const data = await api.getGlobalConfigs() setConfigs(data) } else { // 非管理员不应该访问这个页面 setConfigs({}) } } catch (error) { console.error('Failed to load global configs:', error) setConfigs({}) } } const loadSystemStatus = async () => { try { const res = await api.getTradingSystemStatus() setSystemStatus(res) } catch (error) { // 静默失败 } } const loadBackendStatus = async () => { try { const res = await api.getBackendStatus() setBackendStatus(res) } catch (error) { // 静默失败 } } // 检测当前配置匹配哪个预设方案 const detectCurrentPreset = () => { if (!configs || Object.keys(configs).length === 0) return null for (const [presetKey, preset] of Object.entries(presets)) { let match = true for (const [key, expectedValue] of Object.entries(preset.configs)) { const currentConfig = configs[key] if (!currentConfig) { match = false break } let currentValue = currentConfig.value if (key.includes('PERCENT') || key.includes('PCT')) { if (PCT_LIKE_KEYS.has(key)) { currentValue = currentValue <= 0.05 ? currentValue * 100 : currentValue } else { currentValue = currentValue * 100 } } if (typeof expectedValue === 'number' && typeof currentValue === 'number') { if (Math.abs(currentValue - expectedValue) > 0.01) { match = false break } } else if (currentValue !== expectedValue) { match = false break } } if (match) { return presetKey } } return null } useEffect(() => { loadUsers() loadAccounts() // 只有管理员才加载配置和系统状态 if (isAdmin) { // 加载全局配置(独立于账户) loadConfigs().catch(() => {}) loadConfigMeta().catch(() => {}) // 静默失败 loadSystemStatus().catch(() => {}) // 静默失败 loadBackendStatus().catch(() => {}) // 静默失败 const timer = setInterval(() => { loadSystemStatus().catch(() => {}) loadBackendStatus().catch(() => {}) }, 3000) return () => clearInterval(timer) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAdmin]) // 系统控制函数 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) } } const handleRestartBackend = async () => { if (!window.confirm('确定要重启后端服务吗?重启期间页面接口会短暂不可用(约 3-10 秒)。')) return setSystemBusy(true) setMessage('') try { const res = await api.restartBackend() setMessage(res.message || '已发起后端重启') setTimeout(() => { loadBackendStatus() }, 4000) } catch (error) { setMessage('重启后端失败: ' + (error.message || '未知错误')) } finally { setSystemBusy(false) } } const handleRestartAllTrading = async () => { if (!window.confirm('确定要重启【所有账号】的交易进程吗?这会让所有用户的交易服务短暂中断(约 3-10 秒),用于升级代码后统一生效。')) return setSystemBusy(true) setMessage('') try { const res = await api.restartAllTradingSystems({ prefix: 'auto_sys_acc', do_update: true }) setMessage(`已发起批量重启:共 ${res.count} 个,成功 ${res.ok},失败 ${res.failed}`) } catch (e) { setMessage('批量重启失败: ' + (e?.message || '未知错误')) } finally { setSystemBusy(false) } } const applyPreset = async (presetKey) => { const preset = presets[presetKey] if (!preset) return setSaving(true) setMessage('') try { const configItems = Object.entries(preset.configs).map(([key, value]) => { const config = configs[key] if (!config) { let type = 'number' let category = 'risk' if (typeof value === 'boolean') { type = 'boolean' category = 'strategy' } if (key.startsWith('ENTRY_') || key.startsWith('SMART_ENTRY_') || key === 'SMART_ENTRY_ENABLED') { type = typeof value === 'boolean' ? 'boolean' : 'number' category = 'strategy' } else if (key.startsWith('AUTO_TRADE_')) { type = typeof value === 'boolean' ? 'boolean' : 'number' category = 'strategy' } else if (key === 'LIMIT_ORDER_OFFSET_PCT') { type = 'number' category = 'strategy' } else if (key.includes('PERCENT') || key.includes('PCT')) { type = 'number' if (key.includes('STOP_LOSS') || key.includes('TAKE_PROFIT')) { category = 'risk' } else if (key.includes('POSITION')) { category = 'position' } else { category = 'scan' } } else if (key === 'MIN_VOLATILITY') { type = 'number' category = 'scan' } else if (typeof value === 'number') { type = 'number' category = 'scan' } return { key, value: (key.includes('PERCENT') || key.includes('PCT')) && !PCT_LIKE_KEYS.has(key) ? value / 100 : value, type, category, description: `预设方案配置项:${key}` } } return { key, value: (key.includes('PERCENT') || key.includes('PCT')) && !PCT_LIKE_KEYS.has(key) ? value / 100 : value, type: config.type, category: config.category, description: config.description } }).filter(Boolean) // 管理员全局配置:使用独立的全局配置API let response if (isAdmin) { response = await api.updateGlobalConfigsBatch(configItems) } else { // 非管理员不应该访问这个页面,但为了安全还是处理一下 throw new Error('只有管理员可以修改全局配置') } setMessage(response.message || `已应用${preset.name}`) if (response.note) { setTimeout(() => { setMessage(response.note) }, 2000) } await loadConfigs() } catch (error) { setMessage('应用预设失败: ' + error.message) } finally { setSaving(false) } } // 配置快照函数 const isSecretKey = (key) => { return key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET' } const maskSecret = (val) => { const s = val === null || val === undefined ? '' : String(val) if (!s) return '' if (s.length <= 8) return '****' return `${s.slice(0, 4)}...${s.slice(-4)}` } const toDisplayValueForSnapshot = (key, value) => { if (value === null || value === undefined) return value if (typeof value === 'number' && (key.includes('PERCENT') || key.includes('PCT'))) { if (PCT_LIKE_KEYS.has(key)) { return value <= 0.05 ? value * 100 : value } return value < 1 ? value * 100 : value } return value } const buildConfigSnapshot = async (includeSecrets) => { // 管理员全局配置:从独立的全局配置表读取 let data if (isAdmin) { data = await api.getGlobalConfigs() } else { data = await api.getConfigs() } const now = new Date() const categoryMap = { scan: '市场扫描', position: '仓位控制', risk: '风险控制', strategy: '策略参数', api: 'API配置', } const entries = Object.entries(data || {}).map(([key, cfg]) => { const rawVal = cfg?.value const valMasked = isSecretKey(key) && !includeSecrets ? maskSecret(rawVal) : rawVal const displayVal = toDisplayValueForSnapshot(key, valMasked) return { key, category: cfg?.category || '', category_label: categoryMap[cfg?.category] || cfg?.category || '', type: cfg?.type || '', value: valMasked, display_value: displayVal, description: cfg?.description || '', } }) entries.sort((a, b) => { const ca = a.category_label || a.category || '' const cb = b.category_label || b.category || '' if (ca !== cb) return ca.localeCompare(cb) return a.key.localeCompare(b.key) }) // 临时获取当前配置以检测预设 const tempConfigs = data || {} let detectedPreset = null for (const [presetKey, preset] of Object.entries(presets)) { let match = true for (const [key, expectedValue] of Object.entries(preset.configs)) { const currentConfig = tempConfigs[key] if (!currentConfig) { match = false break } let currentValue = currentConfig.value if (key.includes('PERCENT') || key.includes('PCT')) { if (PCT_LIKE_KEYS.has(key)) { currentValue = currentValue <= 0.05 ? currentValue * 100 : currentValue } else { currentValue = currentValue * 100 } } if (typeof expectedValue === 'number' && typeof currentValue === 'number') { if (Math.abs(currentValue - expectedValue) > 0.01) { match = false break } } else if (currentValue !== expectedValue) { match = false break } } if (match) { detectedPreset = presetKey break } } const snapshot = { fetched_at: now.toISOString(), note: 'display_value 对 PERCENT/PCT 做了百分比换算;敏感字段可选择脱敏/明文。', preset_detected: detectedPreset, system_status: systemStatus ? { running: !!systemStatus.running, pid: systemStatus.pid || null, program: systemStatus.program || null, state: systemStatus.state || null, } : null, configs: entries, } return JSON.stringify(snapshot, null, 2) } const openSnapshot = async (includeSecrets) => { setSnapshotBusy(true) setMessage('') try { const text = await buildConfigSnapshot(includeSecrets) setSnapshotText(text) setShowSnapshot(true) } catch (e) { setMessage('生成配置快照失败: ' + (e?.message || '未知错误')) } finally { setSnapshotBusy(false) } } const copySnapshot = async () => { try { if (navigator?.clipboard?.writeText) { await navigator.clipboard.writeText(snapshotText || '') setMessage('已复制配置快照到剪贴板') } else { setMessage('当前浏览器不支持剪贴板 API,可手动全选复制') } } catch (e) { setMessage('复制失败: ' + (e?.message || '未知错误')) } } const downloadSnapshot = () => { try { const blob = new Blob([snapshotText || ''], { type: 'application/json;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `config-snapshot-${new Date().toISOString().replace(/[:.]/g, '-')}.json` document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) } catch (e) { setMessage('下载失败: ' + (e?.message || '未知错误')) } } const handleCreateUser = async () => { if (!newUser.username || !newUser.password) { setMessage('用户名和密码不能为空') return } setBusy(true) setMessage('') try { await api.createUser(newUser) setMessage('用户创建成功') setShowUserForm(false) setNewUser({ username: '', password: '', role: 'user', status: 'active' }) await loadUsers() } catch (error) { setMessage('创建用户失败: ' + (error.message || '未知错误')) } finally { setBusy(false) } } const handleUpdateUserPassword = async (userId) => { const passwordInput = document.querySelector(`input[data-user-id="${userId}"]`) const password = passwordInput?.value if (!password) { setMessage('密码不能为空') return } setBusy(true) setMessage('') try { await api.updateUserPassword(userId, password) setMessage('密码更新成功') setEditingUserId(null) if (passwordInput) passwordInput.value = '' await loadUsers() } catch (error) { setMessage('更新密码失败: ' + (error.message || '未知错误')) } finally { setBusy(false) } } const handleUpdateUserRole = async (userId, role) => { setBusy(true) setMessage('') try { await api.updateUserRole(userId, role) setMessage('角色更新成功') await loadUsers() } catch (error) { setMessage('更新角色失败: ' + (error.message || '未知错误')) } finally { setBusy(false) } } const handleUpdateUserStatus = async (userId, status) => { setBusy(true) setMessage('') try { await api.updateUserStatus(userId, status) setMessage('状态更新成功') await loadUsers() } catch (error) { setMessage('更新状态失败: ' + (error.message || '未知错误')) } finally { setBusy(false) } } if (loading) { return
加载中...
} // 管理员全局配置页面:不依赖任何 account,直接管理全局配置表 const isGlobalStrategyAccount = isAdmin // 简单计算:当前预设(直接在 render 时计算,不使用 useMemo) let currentPreset = null if (configs && Object.keys(configs).length > 0 && presets) { try { // 直接内联检测逻辑,避免函数调用 for (const [presetKey, preset] of Object.entries(presets)) { let match = true for (const [key, expectedValue] of Object.entries(preset.configs)) { const currentConfig = configs[key] if (!currentConfig) { match = false break } let currentValue = currentConfig.value if (key.includes('PERCENT') || key.includes('PCT')) { if (PCT_LIKE_KEYS.has(key)) { currentValue = currentValue <= 0.05 ? currentValue * 100 : currentValue } else { currentValue = currentValue * 100 } } if (typeof expectedValue === 'number' && typeof currentValue === 'number') { if (Math.abs(currentValue - expectedValue) > 0.01) { match = false break } } else if (currentValue !== expectedValue) { match = false break } } if (match) { currentPreset = presetKey break } } } catch (e) { console.error('detectCurrentPreset error:', e) } } const presetUiMeta = { swing: { group: 'limit', tag: '纯限价' }, strict: { group: 'limit', tag: '纯限价' }, fill: { group: 'smart', tag: '智能入场' }, steady: { group: 'smart', tag: '智能入场' }, conservative: { group: 'legacy', tag: '传统' }, balanced: { group: 'legacy', tag: '传统' }, aggressive: { group: 'legacy', tag: '高频实验' }, } const presetGroups = [ { key: 'limit', title: 'A. 纯限价(SMART_ENTRY_ENABLED=false)', desc: '只下 1 次限价单,未在确认时间内成交就撤单跳过。更控频、更接近"波段",但更容易出现 NEW→撤单。', presetKeys: ['swing', 'strict'], }, { key: 'smart', title: 'B. 智能入场(SMART_ENTRY_ENABLED=true)', desc: '限价回调 + 受限追价 +(趋势强时)可控市价兜底。更少漏单,但必须限制追价步数与偏离上限,避免回到高频追价。', presetKeys: ['fill', 'steady'], }, { key: 'legacy', title: 'C. 传统 / 实验(不建议长期)', desc: '这组更多用于对比或临时实验(频率更高/更容易过度交易),建议在稳定盈利前谨慎使用。', presetKeys: ['conservative', 'balanced', 'aggressive'], }, ] return (

全局配置

管理用户、账号和全局策略配置

📖 配置说明
{message && (
{message}
)} {/* 系统控制 */} {isAdmin && (

系统控制

{systemStatus?.running ? '运行中' : '未运行'} {systemStatus?.pid ? PID: {systemStatus.pid} : null} {systemStatus?.program ? 程序: {systemStatus.program} : null} {systemStatus?.meta?.requested_at ? 上次重启: {systemStatus.meta.requested_at} : null}
后端 {backendStatus?.running ? '运行中' : '未知'} {backendStatus?.pid ? PID: {backendStatus.pid} : null} {backendStatus?.meta?.requested_at ? 上次重启: {backendStatus.meta.requested_at} : null}
建议流程:先更新配置里的 Key → 点击"清除缓存" → 点击"重启交易系统",确保不再使用旧账号下单。
)} {/* 预设方案快速切换(仅管理员 + 全局策略账号) */} {isAdmin && isGlobalStrategyAccount && (

快速切换方案

当前方案: {currentPreset && presets && presets[currentPreset] ? presets[currentPreset].name : '自定义'}
怎么选更不迷糊
  • 先选入场机制:纯限价(更控频但可能撤单) vs 智能入场(更少漏单但需限制追价)。
  • 再看"会不会下单":如果你发现几乎不出单,优先把 AUTO_TRADE_ONLY_TRENDING 关掉、把 AUTO_TRADE_ALLOW_4H_NEUTRAL 打开。
  • 最后再微调:想更容易成交 → 调小 LIMIT_ORDER_OFFSET_PCT、调大 ENTRY_CONFIRM_TIMEOUT_SEC
{presetGroups.map((g) => (
{g.title}
{g.desc}
{g.presetKeys .filter((k) => presets && presets[k]) .map((k) => { const preset = presets && presets[k] ? presets[k] : null if (!preset) return null const meta = presetUiMeta && presetUiMeta[k] ? presetUiMeta[k] : { group: g.key, tag: '' } return ( ) }) .filter(Boolean)}
))}
)} {/* 全局策略配置项编辑(仅管理员) */} {isAdmin && (

全局策略配置

修改全局策略配置,所有普通用户账号将使用这些配置(风险旋钮除外)

{Object.keys(configs).length > 0 ? ( (() => { const configCategories = { 'scan': '市场扫描', 'position': '仓位控制', 'risk': '风险控制', 'strategy': '策略参数', } return Object.entries(configCategories).map(([category, label]) => { const categoryConfigs = Object.entries(configs).filter(([key, config]) => { if (config.category !== category) return false // 排除风险旋钮(这些由用户自己控制) 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'] if (RISK_KNOBS_KEYS.includes(key)) return false // 排除API密钥(在账号管理中) if (key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET' || key === 'USE_TESTNET') return false return true }) if (categoryConfigs.length === 0) return null return (

{label}

{categoryConfigs.map(([key, 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} /> ))}
) }) })() ) : (
{loading ? '加载配置中...' : '暂无配置项'}
)}
)} {/* 用户管理 */}

用户管理

{showUserForm && (

创建新用户

setNewUser({ ...newUser, username: e.target.value })} placeholder="输入用户名" />
setNewUser({ ...newUser, password: e.target.value })} placeholder="输入密码" />
)}
{users.map((user) => ( ))}
ID 用户名 角色 状态 操作
{user.id} {user.username} {editingUserId === user.id ? (
{ if (e.key === 'Enter') { handleUpdateUserPassword(user.id) } }} />
) : ( )}
{/* 账号管理 */}

账号管理

{accounts.map((account) => ( ))}
ID 名称 状态 测试网 API Key
{account.id} {account.name || '未命名'} {account.status === 'active' ? '启用' : '禁用'} {account.use_testnet ? '是' : '否'} {account.has_api_key ? '已配置' : '未配置'}
{/* 配置快照 Modal */} {showSnapshot && (
setShowSnapshot(false)} role="presentation">
e.stopPropagation()}>

当前整体配置快照

默认脱敏 BINANCE_API_KEY/SECRET。你可以选择明文后重新生成再复制/下载。
{snapshotText}
)}
) } export default GlobalConfig