import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { api } from '../services/api'
import './ConfigPanel.css'
const ConfigPanel = () => {
const [configs, setConfigs] = useState({})
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState('')
// 预设方案配置
const presets = {
conservative: {
name: '保守配置',
desc: '适合新手,风险较低,交易频率适中',
configs: {
SCAN_INTERVAL: 3600,
MIN_CHANGE_PERCENT: 2.0,
MIN_SIGNAL_STRENGTH: 5,
TOP_N_SYMBOLS: 10,
MIN_VOLATILITY: 0.02
}
},
balanced: {
name: '平衡配置',
desc: '推荐使用,平衡频率和质量',
configs: {
SCAN_INTERVAL: 600,
MIN_CHANGE_PERCENT: 1.5,
MIN_SIGNAL_STRENGTH: 4,
TOP_N_SYMBOLS: 12,
MIN_VOLATILITY: 0.018
}
},
aggressive: {
name: '激进高频',
desc: '晚间波动大时使用,交易频率高',
configs: {
SCAN_INTERVAL: 300,
MIN_CHANGE_PERCENT: 1.0,
MIN_SIGNAL_STRENGTH: 3,
TOP_N_SYMBOLS: 20,
MIN_VOLATILITY: 0.015
}
}
}
useEffect(() => {
loadConfigs()
}, [])
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()
} catch (error) {
const errorMsg = error.message || '更新失败'
setMessage('更新失败: ' + errorMsg)
console.error('Config update error:', error)
} finally {
setSaving(false)
}
}
// 检测当前配置匹配哪个预设方案
const detectCurrentPreset = () => {
if (!configs || Object.keys(configs).length === 0) return null
for (const [presetKey, preset] of Object.entries(presets)) {
let match = true
for (const [key, expectedValue] of Object.entries(preset.configs)) {
const currentConfig = configs[key]
if (!currentConfig) {
match = false
break
}
// 获取当前值(处理百分比转换)
let currentValue = currentConfig.value
if (key.includes('PERCENT')) {
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) return null
return {
key,
value: key.includes('PERCENT') ? 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
加载中...
const configCategories = {
'scan': '市场扫描',
'position': '仓位控制',
'risk': '风险控制',
'strategy': '策略参数',
'api': 'API配置'
}
return (
交易配置
📖 配置说明
{/* 预设方案快速切换 */}
快速切换方案
当前方案:
{currentPreset ? presets[currentPreset].name : '自定义'}
{Object.entries(presets).map(([key, preset]) => (
))}
{message && (
{message}
)}
{Object.entries(configCategories).map(([category, label]) => (
{label}
{Object.entries(configs)
.filter(([key, config]) => config.category === category)
.map(([key, config]) => (
handleUpdate(key, value, config.type, config.category)}
disabled={saving}
/>
))}
))}
)
}
const ConfigItem = ({ label, config, onUpdate, disabled }) => {
const [value, setValue] = useState(config.value)
const [localValue, setLocalValue] = useState(config.value)
const [isEditing, setIsEditing] = useState(false)
const [showDetail, setShowDetail] = useState(false)
useEffect(() => {
setValue(config.value)
setLocalValue(config.value)
}, [config.value])
const handleChange = (newValue) => {
setLocalValue(newValue)
setIsEditing(true)
}
const handleSave = () => {
setIsEditing(false)
if (localValue !== value) {
// 值发生变化,保存
let finalValue = localValue
if (config.type === 'number') {
finalValue = parseFloat(localValue) || 0
// 百分比配置需要转换
if (label.includes('PERCENT')) {
finalValue = finalValue
}
} else if (config.type === 'boolean') {
finalValue = localValue === 'true' || localValue === true
}
onUpdate(finalValue)
}
}
const handleBlur = () => {
// 移动端不自动保存,需要点击保存按钮
if (window.innerWidth > 768) {
handleSave()
}
}
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
handleSave()
}
}
const displayValue = config.type === 'number' && label.includes('PERCENT')
? (localValue * 100).toFixed(2)
: localValue
if (config.type === 'boolean') {
return (
{config.description && (
)}
{showDetail && (
{getConfigDetail(label)}
)}
{config.description && (
{config.description}
)}
)
}
if (label.includes('INTERVAL') && ['PRIMARY_INTERVAL', 'CONFIRM_INTERVAL', 'ENTRY_INTERVAL', 'KLINE_INTERVAL'].includes(label)) {
const options = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '1d']
return (
{config.description && (
)}
{showDetail && (
{getConfigDetail(label)}
)}
{config.description && (
{config.description}
)}
)
}
return (
{config.description && (
)}
{showDetail && (
{getConfigDetail(label)}
)}
{
const newValue = config.type === 'number' && label.includes('PERCENT')
? parseFloat(e.target.value) / 100
: e.target.value
handleChange(newValue)
}}
onBlur={handleBlur}
onKeyPress={handleKeyPress}
disabled={disabled}
step={config.type === 'number' ? '0.01' : undefined}
className={isEditing ? 'editing' : ''}
/>
{isEditing && (
)}
{isEditing && window.innerWidth > 768 &&
按Enter保存}
{config.description && (
{config.description}
)}
)
}
// 配置项详细说明
const getConfigDetail = (key) => {
const details = {
// 市场扫描参数
'SCAN_INTERVAL': '扫描间隔(秒)。系统每隔多长时间扫描一次市场寻找交易机会。值越小扫描越频繁,能更快捕捉波动,但会增加API请求和系统负载。建议:保守策略3600秒(1小时),平衡策略600秒(10分钟),激进策略300秒(5分钟)。晚间波动大时可降低到300-600秒。',
'MIN_CHANGE_PERCENT': '最小涨跌幅阈值(%)。只有24小时涨跌幅达到此值的交易对才会被考虑交易。值越小捕捉机会越多,但可能包含噪音和假信号。值越大只捕捉大幅波动,信号质量更高但机会更少。建议:保守策略2.0-3.0%,平衡策略1.5-2.0%,激进策略1.0-1.5%。',
'MIN_SIGNAL_STRENGTH': '最小信号强度(0-10)。技术指标综合评分,只有达到此强度的信号才会执行交易。值越小交易机会越多,但信号质量可能下降,胜率降低。值越大只执行高质量信号,胜率更高但机会更少。建议:保守策略5-7,平衡策略4-5,激进策略3-4。',
'TOP_N_SYMBOLS': '每次扫描后处理的交易对数量。从符合条件的交易对中选择涨跌幅最大的前N个进行详细分析。值越大机会越多,但计算量增加,API请求增多。建议:保守策略8-10个,平衡策略12-15个,激进策略15-20个。',
'MIN_VOLATILITY': '最小波动率(小数形式,如0.02表示2%)。过滤掉波动率低于此值的交易对,确保只交易有足够波动的币种。值越小允许更多交易对,但可能包含波动不足的币种。值越大只交易高波动币种,但可能错过一些机会。建议:0.015-0.025(1.5%-2.5%)。',
'MIN_VOLUME_24H': '最小24小时成交量(USDT)。过滤掉24小时交易量低于此值的交易对,确保只交易流动性好的币种,避免滑点和流动性风险。建议:≥500万USDT,主流币种可设置1000万以上。',
'KLINE_INTERVAL': 'K线数据周期。获取K线数据的基础周期,影响技术指标的计算粒度。周期越短反应越快,但可能包含更多噪音。周期越长信号更平滑,但反应较慢。建议:5m-1h,通常与PRIMARY_INTERVAL保持一致。',
'PRIMARY_INTERVAL': '主周期。策略分析的主要时间周期,用于计算技术指标(RSI、MACD、布林带等)。决定策略主要关注的市场趋势级别。周期越短反应越快,周期越长趋势更明确。建议:保守策略4h-1d,平衡策略1h-4h,激进策略15m-1h。',
'CONFIRM_INTERVAL': '确认周期。用于确认交易信号的更长时间周期,增加信号的可靠性,减少假信号。通常比主周期更长。建议:保守策略1d,平衡策略4h-1d,激进策略1h-4h。',
'ENTRY_INTERVAL': '入场周期。用于精确入场的短时间周期,优化入场点,提高单笔交易的盈亏比。通常比主周期更短。建议:保守策略1h,平衡策略15m-30m,激进策略5m-15m。',
// 仓位控制参数
'MAX_POSITION_PERCENT': '单笔最大仓位(账户余额的百分比,如0.05表示5%)。单笔交易允许的最大仓位大小。值越大单笔金额越大,潜在收益和风险都增加。注意:币安要求最小名义价值5 USDT,如果账户余额较小,系统会自动调整。建议:保守策略3-5%,平衡策略5-7%,激进策略7-10%。',
'MAX_TOTAL_POSITION_PERCENT': '总仓位上限(账户余额的百分比,如0.30表示30%)。所有持仓的总价值不能超过账户余额的百分比,防止过度交易和风险集中。值越大可以同时持有更多仓位,但风险集中度增加。建议:保守策略20-30%,平衡策略30-40%,激进策略40-50%。',
'MIN_POSITION_PERCENT': '单笔最小仓位(账户余额的百分比,如0.01表示1%)。单笔交易允许的最小仓位大小,避免交易过小的仓位,减少手续费影响。建议:1-2%。',
// 风险控制参数
'STOP_LOSS_PERCENT': '止损百分比(如0.03表示3%)。当亏损达到此百分比时自动平仓止损,限制单笔交易的最大亏损。值越小止损更严格,单笔损失更小但可能被正常波动触发。值越大允许更大的回撤,但单笔损失可能较大。建议:保守策略3-5%,平衡策略2-3%,激进策略2-3%。注意:止损应该小于止盈,建议盈亏比至少1:1.5。',
'TAKE_PROFIT_PERCENT': '止盈百分比(如0.05表示5%)。当盈利达到此百分比时自动平仓止盈,锁定利润。值越大目标利润更高,但可能错过及时止盈的机会,持仓时间更长。值越小能更快锁定利润,但可能错过更大的趋势。建议:保守策略5-8%,平衡策略5-6%,激进策略3-5%。注意:应该大于止损,建议盈亏比至少1:1.5。',
// 策略参数
'LEVERAGE': '交易杠杆倍数。放大资金利用率,同时放大收益和风险。杠杆越高,相同仓位下需要的保证金越少,但风险越大。建议:保守策略5-10倍,平衡策略10倍,激进策略10-15倍。注意:高杠杆会增加爆仓风险,请谨慎使用。',
'USE_TRAILING_STOP': '是否启用移动止损(true/false)。启用后,当盈利达到激活阈值时,止损会自动跟踪价格,保护利润。适合趋势行情,可以捕捉更大的利润空间。建议:平衡和激进策略启用,保守策略可关闭。',
'TRAILING_STOP_ACTIVATION': '移动止损激活阈值(百分比,如0.01表示1%)。当盈利达到此百分比时,移动止损开始跟踪价格,将止损移至成本价(保本)。值越小激活越早,更早保护利润但可能过早退出。值越大激活越晚,给价格更多波动空间。建议:1-2%。',
'TRAILING_STOP_PROTECT': '移动止损保护利润(百分比,如0.01表示1%)。当价格从最高点回撤达到此百分比时,触发止损平仓,锁定利润。值越小保护更严格,能锁定更多利润但可能过早退出。值越大允许更大回撤,可能捕捉更大趋势但利润可能回吐。建议:1-2%。',
'USE_UNICORN_WEBSOCKET': '是否使用高性能Unicorn WebSocket API(true/false)。启用后使用unicorn-binance-websocket-api获取实时数据,效率更高,减少API请求频率限制。建议:启用(true)。',
// API配置
'BINANCE_API_KEY': '币安API密钥。用于访问币安账户的凭证,需要启用"合约交易"权限。请妥善保管,不要泄露。',
'BINANCE_API_SECRET': '币安API密钥。与API Key配对使用,用于签名验证。请妥善保管,不要泄露。',
'USE_TESTNET': '是否使用币安测试网(true/false)。true表示使用测试网(模拟交易,无真实资金),false表示使用生产网(真实交易)。建议:测试时使用true,正式交易使用false。'
}
return details[key] || '暂无详细说明,请参考配置说明文档'
}
export default ConfigPanel