import React, { useState, useEffect } from 'react' import { Link } from 'react-router-dom' import { useSelector } from 'react-redux' import { api } from '../services/api' import { 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', 'MIN_RR_FOR_TP1', 'POSITION_SCALE_FACTOR', ]) // 配置项中文标签(便于识别) 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: '冷却时间(秒)', RSI_EXTREME_REVERSE_ENABLED: 'RSI 极端反向(超买反空/超卖反多)', RSI_EXTREME_REVERSE_ONLY_NEUTRAL_4H: 'RSI 反向仅允许 4H 中性', MIN_RR_FOR_TP1: '第一目标最小盈亏比(相对止损)', AUTO_TRADE_ALLOW_4H_NEUTRAL: '允许 4H 中性时自动交易', POSITION_SCALE_FACTOR: '仓位放大系数', } 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 已移除,直接显示小数 ]) 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 [localValue, setLocalValue] = useState(getInitialDisplayValue(config.value)) const [isEditing, setIsEditing] = useState(false) useEffect(() => { 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} onKeyDown={(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 isAdmin = useSelector(selectIsAdmin) const [loading, setLoading] = useState(true) const [message, setMessage] = useState('') const [busy, setBusy] = useState(false) // 系统控制相关 const [systemStatus, setSystemStatus] = useState(null) const [backendStatus, setBackendStatus] = useState(null) const [servicesSummary, setServicesSummary] = useState(null) const [systemBusy, setSystemBusy] = useState(false) // 预设方案相关 const [configs, setConfigs] = useState({}) const [saving, setSaving] = useState(false) // 配置快照相关 const [showSnapshot, setShowSnapshot] = useState(false) const [snapshotText, setSnapshotText] = useState('') const [snapshotIncludeSecrets, setSnapshotIncludeSecrets] = useState(false) const [snapshotBusy, setSnapshotBusy] = useState(false) // 新增:搜索和 Tab 状态 const [searchTerm, setSearchTerm] = useState('') const [activeTab, setActiveTab] = useState('all') 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: 0.12, MAX_TOTAL_POSITION_PERCENT: 0.45, MAX_LEVERAGE: 20, 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, MAX_POSITION_PERCENT: 0.12, MAX_LEVERAGE: 20, 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_POSITION_PERCENT: 0.10, MAX_LEVERAGE: 15, 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: '与 2026-02-12 策略一致:4H 中性不自动交易、做多 RSI≤65、第一目标止盈 20%、止损 3×ATR、盈亏比 3:1、同品种连亏冷却。', configs: { // 风险与止盈止损(与当前全局默认一致) ATR_STOP_LOSS_MULTIPLIER: 3.0, // 3×ATR 减少噪音止损 STOP_LOSS_PERCENT: 0.12, RISK_REWARD_RATIO: 3.0, // 盈亏比 3:1 TAKE_PROFIT_1_PERCENT: 0.2, // 第一目标止盈 20% TAKE_PROFIT_PERCENT: 0.30, // 第二目标 30% MIN_RR_FOR_TP1: 1.5, // 第一目标至少 1.5 倍止损距离 MIN_HOLD_TIME_SEC: 1800, USE_FIXED_RISK_SIZING: true, FIXED_RISK_PERCENT: 0.02, // 每笔风险 2% USE_DYNAMIC_ATR_MULTIPLIER: false, USE_TRAILING_STOP: true, TRAILING_STOP_ACTIVATION: 0.20, TRAILING_STOP_PROTECT: 0.10, MAX_POSITION_PERCENT: 0.12, // 单笔最大 12% 可用资金,加大单笔盈利空间 MAX_TOTAL_POSITION_PERCENT: 0.45, MAX_DAILY_ENTRIES: 8, MAX_OPEN_POSITIONS: 4, LEVERAGE: 10, MAX_LEVERAGE: 20, // 动态杠杆上限 20,配合单笔仓位提高收益 MIN_LEVERAGE: 8, // 动态杠杆下限 8,与之前盈利阶段一致,避免被压到 2–4x MAX_LEVERAGE_SMALL_CAP: 8, // 高波动币也允许 8x,与之前一致 USE_DYNAMIC_LEVERAGE: true, MIN_VOLUME_24H: 10000000, MIN_VOLATILITY: 0.02, TOP_N_SYMBOLS: 20, MAX_SCAN_SYMBOLS: 500, MIN_SIGNAL_STRENGTH: 8, EXCLUDE_MAJOR_COINS: true, SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT: 15, SCAN_INTERVAL: 1800, PRIMARY_INTERVAL: '1h', ENTRY_INTERVAL: '15m', CONFIRM_INTERVAL: '4h', AUTO_TRADE_ONLY_TRENDING: true, AUTO_TRADE_ALLOW_4H_NEUTRAL: false, // 4H 中性不自动交易,提升质量 MAX_RSI_FOR_LONG: 65, // 做多不追高 MIN_RSI_FOR_SHORT: 30, MAX_CHANGE_PERCENT_FOR_LONG: 25, MAX_CHANGE_PERCENT_FOR_SHORT: 10, SYMBOL_LOSS_COOLDOWN_ENABLED: true, SYMBOL_MAX_CONSECUTIVE_LOSSES: 2, SYMBOL_LOSS_COOLDOWN_SEC: 3600, BETA_FILTER_ENABLED: true, BETA_FILTER_THRESHOLD: -0.005, ENTRY_SHORT_TREND_FILTER_ENABLED: true, MAX_TREND_MOVE_BEFORE_ENTRY: 0.05, }, }, more_opportunities: { name: '增加机会(放宽过滤)', desc: '放宽过滤条件以增加下单机会:允许 4H 中性、大盘共振阈值放宽到 -1%、做多 RSI≤70、做空 RSI≥25。适合想要更多交易机会时使用。', configs: { AUTO_TRADE_ALLOW_4H_NEUTRAL: true, // 允许 4H 中性,增加机会 BETA_FILTER_THRESHOLD: -0.01, // 大盘共振阈值放宽到 -1%(从 -0.5% 放宽) MAX_RSI_FOR_LONG: 70, // 做多 RSI 上限放宽到 70(从 65) MIN_RSI_FOR_SHORT: 25, // 做空 RSI 下限放宽到 25(从 30),允许更多做空机会 ATR_STOP_LOSS_MULTIPLIER: 3.0, RISK_REWARD_RATIO: 3.0, TAKE_PROFIT_1_PERCENT: 0.2, TAKE_PROFIT_PERCENT: 0.30, MIN_RR_FOR_TP1: 1.5, SYMBOL_LOSS_COOLDOWN_ENABLED: true, SYMBOL_MAX_CONSECUTIVE_LOSSES: 2, SYMBOL_LOSS_COOLDOWN_SEC: 3600, BETA_FILTER_ENABLED: true, // 保持开启但放宽阈值 MIN_SIGNAL_STRENGTH: 8, MAX_POSITION_PERCENT: 0.12, MAX_LEVERAGE: 20, }, }, profit_scale: { name: '盈利放大(适度激进)', desc: '在推荐策略基础上:仓位放大 1.25 倍、单笔上限 12%、杠杆上限 20、每笔风险 2.5%、最多 5 仓。盈利阶段可一键放大收益,仍受单笔上限约束。', configs: { POSITION_SCALE_FACTOR: 1.25, MAX_POSITION_PERCENT: 0.12, MAX_LEVERAGE: 20, MAX_TOTAL_POSITION_PERCENT: 0.45, FIXED_RISK_PERCENT: 0.025, MAX_OPEN_POSITIONS: 5, MAX_DAILY_ENTRIES: 10, ATR_STOP_LOSS_MULTIPLIER: 3.0, RISK_REWARD_RATIO: 3.0, TAKE_PROFIT_1_PERCENT: 0.2, TAKE_PROFIT_PERCENT: 0.30, MIN_RR_FOR_TP1: 1.5, AUTO_TRADE_ALLOW_4H_NEUTRAL: false, MAX_RSI_FOR_LONG: 65, MIN_RSI_FOR_SHORT: 30, SYMBOL_LOSS_COOLDOWN_ENABLED: true, SYMBOL_MAX_CONSECUTIVE_LOSSES: 2, SYMBOL_LOSS_COOLDOWN_SEC: 3600, BETA_FILTER_ENABLED: true, MIN_SIGNAL_STRENGTH: 8, }, }, classic_profit: { name: '之前盈利风格', desc: '对齐 2 月初有盈利时的下单方式:固定 8 倍杠杆、单笔 12%、每笔风险 2.5%,不做动态降杠杆,单笔保证金与盈利空间更接近当时。', configs: { LEVERAGE: 8, USE_DYNAMIC_LEVERAGE: false, // 固定 8x,与之前一致 MIN_LEVERAGE: 8, MAX_LEVERAGE: 20, MAX_POSITION_PERCENT: 0.12, MAX_TOTAL_POSITION_PERCENT: 0.45, FIXED_RISK_PERCENT: 0.025, // 略高于 2%,单笔保证金更接近之前 MAX_LEVERAGE_SMALL_CAP: 8, ATR_STOP_LOSS_MULTIPLIER: 3.0, RISK_REWARD_RATIO: 3.0, TAKE_PROFIT_1_PERCENT: 0.2, TAKE_PROFIT_PERCENT: 0.30, MIN_RR_FOR_TP1: 1.5, AUTO_TRADE_ALLOW_4H_NEUTRAL: false, MAX_RSI_FOR_LONG: 65, MIN_RSI_FOR_SHORT: 30, SYMBOL_LOSS_COOLDOWN_ENABLED: true, SYMBOL_MAX_CONSECUTIVE_LOSSES: 2, SYMBOL_LOSS_COOLDOWN_SEC: 3600, BETA_FILTER_ENABLED: true, MIN_SIGNAL_STRENGTH: 8, }, }, classic_profit_amplify: { name: '之前盈利·放大', desc: '在「之前盈利风格」基础上把杠杆提到 10 倍:同保证金下名义更大,盈利与亏损都会按比例放大。适合想进一步放大收益时使用。', configs: { LEVERAGE: 10, USE_DYNAMIC_LEVERAGE: false, // 固定 10x MIN_LEVERAGE: 10, MAX_LEVERAGE: 20, MAX_POSITION_PERCENT: 0.12, MAX_TOTAL_POSITION_PERCENT: 0.45, FIXED_RISK_PERCENT: 0.025, MAX_LEVERAGE_SMALL_CAP: 10, ATR_STOP_LOSS_MULTIPLIER: 3.0, RISK_REWARD_RATIO: 3.0, TAKE_PROFIT_1_PERCENT: 0.2, TAKE_PROFIT_PERCENT: 0.30, MIN_RR_FOR_TP1: 1.5, AUTO_TRADE_ALLOW_4H_NEUTRAL: false, MAX_RSI_FOR_LONG: 65, MIN_RSI_FOR_SHORT: 30, SYMBOL_LOSS_COOLDOWN_ENABLED: true, SYMBOL_MAX_CONSECUTIVE_LOSSES: 2, SYMBOL_LOSS_COOLDOWN_SEC: 3600, BETA_FILTER_ENABLED: true, MIN_SIGNAL_STRENGTH: 8, }, }, 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倍 } }, script_v1: { name: '高收益趋势 (脚本配置)', desc: '原 update_global_config.py 脚本中的配置。高止盈(60%) + 紧止损(10%) + 移动止损保护。适合捕捉大单边行情。', configs: { TAKE_PROFIT_PERCENT: 60.0, TAKE_PROFIT_1_PERCENT: 30.0, STOP_LOSS_PERCENT: 10.0, TRAILING_STOP_ACTIVATION: 30.0, TRAILING_STOP_PROTECT: 5.0, LEVERAGE: 4, RISK_REWARD_RATIO: 3.0, ATR_TAKE_PROFIT_MULTIPLIER: 4.5, ATR_STOP_LOSS_MULTIPLIER: 1.5, USE_FIXED_RISK_SIZING: true, FIXED_RISK_PERCENT: 1.0, // 1% MAX_LEVERAGE_SMALL_CAP: 4, } } } // 已知全局配置项默认值(兜底:后端未返回时前端仍能显示,避免看不到新配置项) const KNOWN_GLOBAL_CONFIG_DEFAULTS = { MAX_RSI_FOR_LONG: { value: 65, type: 'number', category: 'strategy', description: '做多时 RSI 超过此值则不开多(2026-02-12:65 避免追高)。' }, 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.2, type: 'number', category: 'strategy', description: '分步止盈第一目标(保证金百分比,如 0.2=20%)。2026-02-12 提高以改善盈亏比。' }, 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小时。' }, MIN_RR_FOR_TP1: { value: 1.5, type: 'number', category: 'strategy', description: '第一目标止盈相对止损的最小盈亏比(如 1.5 表示 TP1 至少为止损距离的 1.5 倍)。2026-02-12 新增。' }, POSITION_SCALE_FACTOR: { value: 1.0, type: 'number', category: 'risk', description: '仓位放大系数:1.0=正常,1.2=+20% 仓位,1.5=+50%,上限 2.0。盈利时适度调高可扩大收益,仍受单笔上限约束。' }, AUTO_TRADE_ALLOW_4H_NEUTRAL: { value: false, type: 'boolean', category: 'strategy', description: '是否允许 4H 趋势为中性时自动交易。关闭可减少震荡扫损、提升质量(建议关闭)。' }, 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%)。' }, RSI_EXTREME_REVERSE_ENABLED: { value: false, type: 'boolean', category: 'strategy', description: '开启后:原信号做多但 RSI 超买(≥做多上限)时改为做空;原信号做空但 RSI 超卖(≤做空下限)时改为做多。属均值回归思路,可填补超买超卖时不下单的空置;默认关闭。' }, RSI_EXTREME_REVERSE_ONLY_NEUTRAL_4H: { value: true, type: 'boolean', category: 'strategy', description: '建议开启:仅在 4H 趋势为中性时允许 RSI 反向单,避免在强趋势里逆势抄底/摸顶,降低风险。关闭则反向可与 4H 同向(仍受“禁止逆4H趋势”约束)。' }, TAKE_PROFIT_PERCENT: { value: 0.25, type: 'number', category: 'risk', description: '止盈百分比(保证金比例,如 25.0=25%)。' }, STOP_LOSS_PERCENT: { value: 0.08, type: 'number', category: 'risk', description: '止损百分比(保证金比例,如 8.0=8%)。' }, TRAILING_STOP_ACTIVATION: { value: 0.20, type: 'number', category: 'risk', description: '移动止损激活阈值(盈利达到此比例后激活,如 20.0=20%)。' }, TRAILING_STOP_PROTECT: { value: 0.10, type: 'number', category: 'risk', description: '移动止损保护阈值(回撤至此比例时触发止损,如 10.0=10%)。' }, LEVERAGE: { value: 10, type: 'number', category: 'risk', description: '基础杠杆倍数。' }, MAX_POSITION_PERCENT: { value: 0.12, type: 'number', category: 'risk', description: '单笔最大保证金占可用资金比例(如 0.12=12%),加大可提高单笔盈利。' }, MAX_LEVERAGE: { value: 20, type: 'number', category: 'risk', description: '动态杠杆上限(如 20 表示最高 20 倍),配合单笔仓位提高收益。' }, MIN_LEVERAGE: { value: 8, type: 'number', category: 'risk', description: '动态杠杆下限(如 8 表示不低于 8 倍)。之前盈利阶段多为 8x,避免被压到 2–4x 导致单笔盈利过少。' }, MAX_LEVERAGE_SMALL_CAP: { value: 8, type: 'number', category: 'risk', description: '高波动/小众币最大允许杠杆(与之前盈利阶段一致)。' }, RISK_REWARD_RATIO: { value: 3, type: 'number', category: 'risk', description: '盈亏比目标(用于计算动态止盈止损,建议 3:1)。' }, ATR_TAKE_PROFIT_MULTIPLIER: { value: 1.5, type: 'number', category: 'risk', description: 'ATR 止盈倍数。' }, ATR_STOP_LOSS_MULTIPLIER: { value: 3, type: 'number', category: 'risk', description: 'ATR 止损倍数(2026-02-12:3 减少噪音止损)。' }, USE_FIXED_RISK_SIZING: { value: false, type: 'boolean', category: 'risk', description: '是否使用固定风险仓位计算(基于止损距离和账户余额)。' }, FIXED_RISK_PERCENT: { value: 0.01, type: 'number', category: 'risk', description: '每笔交易固定风险百分比(如 0.01=1%)。' }, } 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) try { const servicesRes = await api.get('/system/trading/services') setServicesSummary(servicesRes.data.summary) } catch (e) { // Services summary failed } } catch (error) { // 静默失败 } } const loadBackendStatus = async () => { try { const res = await api.getBackendStatus() setBackendStatus(res) } catch (error) { // 静默失败 } } useEffect(() => { const init = async () => { if (isAdmin) { // 加载全局配置(独立于账户) await Promise.allSettled([ loadConfigs(), loadSystemStatus(), loadBackendStatus() ]) } setLoading(false) } init() // 只有管理员才轮询系统状态 if (isAdmin) { 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 handleStopBackend = async () => { if (!window.confirm('警告:确定要停止后端服务吗?\n\n停止后 Web 界面将立即无法访问!\n必须手动登录服务器执行 ./start.sh 才能恢复。')) return setSystemBusy(true) setMessage('') try { const res = await api.stopBackend() setMessage(res.message || '后端服务已停止') alert('后端服务已停止,页面将不再响应。请手动去服务器启动。') } 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 handleStopAllTrading = async () => { if (!window.confirm('确定要停止【所有账号】的交易进程吗?所有账号将停止自动交易!')) return setSystemBusy(true) setMessage('') try { const res = await api.stopAllTradingSystems({ prefix: 'auto_sys_acc', include_default: true }) setMessage(`已发起批量停止:共 ${res.count} 个,成功 ${res.ok},失败 ${res.failed}`) await loadSystemStatus() } catch (e) { setMessage('批量停止失败: ' + (e?.message || '未知错误')) } finally { setSystemBusy(false) } } const applyPreset = async (presetKey) => { const 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 handleConfigUpdate = async (key, value, config) => { // 检查是否有实质变化 if (config.value === value) return // 格式化显示值用于确认弹窗 let displayOld = config.value let displayNew = value // 如果是百分比相关的,尝试转回百分数显示,更直观 const isPercent = key.includes('PERCENT') || key.includes('PCT') if (isPercent && typeof value === 'number' && Math.abs(value) <= 1) { // 简单判断:如果原值是小数且 key 含 PERCENT,可能是小数存储。 // 这里为了保险,直接显示原始值和新值,管理员自己判断。 // 或者简单转一下 displayOld = `${config.value} (${(config.value * 100).toFixed(2)}%)` displayNew = `${value} (${(value * 100).toFixed(2)}%)` } const confirmMsg = `确定修改配置项【${key}】吗?\n\n原值: ${displayOld}\n新值: ${displayNew}\n\n修改将立即生效。` if (!window.confirm(confirmMsg)) { // 如果用户取消,理论上 UI 应该回滚。 // 但 ConfigItem 内部状态已经变了(onBlur 触发)。 // 触发一次重载配置可以强制 UI 回滚 await loadConfigs() return } try { setSaving(true) setMessage('') if (!isAdmin) { setMessage('只有管理员可以修改全局配置') return } await api.updateGlobalConfigsBatch([{ key, value, type: config.type, category: config.category, description: config.description }]) setMessage(`已更新 ${key}`) await loadConfigs() } catch (error) { setMessage('更新配置失败: ' + error.message) } finally { setSaving(false) } } // 配置快照函数 const isSecretKey = (key) => { 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 || '未知错误')) } } 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: '当前推荐' }, more_opportunities: { group: 'altcoin', tag: '增加机会' }, profit_scale: { group: 'altcoin', tag: '盈利放大' }, classic_profit: { group: 'altcoin', tag: '之前盈利' }, classic_profit_amplify: { group: 'altcoin', tag: '杠杆放大' }, swing: { group: 'backup', tag: '纯限价' }, strict: { group: 'backup', tag: '精选低频' }, conservative: { group: 'backup', tag: '保守' }, } // 快速方案:当前推荐(含增加机会/盈利放大/之前盈利)+ 备选 const presetGroups = [ { key: 'altcoin', title: '当前推荐 · 山寨币策略', desc: '与当前策略一致。想增加下单机会可点「增加机会」;盈利阶段可点「盈利放大」;想恢复 2 月初有盈利时的下单风格可点「之前盈利」;在之前盈利基础上再放大可点「杠杆放大」(10x)。', presetKeys: ['altcoin', 'more_opportunities', 'profit_scale', 'classic_profit', 'classic_profit_amplify'], }, { key: 'backup', title: '备选方案(纯限价 / 保守)', desc: '需要更少出单或纯限价时可选:波段回归、精选低频、保守传统。', presetKeys: ['swing', 'strict', 'conservative'], }, ] return (

