import React, { useState, useEffect, useRef } from 'react' import { Link } from 'react-router-dom' import { useSelector, useDispatch } from 'react-redux' import { api } from '../services/api' import { setAccountId, selectAccountId, selectCurrentUser, selectViewingUserId, selectIsAdmin, selectEffectiveUserId, } from '../store/appSlice' import './ConfigPanel.css' const ConfigPanel = () => { const dispatch = useDispatch() const accountId = useSelector(selectAccountId) // 从 Redux 获取当前账号ID const currentUser = useSelector(selectCurrentUser) const isAdmin = useSelector(selectIsAdmin) const [configs, setConfigs] = useState({}) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [message, setMessage] = useState('') const [feasibilityCheck, setFeasibilityCheck] = useState(null) const [checkingFeasibility, setCheckingFeasibility] = useState(false) const [systemStatus, setSystemStatus] = useState(null) const [backendStatus, setBackendStatus] = useState(null) const [systemBusy, setSystemBusy] = useState(false) const [accountTradingStatus, setAccountTradingStatus] = useState(null) const [accountTradingErr, setAccountTradingErr] = useState('') const [currentAccountMeta, setCurrentAccountMeta] = useState(null) const [configMeta, setConfigMeta] = useState(null) const globalStrategyAccountId = parseInt(String(configMeta?.global_strategy_account_id || '1'), 10) || 1 const isGlobalStrategyAccount = isAdmin && accountId === globalStrategyAccountId const loadConfigMeta = async () => { try { const m = await api.getConfigMeta() setConfigMeta(m || null) } catch (e) { setConfigMeta(null) } } // 账号管理(超管) const [accountsAdmin, setAccountsAdmin] = useState([]) const [accountsBusy, setAccountsBusy] = useState(false) const [showAccountsAdmin, setShowAccountsAdmin] = useState(false) const [newAccount, setNewAccount] = useState({ name: '', api_key: '', api_secret: '', use_testnet: false, status: 'active', }) const [credEditId, setCredEditId] = useState(null) const [credForm, setCredForm] = useState({ api_key: '', api_secret: '', use_testnet: false }) // “PCT”类配置里有少数是“百分比数值(<=1表示<=1%)”,而不是“0~1比例” // 例如 LIMIT_ORDER_OFFSET_PCT=0.5 表示 0.5%(而不是 50%) const PCT_LIKE_KEYS = new Set([ 'LIMIT_ORDER_OFFSET_PCT', 'ENTRY_MAX_DRIFT_PCT_TRENDING', 'ENTRY_MAX_DRIFT_PCT_RANGING', ]) // 配置快照(用于整体分析/导出) const [showSnapshot, setShowSnapshot] = useState(false) const [snapshotText, setSnapshotText] = useState('') const [snapshotIncludeSecrets, setSnapshotIncludeSecrets] = useState(false) const [snapshotBusy, setSnapshotBusy] = useState(false) // 预设方案配置 // 注意:百分比配置使用整数形式(如8.0表示8%),在应用时会转换为小数(0.08) const presets = { altcoin: { name: '⭐山寨币狙击', desc: '高盈亏比(4:1)+ 宽止损(2.0×ATR)+ 移动止损保护 + 严格流动性筛选。期望胜率35%,每笔+0.75%。', configs: { ATR_STOP_LOSS_MULTIPLIER: 2.0, STOP_LOSS_PERCENT: 15.0, RISK_REWARD_RATIO: 4.0, ATR_TAKE_PROFIT_MULTIPLIER: 8.0, TAKE_PROFIT_PERCENT: 60.0, MIN_HOLD_TIME_SEC: 0, USE_FIXED_RISK_SIZING: true, FIXED_RISK_PERCENT: 1.0, USE_TRAILING_STOP: true, TRAILING_STOP_ACTIVATION: 30.0, TRAILING_STOP_PROTECT: 15.0, MAX_POSITION_PERCENT: 1.5, MAX_TOTAL_POSITION_PERCENT: 12.0, MAX_DAILY_ENTRIES: 5, MIN_VOLUME_24H: 30000000, MIN_VOLATILITY: 3.0, TOP_N_SYMBOLS: 5, MIN_SIGNAL_STRENGTH: 7, SCAN_INTERVAL: 3600, SMART_ENTRY_ENABLED: true, AUTO_TRADE_ONLY_TRENDING: true, }, }, swing: { name: '波段回归(推荐)', desc: '根治高频与追价:关闭智能入场,回归“纯限价 + 30分钟扫描 + 更高信号门槛”的低频波段。建议先跑20-30单再评估。', configs: { // 操作频率 SCAN_INTERVAL: 1800, // 30分钟 TOP_N_SYMBOLS: 8, // 仓位管理(重要语义:这些百分比均按“保证金占用比例”理解) MAX_POSITION_PERCENT: 2.0, // 2% MAX_TOTAL_POSITION_PERCENT: 20.0, // 20% MIN_POSITION_PERCENT: 0.0, // 0%(等价于关闭最小仓位占比) // 风控 MIN_SIGNAL_STRENGTH: 8, USE_TRAILING_STOP: false, // 禁用移动止损,让利润奔跑 ATR_TAKE_PROFIT_MULTIPLIER: 1.5, // ATR止盈倍数(从4.5降至1.5,将盈亏比从3:1降至更现实、可达成的1.5:1,提升止盈触发率) TAKE_PROFIT_PERCENT: 25.0, // 放宽固定止盈(从30%到25%,配合ATR止盈放大盈亏比) MIN_HOLD_TIME_SEC: 1800, // 最小持仓时间30分钟(强制波段持仓纪律) // 根治:关闭智能入场(回归纯限价,不追价/不市价兜底) 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, // 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, // 0.3% ENTRY_MAX_DRIFT_PCT_RANGING: 0.15, // 0.15% // 风控:默认关闭移动止损(避免“保本价”过早触发) USE_TRAILING_STOP: false, // 禁用移动止损,让利润奔跑 ATR_TAKE_PROFIT_MULTIPLIER: 1.5, // ATR止盈倍数(从4.5降至1.5,将盈亏比从3:1降至更现实、可达成的1.5:1,提升止盈触发率) TAKE_PROFIT_PERCENT: 25.0, // 放宽固定止盈(从30%到25%,配合ATR止盈放大盈亏比) MIN_HOLD_TIME_SEC: 1800, // 最小持仓时间30分钟(强制波段持仓纪律) }, }, 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: 1.5, // ATR止盈倍数(从4.5降至1.5,将盈亏比从3:1降至更现实、可达成的1.5:1,提升止盈触发率) TAKE_PROFIT_PERCENT: 25.0, // 放宽固定止盈(从30%到25%,配合ATR止盈放大盈亏比) MIN_HOLD_TIME_SEC: 1800, // 最小持仓时间30分钟(强制波段持仓纪律) }, }, 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: 1.5, // ATR止盈倍数(从4.5降至1.5,将盈亏比从3:1降至更现实、可达成的1.5:1,提升止盈触发率) TAKE_PROFIT_PERCENT: 25.0, // 放宽固定止盈(从30%到25%,配合ATR止盈放大盈亏比) MIN_HOLD_TIME_SEC: 1800, // 最小持仓时间30分钟(强制波段持仓纪律) }, }, conservative: { name: '保守配置', desc: '适合新手,风险较低,止损止盈较宽松,避免被正常波动触发', configs: { SCAN_INTERVAL: 900, MIN_CHANGE_PERCENT: 2.0, // 2% MIN_SIGNAL_STRENGTH: 5, TOP_N_SYMBOLS: 10, MAX_SCAN_SYMBOLS: 150, MIN_VOLATILITY: 0.02, // 保持小数形式(波动率) STOP_LOSS_PERCENT: 10.0, // 10%(相对于保证金,更宽松) TAKE_PROFIT_PERCENT: 25.0, // 25%(相对于保证金,从20%提升到25%,放大盈亏比) MIN_STOP_LOSS_PRICE_PCT: 2.0, // 2%最小价格变动保护 MIN_TAKE_PROFIT_PRICE_PCT: 3.0, // 3%最小价格变动保护 USE_TRAILING_STOP: false, // 禁用移动止损,让利润奔跑 ATR_TAKE_PROFIT_MULTIPLIER: 1.5, // ATR止盈倍数(从4.5降至1.5,将盈亏比从3:1降至更现实、可达成的1.5:1,提升止盈触发率) MIN_HOLD_TIME_SEC: 1800 // 最小持仓时间30分钟(强制波段持仓纪律) } }, balanced: { name: '平衡配置', desc: '推荐使用,平衡频率和质量,止损止盈适中(盈亏比2.5:1)', configs: { SCAN_INTERVAL: 600, MIN_CHANGE_PERCENT: 1.5, // 1.5% MIN_SIGNAL_STRENGTH: 4, TOP_N_SYMBOLS: 12, MAX_SCAN_SYMBOLS: 250, MIN_VOLATILITY: 0.018, // 保持小数形式(波动率) STOP_LOSS_PERCENT: 8.0, // 8%(相对于保证金,默认值) TAKE_PROFIT_PERCENT: 25.0, // 25%(相对于保证金,从20%提升到25%,放大盈亏比) MIN_STOP_LOSS_PRICE_PCT: 2.0, // 2%最小价格变动保护 MIN_TAKE_PROFIT_PRICE_PCT: 3.0, // 3%最小价格变动保护 USE_TRAILING_STOP: false, // 禁用移动止损,让利润奔跑 ATR_TAKE_PROFIT_MULTIPLIER: 1.5, // ATR止盈倍数(从4.5降至1.5,将盈亏比从3:1降至更现实、可达成的1.5:1,提升止盈触发率) MIN_HOLD_TIME_SEC: 1800 // 最小持仓时间30分钟(强制波段持仓纪律) } }, aggressive: { name: '激进高频', desc: '晚间波动大时使用,交易频率高,止损较紧但止盈合理(盈亏比3:1)', configs: { SCAN_INTERVAL: 300, MIN_CHANGE_PERCENT: 1.0, // 1% MIN_SIGNAL_STRENGTH: 3, TOP_N_SYMBOLS: 18, MAX_SCAN_SYMBOLS: 350, MIN_VOLATILITY: 0.015, // 保持小数形式(波动率) STOP_LOSS_PERCENT: 5.0, // 5%(相对于保证金,较紧) TAKE_PROFIT_PERCENT: 25.0, // 25%(相对于保证金,从15%大幅提升到25%,放大盈亏比到5:1) MIN_STOP_LOSS_PRICE_PCT: 1.5, // 1.5%最小价格变动保护 MIN_TAKE_PROFIT_PRICE_PCT: 2.0, // 2%最小价格变动保护 USE_TRAILING_STOP: false, // 禁用移动止损,让利润奔跑 ATR_TAKE_PROFIT_MULTIPLIER: 1.5, // ATR止盈倍数(从4.5降至1.5,将盈亏比从3:1降至更现实、可达成的1.5:1,提升止盈触发率) MIN_HOLD_TIME_SEC: 1800 // 最小持仓时间30分钟(强制波段持仓纪律) } } } 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 loadAccountTradingStatus = async () => { try { const res = await api.getAccountTradingStatus(accountId) setAccountTradingStatus(res) setAccountTradingErr('') } catch (error) { setAccountTradingStatus(null) setAccountTradingErr(error?.message || '获取交易进程状态失败') } } const loadCurrentAccountMeta = async (targetAccountId = null) => { try { // 使用传入的 accountId 或当前的 accountId(确保使用最新的值) const targetId = targetAccountId !== null ? targetAccountId : accountId // 统一使用getAccounts获取所有账号(管理员会返回所有账号,普通用户返回自己的) // 这样可以确保获取到完整的status等信息 const list = await api.getAccounts() const accounts = Array.isArray(list) ? list : [] const meta = accounts.find((a) => parseInt(String(a?.id || '0'), 10) === parseInt(String(targetId || '0'), 10)) // 确保status字段正确设置(如果meta存在但status为空,默认为'active') if (meta) { meta.status = meta.status || 'active' } setCurrentAccountMeta(meta || null) // 同步 testnet 开关到表单(仅在未输入时同步,避免打断正在输入) if (meta && !credForm.api_key && !credForm.api_secret) { setCredForm((prev) => ({ ...prev, use_testnet: !!meta.use_testnet })) } return meta } catch (e) { setCurrentAccountMeta(null) return null } } const handleAccountTradingEnsure = async () => { setSystemBusy(true) setMessage('') try { const res = await api.ensureAccountTradingProgram(accountId) setMessage(`已生成/刷新 supervisor 配置:${res.program || ''}`) await loadAccountTradingStatus() } catch (error) { setMessage('生成 supervisor 配置失败: ' + (error.message || '未知错误')) } finally { setSystemBusy(false) } } const handleAccountTradingStart = async () => { const hasKey = !!currentAccountMeta?.has_api_key const hasSecret = !!currentAccountMeta?.has_api_secret if (!hasKey || !hasSecret) { setMessage('请先在“账号密钥(当前账号)”中配置 BINANCE_API_KEY 与 BINANCE_API_SECRET,然后再启动交易进程') return } setSystemBusy(true) setMessage('') try { const res = await api.startAccountTrading(accountId) setMessage(res.message || '交易进程已启动') await loadAccountTradingStatus() } catch (error) { setMessage('启动交易进程失败: ' + (error.message || '未知错误')) } finally { setSystemBusy(false) } } const handleAccountTradingStop = async () => { setSystemBusy(true) setMessage('') try { const res = await api.stopAccountTrading(accountId) setMessage(res.message || '交易进程已停止') await loadAccountTradingStatus() } catch (error) { setMessage('停止交易进程失败: ' + (error.message || '未知错误')) } finally { setSystemBusy(false) } } const handleAccountTradingRestart = async () => { const hasKey = !!currentAccountMeta?.has_api_key const hasSecret = !!currentAccountMeta?.has_api_secret if (!hasKey || !hasSecret) { setMessage('请先在“账号密钥(当前账号)”中配置 BINANCE_API_KEY 与 BINANCE_API_SECRET,然后再重启交易进程') return } setSystemBusy(true) setMessage('') try { const res = await api.restartAccountTrading(accountId) setMessage(res.message || '交易进程已重启') await loadAccountTradingStatus() } catch (error) { setMessage('重启交易进程失败: ' + (error.message || '未知错误')) } finally { setSystemBusy(false) } } 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) } } // 使用 ref 记录上一次的 accountId,避免初始化时刷新页面 const prevAccountIdRef = useRef(accountId) useEffect(() => { // 如果 accountId 变化了(不是初始化),刷新页面 // if (prevAccountIdRef.current !== null && prevAccountIdRef.current !== accountId) { // // accountId 变化时,刷新页面以确保所有状态都正确更新 // window.location.reload() // return // } // 初始化时,更新 ref 并加载数据 prevAccountIdRef.current = accountId loadConfigMeta() loadConfigs() checkFeasibility() loadAccountTradingStatus() loadCurrentAccountMeta(accountId) const timer = setInterval(() => { // 定时器中使用最新的 accountId loadAccountTradingStatus() loadCurrentAccountMeta(accountId) }, 3000) return () => clearInterval(timer) }, [accountId]) // 当 accountId 变化时,如果是切换账号则刷新页面 // 当accountId变化时,重新加载相关数据(避免重复调用,已在onChanged和定时器中处理) const loadAccountsAdmin = async () => { try { const list = await api.getAccounts() setAccountsAdmin(Array.isArray(list) ? list : []) } catch (e) { setAccountsAdmin([]) } } const notifyAccountsUpdated = () => { try { window.dispatchEvent(new Event('ats:accounts:updated')) } catch (e) { // ignore } } // 注意:accountId 变化时的刷新逻辑已在上面的 useEffect 中处理 const checkFeasibility = async () => { setCheckingFeasibility(true) try { const result = await api.checkConfigFeasibility() setFeasibilityCheck(result) } catch (error) { console.error('检查配置可行性失败:', error) // 静默失败,不显示错误 } finally { setCheckingFeasibility(false) } } const loadConfigs = async () => { try { const data = await api.getConfigs() setConfigs(data) } catch (error) { console.error('Failed to load configs:', error) setMessage('加载配置失败: ' + error.message) } finally { setLoading(false) } } const handleUpdate = async (key, value, type, category) => { setSaving(true) setMessage('') try { const response = await api.updateConfig(key, { value, type, category }) setMessage(response.message || '配置已更新') if (response.note) { setTimeout(() => { setMessage(response.note) }, 2000) } // 重新加载配置 await loadConfigs() // 如果更新的是与可行性相关的配置,重新检查可行性 if (['MIN_MARGIN_USDT', 'MIN_POSITION_PERCENT', 'MAX_POSITION_PERCENT', 'LEVERAGE'].includes(key)) { await checkFeasibility() } } catch (error) { const errorMsg = error.message || '更新失败' setMessage('更新失败: ' + errorMsg) console.error('Config update error:', error) } 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'))) { // 兼容两种: // - 常规 PERCENT/PCT:存储为 0~1(比例),展示为 0~100(%) // - PCT_LIKE_KEYS:存储可能是 0.006(=0.6%) 或 0.6(=0.6%),展示统一为“百分比数值” 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 || '', } }) // 排序:先分类再按 key 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, feasibility_check_summary: feasibilityCheck ? { feasible: !!feasibilityCheck.feasible, account_balance: feasibilityCheck.account_balance ?? null, base_leverage: feasibilityCheck.base_leverage ?? feasibilityCheck.leverage ?? null, max_leverage: feasibilityCheck.max_leverage ?? null, use_dynamic_leverage: feasibilityCheck.use_dynamic_leverage ?? null, min_margin_usdt: feasibilityCheck.current_config?.min_margin_usdt ?? 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 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)) { // 兼容旧值:0.006(=0.6%) 或 0.6(=0.6%) 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) { // 如果配置项不存在,尝试创建(用于新增的配置项) // 根据key判断类型和分类 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' } const detail = typeof getConfigDetail === 'function' ? getConfigDetail(key) : '' const desc = detail && typeof detail === 'string' && !detail.includes('暂无详细说明') ? detail : `预设方案配置项:${key}` return { key, value: (key.includes('PERCENT') || key.includes('PCT')) && !PCT_LIKE_KEYS.has(key) ? value / 100 : value, type, category, description: desc } } 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 currentPreset = detectCurrentPreset() if (loading) return
修改配置后,交易系统将在下次扫描时自动使用新配置
{String(accountTradingStatus.raw || '')}
{String(accountTradingStatus.stderr_tail || '')}
{String(accountTradingStatus.stdout_tail || '')}
{String(accountTradingStatus.stderr_tail_error || '')}
{String(accountTradingStatus?.supervisord_tail || '(暂无:可能未配置 supervisord logfile 或当前用户无权限读取。可在后端配置 SUPERVISOR_LOGFILE 指向 supervisord 主日志路径)')}
| ID | 名称 | 状态 | 测试网 | API KEY | SECRET | 操作 |
|---|---|---|---|---|---|---|
| #{a.id} | {a.name || '-'} | {a.status === 'active' ? '启用' : '禁用'} | {a.use_testnet ? '是' : '否'} | {a.api_key_masked || (a.has_api_key ? '已配置' : '未配置')} | {a.has_api_secret ? '已配置' : '未配置'} |
账户余额: {feasibilityCheck.account_balance?.toFixed(2) || 'N/A'} USDT | 基础杠杆: {feasibilityCheck.base_leverage || feasibilityCheck.leverage || 'N/A'}x {feasibilityCheck.use_dynamic_leverage && feasibilityCheck.max_leverage && ( <> | 最大杠杆: {feasibilityCheck.max_leverage}x> )} | 最小保证金: {feasibilityCheck.current_config?.min_margin_usdt?.toFixed(2) || 'N/A'} USDT
{!feasibilityCheck.feasible && (需要保证金占比: {feasibilityCheck.calculated_values?.required_position_percent?.toFixed(1)}% | 最大允许保证金占比: {feasibilityCheck.calculated_values?.max_allowed_position_percent?.toFixed(1)}%
{feasibilityCheck.calculated_values?.actual_min_margin !== undefined && (最小保证金占比可提供保证金: {feasibilityCheck.calculated_values.actual_min_margin.toFixed(2)} USDT (MIN_POSITION_PERCENT={feasibilityCheck.calculated_values.min_position_percent?.toFixed(1)}%) | 最小保证金要求: {feasibilityCheck.current_config?.min_margin_usdt?.toFixed(2)} USDT
)}| 杠杆倍数 | 需要保证金(USDT) | 需要保证金% | 实际最小保证金 | 状态 |
|---|---|---|---|---|
| {result.leverage}x | {result.required_position_value.toFixed(2)} USDT | {result.required_position_percent.toFixed(1)}% | {result.actual_min_margin.toFixed(2)} USDT | {result.feasible ? '✓ 可行' : '✗ 不可行'} |
{suggestion.description}
{suggestion.config_key && suggestion.suggested_value !== null && ( )}