auto_trade_sys/frontend/src/components/GlobalConfig.jsx
薇薇安 01c11d62f6 1
2026-02-13 07:35:23 +08:00

1539 lines
62 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 0100、24h 涨跌幅 25 表示 25%),不做 01 比例转换
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 (
<div className="config-item">
<div className="config-item-header">
<label>{displayLabel}</label>
</div>
<div className="config-input-wrapper">
{config.type === 'boolean' ? (
<select
value={String(config.value || false)}
onChange={(e) => {
const boolValue = e.target.value === 'true'
onUpdate(boolValue)
}}
disabled={disabled}
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
>
<option value="true"></option>
<option value="false"></option>
</select>
) : (
<input
type="text"
inputMode={config.type === 'number' ? 'decimal' : 'text'}
value={displayValue === '' ? '' : String(displayValue)}
onChange={(e) => {
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 */}
</div>
{config.description && (
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>{config.description}</div>
)}
</div>
)
}
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与之前盈利阶段一致避免被压到 24x
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-1265 避免追高)。' },
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避免被压到 24x 导致单笔盈利过少。' },
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-123 减少噪音止损)。' },
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 <div className="global-config">加载中...</div>
}
// 管理员全局配置页面:不依赖任何 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 (
<div className="global-config">
<div className="global-config-header">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h2>全局配置</h2>
<p>管理用户账号和全局策略配置</p>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button
type="button"
className="guide-link snapshot-btn"
onClick={() => openSnapshot(snapshotIncludeSecrets)}
disabled={snapshotBusy}
title="导出当前全量配置(用于分析)"
>
{snapshotBusy ? '生成中...' : '查看整体配置'}
</button>
<Link to="/config/guide" className="guide-link">📖 配置说明</Link>
</div>
</div>
</div>
{message && (
<div className={`message ${message.includes('失败') ? 'error' : 'success'}`}>
{message}
</div>
)}
{/* 系统控制 */}
{isAdmin && (
<section className="global-section system-section">
<div className="system-header">
<h3>系统控制</h3>
<div className="system-status-indicators" style={{ display: 'flex', gap: '15px', fontSize: '14px', alignItems: 'center', marginLeft: '20px' }}>
<div className="status-item" style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: backendStatus?.running ? '#4caf50' : '#f44336', display: 'inline-block' }}></span>
<span style={{ fontWeight: 500 }}>后端: {backendStatus?.running ? '运行中' : '停止'}</span>
</div>
{servicesSummary && (
<div className="status-item" style={{ display: 'flex', alignItems: 'center', gap: '6px', borderLeft: '1px solid #ddd', paddingLeft: '15px' }}>
<span style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: '#2196f3', display: 'inline-block' }}></span>
<span style={{ fontWeight: 500 }}>交易服务: </span>
<span style={{ fontWeight: 'bold' }}>{servicesSummary.total}</span>
<span style={{ color: '#666' }}></span>
<span style={{ color: '#4caf50', marginLeft: '8px', fontSize: '0.9em' }}> 运行 {servicesSummary.running}</span>
<span style={{ color: '#f44336', marginLeft: '8px', fontSize: '0.9em' }}> 停止 {servicesSummary.stopped}</span>
{servicesSummary.unknown > 0 && <span style={{ color: '#999', marginLeft: '8px', fontSize: '0.9em' }}> 未知 {servicesSummary.unknown}</span>}
</div>
)}
</div>
</div>
<div className="system-control-group">
<h4 className="control-group-title" style={{ margin: '10px 0 8px', fontSize: '14px', color: '#666' }}>后端服务管理</h4>
<div className="system-actions">
<button
type="button"
className="system-btn primary"
onClick={handleRestartBackend}
disabled={systemBusy}
title="通过 backend/restart.sh 重启后端uvicorn。重启期间接口会短暂不可用。"
>
重启后端服务
</button>
<button
type="button"
className="system-btn danger"
onClick={handleStopBackend}
disabled={systemBusy}
title="停止后端服务(需要手动去服务器启动)"
>
停止后端服务
</button>
</div>
</div>
<div className="system-control-group" style={{ marginTop: '15px' }}>
<h4 className="control-group-title" style={{ margin: '10px 0 8px', fontSize: '14px', color: '#666' }}>系统数据维护</h4>
<div className="system-actions">
<button
type="button"
className="system-btn warning"
onClick={handleClearCache}
disabled={systemBusy}
title="清理Redis配置缓存并从数据库回灌。这将影响后端服务和所有交易进程。切换API Key或修改配置后建议执行。"
>
清除全局缓存
</button>
<span style={{ fontSize: '12px', color: '#888', alignSelf: 'center', marginLeft: '10px' }}>
* 影响后端服务及所有交易账号修改配置后请点击
</span>
</div>
</div>
<div className="system-control-group" style={{ marginTop: '15px' }}>
<h4 className="control-group-title" style={{ margin: '10px 0 8px', fontSize: '14px', color: '#666' }}>交易服务管理</h4>
<div className="system-actions">
<button
type="button"
className="system-btn primary"
onClick={handleRestartAllTrading}
disabled={systemBusy}
title="批量重启所有账号交易进程auto_sys_acc*),用于代码升级后统一生效"
>
重启所有账号交易
</button>
<button
type="button"
className="system-btn danger"
onClick={handleStopAllTrading}
disabled={systemBusy}
title="批量停止所有账号交易进程auto_sys_acc*"
>
停止所有账号交易
</button>
</div>
</div>
<div className="system-hint">
建议流程先更新配置里的 Key 点击"清除缓存" 点击"重启所有账号交易"确保不再使用旧账号下单
</div>
</section>
)}
{/* 预设方案快速切换(仅管理员 + 全局策略账号) */}
{isAdmin && isGlobalStrategyAccount && (
<section className="global-section preset-section">
<div className="preset-header">
<h3>快速切换方案</h3>
<div className="current-preset-status">
<span className="status-label">当前方案</span>
<span className={`status-badge ${currentPreset ? 'preset' : 'custom'}`}>
{currentPreset && presets && presets[currentPreset] ? presets[currentPreset].name : '自定义'}
</span>
</div>
</div>
<div className="preset-guide">
<div className="preset-guide-title">使用说明</div>
<ul className="preset-guide-list">
<li>
<strong>日常使用</strong>4H RSI///
</li>
<li>
<strong>若几乎不出单</strong> <code>AUTO_TRADE_ALLOW_4H_NEUTRAL</code> 4H
</li>
<li>
<strong>要更少出单</strong>
</li>
</ul>
</div>
<div className="preset-groups">
{presetGroups.map((g) => (
<div key={g.key} className="preset-group">
<div className="preset-group-header">
<div className="preset-group-title">{g.title}</div>
<div className="preset-group-desc">{g.desc}</div>
</div>
<div className="preset-buttons">
{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 (
<button
key={k}
className={`preset-btn ${currentPreset === k ? 'active' : ''}`}
onClick={() => {
if (typeof applyPreset === 'function') {
applyPreset(k)
}
}}
disabled={saving}
title={preset.desc}
>
<div className="preset-name">
{preset.name}
{meta.tag ? (
<span className={`preset-tag preset-tag--${meta.group}`}>{meta.tag}</span>
) : null}
{currentPreset === k ? <span className="active-indicator"></span> : null}
</div>
<div className="preset-desc">{preset.desc}</div>
</button>
)
})
.filter(Boolean)}
</div>
</div>
))}
</div>
</section>
)}
{/* 全局策略配置项编辑(仅管理员) */}
{isAdmin && (
<section className="global-section config-section">
<div className="section-header">
<h3>全局策略配置</h3>
<p style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>
修改全局策略配置所有普通用户账号将使用这些配置风险旋钮除外
</p>
</div>
{/* 搜索和筛选栏 */}
<div className="config-toolbar" style={{ marginBottom: '20px', display: 'flex', flexDirection: 'column', gap: '15px' }}>
{/* 搜索框 */}
<div className="search-bar" style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
<input
type="text"
placeholder="🔍 搜索配置项 Key、描述或中文名..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{
width: '100%',
padding: '10px 12px',
borderRadius: '6px',
border: '1px solid #ddd',
fontSize: '14px'
}}
/>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
style={{
position: 'absolute',
right: '10px',
border: 'none',
background: 'none',
cursor: 'pointer',
color: '#999',
fontSize: '16px'
}}
>
</button>
)}
</div>
{/* Tabs */}
<div className="config-tabs" style={{ display: 'flex', gap: '10px', overflowX: 'auto', paddingBottom: '5px' }}>
{[
{ key: 'all', label: '全部' },
{ key: 'risk', label: '风险控制' },
{ key: 'strategy', label: '策略参数' },
{ key: 'scan', label: '市场扫描' },
{ key: 'position', label: '仓位控制' },
].map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`tab-btn ${activeTab === tab.key ? 'active' : ''}`}
style={{
padding: '8px 16px',
borderRadius: '20px',
border: activeTab === tab.key ? '1px solid #007bff' : '1px solid #eee',
background: activeTab === tab.key ? '#e7f1ff' : '#f9f9f9',
color: activeTab === tab.key ? '#007bff' : '#666',
cursor: 'pointer',
fontWeight: activeTab === tab.key ? '600' : 'normal',
whiteSpace: 'nowrap',
transition: 'all 0.2s'
}}
>
{tab.label}
</button>
))}
</div>
</div>
{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 <div style={{ textAlign: 'center', color: '#999', padding: '30px', background: '#f5f5f5', borderRadius: '8px' }}>未找到匹配的配置项</div>
}
// 分组渲染
// 如果是 '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 (
<div key={category} style={{ marginBottom: '24px' }}>
<h4 style={{ marginBottom: '12px', fontSize: '16px', fontWeight: '600', borderLeft: '4px solid #007bff', paddingLeft: '10px', color: '#333' }}>
{categoryLabel}
</h4>
<div className="config-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '16px' }}>
{groupConfigs.map(([key, config]) => (
<ConfigItem
key={key}
label={key}
config={config}
onUpdate={(val) => handleConfigUpdate(key, val, config)}
disabled={saving}
/>
))}
</div>
</div>
)
})
})()
) : (
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
{loading ? '加载配置中...' : '暂无配置项'}
</div>
)}
</section>
)}
{/* 配置快照 Modal */}
{showSnapshot && (
<div className="snapshot-modal-overlay" onClick={() => setShowSnapshot(false)} role="presentation">
<div className="snapshot-modal" onClick={(e) => e.stopPropagation()}>
<div className="snapshot-modal-header">
<div>
<h3>当前整体配置快照</h3>
<div className="snapshot-hint">
默认脱敏 BINANCE_API_KEY/SECRET你可以选择明文后重新生成再复制/下载
</div>
</div>
<button type="button" className="snapshot-close" onClick={() => setShowSnapshot(false)}>
关闭
</button>
</div>
<div className="snapshot-toolbar">
<label className="snapshot-checkbox">
<input
type="checkbox"
checked={snapshotIncludeSecrets}
onChange={async (e) => {
const checked = e.target.checked
setSnapshotIncludeSecrets(checked)
await openSnapshot(checked)
}}
/>
显示敏感信息明文
</label>
<div className="snapshot-actions">
<button type="button" className="system-btn" onClick={copySnapshot}>
复制
</button>
<button type="button" className="system-btn primary" onClick={downloadSnapshot}>
下载 JSON
</button>
</div>
</div>
<pre className="snapshot-pre">{snapshotText}</pre>
</div>
</div>
)}
</div>
)
}
export default GlobalConfig