全局配置

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

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

系统控制

后端: {backendStatus?.running ? '运行中' : '停止'}
{servicesSummary && (
交易服务: {servicesSummary.total} ● 运行 {servicesSummary.running} ● 停止 {servicesSummary.stopped} {servicesSummary.unknown > 0 && ● 未知 {servicesSummary.unknown}}
)}

后端服务管理

系统数据维护

* 影响后端服务及所有交易账号,修改配置后请点击。

交易服务管理

建议流程:先更新配置里的 Key → 点击"清除缓存" → 点击"重启所有账号交易",确保不再使用旧账号下单。
)} {/* 预设方案快速切换(仅管理员 + 全局策略账号) */} {isAdmin && isGlobalStrategyAccount && (

快速切换方案

当前方案: {currentPreset && presets && presets[currentPreset] ? presets[currentPreset].name : '自定义'}
使用说明
  • 日常使用:选「山寨币策略(当前推荐)」即可,已与当前策略(4H 中性关闭、RSI/止盈/止损/盈亏比)对齐。
  • 若几乎不出单:可临时把 AUTO_TRADE_ALLOW_4H_NEUTRAL 打开(会增加 4H 震荡单,信号质量下降)。
  • 要更少出单:选备选里的「波段回归」或「精选低频」。
{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 && (

全局策略配置

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

{/* 搜索和筛选栏 */}
{/* 搜索框 */}
setSearchTerm(e.target.value)} style={{ width: '100%', padding: '10px 12px', borderRadius: '6px', border: '1px solid #ddd', fontSize: '14px' }} /> {searchTerm && ( )}
{/* Tabs */}
{[ { key: 'all', label: '全部' }, { key: 'risk', label: '风险控制' }, { key: 'strategy', label: '策略参数' }, { key: 'scan', label: '市场扫描' }, { key: 'position', label: '仓位控制' }, ].map(tab => ( ))}
{Object.keys(configs).length > 0 ? ( (() => { const configCategories = { 'risk': '风险控制', 'strategy': '策略参数', 'scan': '市场扫描', 'position': '仓位控制', } // 过滤逻辑 const filteredConfigs = Object.entries(configs).filter(([key, config]) => { // 1. 基础过滤(排除非对象、风险旋钮、API Key) if (!config || typeof config !== 'object') 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 if (key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET' || key === 'USE_TESTNET') return false // 2. Tab 过滤 if (activeTab !== 'all' && config.category !== activeTab) return false // 3. 搜索过滤 if (searchTerm) { const lowerTerm = searchTerm.toLowerCase() const matchKey = key.toLowerCase().includes(lowerTerm) const matchDesc = (config.description || '').toLowerCase().includes(lowerTerm) const matchLabel = (KEY_LABELS[key] || '').toLowerCase().includes(lowerTerm) if (!matchKey && !matchDesc && !matchLabel) return false } return true }) if (filteredConfigs.length === 0) { return
未找到匹配的配置项
} // 分组渲染 // 如果是 'all' Tab,按 Category 分组 // 如果是特定 Tab,直接渲染(或者也分组,只有一个组) const groupsToRender = activeTab === 'all' ? Object.keys(configCategories) : [activeTab] // 将 filteredConfigs 转为 Map 或方便查找的结构,或者每次 filter return groupsToRender.map(category => { const categoryLabel = configCategories[category] || category const groupConfigs = filteredConfigs.filter(([_, cfg]) => cfg.category === category) if (groupConfigs.length === 0) return null return (

{categoryLabel}

{groupConfigs.map(([key, config]) => ( handleConfigUpdate(key, val, config)} disabled={saving} /> ))}
) }) })() ) : (
{loading ? '加载配置中...' : '暂无配置项'}
)}
)} {/* 配置快照 Modal */} {showSnapshot && (
setShowSnapshot(false)} role="presentation">
e.stopPropagation()}>

当前整体配置快照

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