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 组件
// 部分配置项使用“数值原样”(如 RSI 0–100、24h 涨跌幅 25 表示 25%),不做 0–1 比例转换
const NUMBER_AS_IS_KEYS = new Set([
'MAX_RSI_FOR_LONG',
'MIN_RSI_FOR_SHORT',
'MAX_CHANGE_PERCENT_FOR_LONG',
'MAX_CHANGE_PERCENT_FOR_SHORT',
])
// 配置项中文标签(便于识别)
const KEY_LABELS = {
MAX_RSI_FOR_LONG: '做多 RSI 上限',
MIN_RSI_FOR_SHORT: '做空 RSI 下限',
MAX_CHANGE_PERCENT_FOR_LONG: '做多 24h 涨跌幅上限(%)',
MAX_CHANGE_PERCENT_FOR_SHORT: '做空 24h 涨跌幅上限(%)',
TAKE_PROFIT_1_PERCENT: '第一目标止盈(%)',
SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT: '智能补单候选数',
SYMBOL_LOSS_COOLDOWN_ENABLED: '连续亏损冷却',
SYMBOL_MAX_CONSECUTIVE_LOSSES: '连续亏损次数阈值',
SYMBOL_LOSS_COOLDOWN_SEC: '冷却时间(秒)',
}
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 isNumberAsIs = NUMBER_AS_IS_KEYS.has(label)
const displayLabel = KEY_LABELS[label] || label
const formatPercent = (n) => {
if (typeof n !== 'number' || isNaN(n)) return ''
return n.toFixed(4).replace(/\.?0+$/, '')
}
const getInitialDisplayValue = (val) => {
if (config.type === 'number' && isNumberAsIs) {
if (val === null || val === undefined || val === '') return ''
const numVal = typeof val === 'string' ? parseFloat(val) : val
return isNaN(numVal) ? '' : String(numVal)
}
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) {
return formatPercent(numVal <= 0.05 ? numVal * 100 : numVal)
}
return formatPercent(numVal <= 1 ? numVal : numVal / 100)
}
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 (isNumberAsIs) {
const numVal = parseFloat(localValue)
if (isNaN(numVal)) {
setLocalValue(getInitialDisplayValue(config.value))
setIsEditing(false)
return
}
finalValue = numVal
} else if (isPercentKey) {
const numVal = parseFloat(localValue)
if (isNaN(numVal)) {
setLocalValue(getInitialDisplayValue(config.value))
setIsEditing(false)
return
}
if (numVal < 0 || numVal > 1) {
setLocalValue(getInitialDisplayValue(config.value))
setIsEditing(false)
return
}
finalValue = numVal
} 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' && !isNumberAsIs) {
if (isPercentKey) {
const numValue = parseFloat(newValue)
if (newValue !== '' && !isNaN(numValue) && (numValue < 0 || numValue > 1)) {
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' }}
/>
)}
{/* ⚠️ 简化:去掉%符号,直接显示小数(0.30) */}
{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倍
}
},
altcoin: {
name: '山寨币狙击(高盈亏比)',
desc: '专为山寨币设计:宽止损(2.0倍ATR+12%固定)、合理盈亏比(3:1)、移动止损保护利润、严格成交量过滤(≥3000万美元)。2026-01-27优化:让收益率真实,胜率正常化。期望胜率40%+,盈亏比1.5:1+。',
configs: {
// 风险控制(最关键)- 2026-01-27优化:让收益率真实,胜率正常化
ATR_STOP_LOSS_MULTIPLIER: 1.5, // ATR止损1.5倍(2026-01-27优化:收紧止损,减少单笔亏损)
STOP_LOSS_PERCENT: 12.0, // 固定止损12%(收紧止损,减少单笔亏损)
RISK_REWARD_RATIO: 3.0, // 盈亏比3:1(降低,更容易触发,保证胜率)
ATR_TAKE_PROFIT_MULTIPLIER: 2.0, // ATR止盈2.0倍(2026-01-27优化:降低止盈目标,更容易触发)
TAKE_PROFIT_PERCENT: 20.0, // 固定止盈20%(降低止盈目标,更容易触发,提升止盈单比例)
MIN_HOLD_TIME_SEC: 0, // 取消持仓锁(山寨币变化快)
USE_FIXED_RISK_SIZING: true, // 固定风险
FIXED_RISK_PERCENT: 1.0, // 每笔最多亏1%
USE_DYNAMIC_ATR_MULTIPLIER: false, // 不使用动态ATR
// 移动止损(必须开启)- 2026-01-27优化:与第一目标止盈一致
USE_TRAILING_STOP: true, // 启用移动止损保护利润
TRAILING_STOP_ACTIVATION: 20.0, // 盈利20%后激活(与第一目标止盈一致)
TRAILING_STOP_PROTECT: 10.0, // 保护10%利润(给回撤足够空间)
// 仓位管理
MAX_POSITION_PERCENT: 1.5, // 单笔1.5%(山寨币不加仓)
MAX_TOTAL_POSITION_PERCENT: 12.0, // 总仓位12%
MAX_DAILY_ENTRIES: 8, // 每日最多8笔(增加交易频率)
MAX_OPEN_POSITIONS: 4, // 最多4个持仓
LEVERAGE: 8, // 基础杠杆8倍
MAX_LEVERAGE: 12, // 最大杠杆12倍
USE_DYNAMIC_LEVERAGE: false, // 不使用动态杠杆
// 品种筛选(流动性为王)
MIN_VOLUME_24H: 30000000, // 24H成交额≥3000万美元
MIN_VOLUME_24H_STRICT: 50000000, // 严格过滤≥5000万
MIN_VOLATILITY: 3.0, // 最小波动率3%
TOP_N_SYMBOLS: 8, // 选择信号最强的8个(给更多选择余地,避免错过好机会)
MAX_SCAN_SYMBOLS: 250, // 扫描前250个(增加覆盖率,从27.6%提升到46.0%)
MIN_SIGNAL_STRENGTH: 5, // 信号强度≥5(MACD金叉/死叉已足够,配合其他筛选)
EXCLUDE_MAJOR_COINS: true, // 排除主流币(BTC、ETH、BNB等),专注于山寨币
// 时间框架
SCAN_INTERVAL: 1800, // 扫描间隔30分钟(增加交易机会)
PRIMARY_INTERVAL: '4h', // 主周期4小时
ENTRY_INTERVAL: '1h', // 入场周期1小时
CONFIRM_INTERVAL: '1d', // 确认周期日线
// 智能入场
SMART_ENTRY_ENABLED: true, // 开启智能入场
ENTRY_SYMBOL_COOLDOWN_SEC: 1800, // 币种冷却30分钟
ENTRY_MAX_DRIFT_PCT_TRENDING: 0.8, // 追价偏离0.8%
ENTRY_MAX_DRIFT_PCT_RANGING: 0.3, // 震荡偏离0.3%
// 交易控制
AUTO_TRADE_ONLY_TRENDING: true, // 只做趋势市
AUTO_TRADE_ALLOW_4H_NEUTRAL: true, // 允许4H中性(提高交易频率,宽止损+高盈亏比已考虑低胜率)
},
},
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 KNOWN_GLOBAL_CONFIG_DEFAULTS = {
MAX_RSI_FOR_LONG: { value: 70, type: 'number', category: 'strategy', description: '做多时 RSI 超过此值则不开多(避免超买区追多)。2026-01-31新增。' },
MAX_CHANGE_PERCENT_FOR_LONG: { value: 25, type: 'number', category: 'strategy', description: '做多时 24h 涨跌幅超过此值则不开多(避免追大涨)。单位:百分比数值,如 25 表示 25%。' },
MIN_RSI_FOR_SHORT: { value: 30, type: 'number', category: 'strategy', description: '做空时 RSI 低于此值则不做空(避免深超卖反弹)。2026-01-31新增。' },
MAX_CHANGE_PERCENT_FOR_SHORT: { value: 10, type: 'number', category: 'strategy', description: '做空时 24h 涨跌幅超过此值则不做空(24h 仍大涨时不做空)。单位:百分比数值。' },
TAKE_PROFIT_1_PERCENT: { value: 0.15, type: 'number', category: 'strategy', description: '分步止盈第一目标(保证金百分比,如 0.15=15%)。第一目标触发后了结50%仓位。' },
SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT: { value: 8, type: 'number', category: 'scan', description: '智能补单:多返回的候选数量,冷却时仍可尝试后续交易对。' },
SYMBOL_LOSS_COOLDOWN_ENABLED: { value: true, type: 'boolean', category: 'strategy', description: '是否启用同一交易对连续亏损后的冷却。' },
SYMBOL_MAX_CONSECUTIVE_LOSSES: { value: 2, type: 'number', category: 'strategy', description: '最大允许连续亏损次数(超过则禁止交易该交易对一段时间)。' },
SYMBOL_LOSS_COOLDOWN_SEC: { value: 3600, type: 'number', category: 'strategy', description: '连续亏损后的冷却时间(秒),默认1小时。' },
BETA_FILTER_ENABLED: { value: true, type: 'boolean', category: 'strategy', description: '大盘共振过滤:BTC/ETH 下跌时屏蔽多单。' },
BETA_FILTER_THRESHOLD: { value: -0.005, type: 'number', category: 'strategy', description: '大盘共振阈值(比例,如 -0.005 表示 -0.5%)。' },
}
const loadConfigs = async () => {
try {
// 管理员全局配置:从独立的全局配置表读取,不依赖任何 account
if (isAdmin) {
const data = await api.getGlobalConfigs()
// 兜底:若后端未返回新配置项(如未重启),用已知默认值合并,确保前端能看到
const merged = { ...(data || {}) }
Object.keys(KNOWN_GLOBAL_CONFIG_DEFAULTS).forEach(key => {
if (!(key in merged)) {
merged[key] = KNOWN_GLOBAL_CONFIG_DEFAULTS[key]
}
})
console.log('Loaded global configs:', Object.keys(merged).length, 'keys')
setConfigs(merged)
} 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 = {
altcoin: { group: 'altcoin', tag: '山寨币专用' },
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: 'altcoin',
title: '⭐ 山寨币高盈亏比狙击策略',
desc: '专为山寨币设计:宽止损(2.0×ATR)+ 高盈亏比(4:1)+ 移动止损 + 严格流动性筛选。目标胜率35%,期望值+0.75%/笔。',
presetKeys: ['altcoin'],
},
{
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.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]) => {
// 确保config是对象且有category字段
if (!config || typeof config !== 'object') {
console.warn(`Config ${key} is not an object:`, config)
return false
}
if (!config.category || 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
console.log(`Category ${category} (${label}): ${categoryConfigs.length} configs`, categoryConfigs.map(([k]) => k))
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 && (
)}
{/* 账号管理 */}
账号管理
| ID |
名称 |
状态 |
测试网 |
API Key |
{accounts.map((account) => (
| {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