From 3bac0422733441810513555f119f16823e1c6f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Thu, 22 Jan 2026 11:24:57 +0800 Subject: [PATCH] a --- frontend/src/App.jsx | 144 +++- frontend/src/components/AccountSelector.jsx | 122 +-- frontend/src/components/ConfigPanel.jsx | 304 +------- frontend/src/components/GlobalConfig.jsx | 790 +++++++++++++++++++- 4 files changed, 991 insertions(+), 369 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7278f4f..90b784c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -10,23 +10,54 @@ import AccountSelector from './components/AccountSelector' import GlobalConfig from './components/GlobalConfig' import Login from './components/Login' import { api, clearAuthToken, clearCurrentAccountId, setCurrentAccountId, getCurrentAccountId } from './services/api' + +const VIEWING_USER_ID_KEY = 'ats_viewing_user_id' import './App.css' function App() { const [me, setMe] = useState(null) const [checking, setChecking] = useState(true) + const [users, setUsers] = useState([]) + const [viewingUserId, setViewingUserId] = useState(() => { + // 管理员:从localStorage读取当前查看的用户ID,默认是当前登录用户 + const stored = localStorage.getItem(VIEWING_USER_ID_KEY) + if (stored) { + const parsed = parseInt(stored, 10) + if (Number.isFinite(parsed) && parsed > 0) return parsed + } + return null + }) + const [showUserPopover, setShowUserPopover] = useState(false) + const userPopoverRef = React.useRef(null) const refreshMe = async () => { try { const u = await api.me() setMe(u) + + const isAdmin = (u?.role || '') === 'admin' + + // 管理员:加载用户列表 + if (isAdmin) { + try { + const userList = await api.getUsers() + setUsers(Array.isArray(userList) ? userList : []) + // 如果viewingUserId未设置或不在列表中,设置为当前登录用户 + const currentUserId = parseInt(String(u?.id || ''), 10) + if (!viewingUserId || !userList.some((user) => parseInt(String(user?.id || ''), 10) === viewingUserId)) { + setViewingUserId(currentUserId) + localStorage.setItem(VIEWING_USER_ID_KEY, String(currentUserId)) + } + } catch (e) { + console.error('加载用户列表失败:', e) + } + } // 登录后默认选择账号(避免 admin/用户切换时沿用旧 accountId) try { const list = await api.getAccounts() const accounts = Array.isArray(list) ? list : [] const active = accounts.filter((a) => String(a?.status || 'active') === 'active') - const isAdmin = (u?.role || '') === 'admin' let target = null if (isAdmin) { @@ -53,6 +84,34 @@ function App() { setChecking(false) } } + + // 点击外部关闭popover + React.useEffect(() => { + const handleClickOutside = (event) => { + if (userPopoverRef.current && !userPopoverRef.current.contains(event.target)) { + setShowUserPopover(false) + } + } + if (showUserPopover) { + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + } + }, [showUserPopover]) + + // 当前查看的用户信息 + const currentViewingUser = users.find((u) => parseInt(String(u?.id || ''), 10) === viewingUserId) + const effectiveUserId = isAdmin && viewingUserId ? viewingUserId : parseInt(String(me?.id || ''), 10) + + const handleSwitchUser = (userId) => { + const nextUserId = parseInt(String(userId || ''), 10) + if (Number.isFinite(nextUserId) && nextUserId > 0) { + setViewingUserId(nextUserId) + localStorage.setItem(VIEWING_USER_ID_KEY, String(nextUserId)) + setShowUserPopover(false) + // 切换用户后,触发account变化事件,让AccountSelector重新加载该用户的账号列表 + window.dispatchEvent(new CustomEvent('ats:user:switched', { detail: { userId: nextUserId } })) + } + } useEffect(() => { refreshMe() @@ -82,7 +141,7 @@ function App() {

自动交易系统

- +
仪表板 @@ -97,15 +156,88 @@ function App() { ) : null}
- - {me?.username ? me.username : 'user'}{isAdmin ? '(管理员)' : ''} - + {isAdmin ? ( +
+ + {showUserPopover && ( +
+
+ 切换用户视角 +
+
+ {users.map((u) => ( +
handleSwitchUser(u.id)} + style={{ + padding: '12px 16px', + cursor: 'pointer', + transition: 'background-color 0.2s', + borderBottom: '1px solid #f0f0f0', + background: viewingUserId === u.id ? '#e3f2fd' : 'white' + }} + onMouseEnter={(e) => { + if (viewingUserId !== u.id) e.currentTarget.style.background = '#f5f5f5' + }} + onMouseLeave={(e) => { + if (viewingUserId !== u.id) e.currentTarget.style.background = 'white' + }} + > +
+ {u.username || 'user'} {u.role === 'admin' ? '(管理员)' : ''} +
+
+ {u.status === 'active' ? '启用' : '禁用'} +
+
+ ))} +
+
+ )} +
+ ) : ( + + {me?.username ? me.username : 'user'} + + )} - {showUserPopover && ( -
-
切换用户视角
-
- {users.map((u) => ( -
handleSwitchUser(u.id)} - > -
- {u.username || 'user'} {u.role === 'admin' ? '(管理员)' : ''} -
-
- {u.status === 'active' ? '启用' : '禁用'} -
-
- ))} -
-
- )} -
- ) : null} 账号 { - const checked = e.target.checked - setSnapshotIncludeSecrets(checked) - // 重新生成(确保脱敏/明文一致) - await openSnapshot(checked) - }} - /> - 显示敏感信息(明文) - - -
- - -
-
- -
{snapshotText}
- - - )} + {/* 配置快照已移至全局配置页面 */} ) } diff --git a/frontend/src/components/GlobalConfig.jsx b/frontend/src/components/GlobalConfig.jsx index fd50c40..33a900b 100644 --- a/frontend/src/components/GlobalConfig.jsx +++ b/frontend/src/components/GlobalConfig.jsx @@ -1,6 +1,8 @@ -import React, { useState, useEffect } from 'react' -import { api } from '../services/api' +import React, { useState, useEffect, useRef } from 'react' +import { Link } from 'react-router-dom' +import { api, getCurrentAccountId } from '../services/api' import './GlobalConfig.css' +import './ConfigPanel.css' // 复用 ConfigPanel 的样式 const GlobalConfig = ({ currentUser }) => { const [users, setUsers] = useState([]) @@ -12,10 +14,191 @@ const GlobalConfig = ({ currentUser }) => { 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 [currentAccountId, setCurrentAccountId] = useState(getCurrentAccountId()) + 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', + ]) + + const isAdmin = (currentUser?.role || '') === 'admin' + const globalStrategyAccountId = parseInt(String(configMeta?.global_strategy_account_id || '1'), 10) || 1 + const isGlobalStrategyAccount = isAdmin && currentAccountId === globalStrategyAccountId + + // 预设方案配置 + 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_TAKE_PROFIT_MULTIPLIER: 4.5, + TAKE_PROFIT_PERCENT: 25.0, + MIN_HOLD_TIME_SEC: 1800, + SMART_ENTRY_ENABLED: false, + }, + }, + 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_TAKE_PROFIT_MULTIPLIER: 4.5, + TAKE_PROFIT_PERCENT: 25.0, + MIN_HOLD_TIME_SEC: 1800, + }, + }, + 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_TAKE_PROFIT_MULTIPLIER: 4.5, + TAKE_PROFIT_PERCENT: 25.0, + MIN_HOLD_TIME_SEC: 1800, + }, + }, + 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_TAKE_PROFIT_MULTIPLIER: 4.5, + TAKE_PROFIT_PERCENT: 25.0, + MIN_HOLD_TIME_SEC: 1800, + }, + }, + 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_TAKE_PROFIT_MULTIPLIER: 4.5, + MIN_HOLD_TIME_SEC: 1800 + } + }, + balanced: { + name: '平衡配置', + desc: '推荐使用,平衡频率和质量,止损止盈适中(盈亏比2.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_TAKE_PROFIT_MULTIPLIER: 4.5, + MIN_HOLD_TIME_SEC: 1800 + } + }, + aggressive: { + name: '激进高频', + desc: '晚间波动大时使用,交易频率高,止损较紧但止盈合理(盈亏比3: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_TAKE_PROFIT_MULTIPLIER: 4.5, + MIN_HOLD_TIME_SEC: 1800 + } + } + } useEffect(() => { loadUsers() loadAccounts() + if (isAdmin) { + loadConfigMeta() + loadConfigs() + loadSystemStatus() + loadBackendStatus() + + const timer = setInterval(() => { + loadSystemStatus() + loadBackendStatus() + }, 3000) + return () => clearInterval(timer) + } }, []) const loadUsers = async () => { @@ -38,6 +221,364 @@ const GlobalConfig = ({ currentUser }) => { } } + const loadConfigMeta = async () => { + try { + const m = await api.getConfigMeta() + setConfigMeta(m || null) + } catch (e) { + setConfigMeta(null) + } + } + + const loadConfigs = async () => { + try { + const data = await api.getConfigs() + setConfigs(data) + } catch (error) { + console.error('Failed to load configs:', error) + } + } + + 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 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 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 + } + + 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) + + const response = await api.updateConfigsBatch(configItems) + 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) => { + const 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 snapshot = { + fetched_at: now.toISOString(), + note: 'display_value 对 PERCENT/PCT 做了百分比换算;敏感字段可选择脱敏/明文。', + preset_detected: detectCurrentPreset(), + 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 currentPreset = detectCurrentPreset() + const handleCreateUser = async () => { if (!newUser.username || !newUser.password) { setMessage('用户名和密码不能为空') @@ -112,11 +653,58 @@ const GlobalConfig = ({ currentUser }) => { return
加载中...
} + 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 && ( @@ -125,6 +713,155 @@ const GlobalConfig = ({ currentUser }) => {
)} + {/* 系统控制 */} + {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[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[k]) + .map((k) => { + const preset = presets[k] + const meta = presetUiMeta[k] || { group: g.key, tag: '' } + return ( + + ) + })} +
+
+ ))} +
+
+ )} + {/* 用户管理 */}
@@ -321,6 +1058,51 @@ const GlobalConfig = ({ currentUser }) => {
+ + {/* 配置快照 Modal */} + {showSnapshot && ( +
setShowSnapshot(false)} role="presentation"> +
e.stopPropagation()}> +
+
+

当前整体配置快照

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