734 lines
31 KiB
JavaScript
734 lines
31 KiB
JavaScript
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 [feasibilityCheck, setFeasibilityCheck] = useState(null)
|
||
const [checkingFeasibility, setCheckingFeasibility] = useState(false)
|
||
|
||
// 预设方案配置
|
||
// 注意:百分比配置使用整数形式(如8.0表示8%),在应用时会转换为小数(0.08)
|
||
const presets = {
|
||
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: 20.0, // 20%(相对于保证金,给趋势更多空间)
|
||
MIN_STOP_LOSS_PRICE_PCT: 2.0, // 2%最小价格变动保护
|
||
MIN_TAKE_PROFIT_PRICE_PCT: 3.0 // 3%最小价格变动保护
|
||
}
|
||
},
|
||
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: 20.0, // 20%(相对于保证金,盈亏比2.5:1,提高收益)
|
||
MIN_STOP_LOSS_PRICE_PCT: 2.0, // 2%最小价格变动保护
|
||
MIN_TAKE_PROFIT_PRICE_PCT: 3.0 // 3%最小价格变动保护
|
||
}
|
||
},
|
||
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: 15.0, // 15%(相对于保证金,盈亏比3:1,能捕捉更大趋势)
|
||
MIN_STOP_LOSS_PRICE_PCT: 1.5, // 1.5%最小价格变动保护
|
||
MIN_TAKE_PROFIT_PRICE_PCT: 2.0 // 2%最小价格变动保护
|
||
}
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
loadConfigs()
|
||
checkFeasibility()
|
||
}, [])
|
||
|
||
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 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')) {
|
||
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 (key.includes('PERCENT') || key.includes('PCT')) {
|
||
type = 'number'
|
||
if (key.includes('STOP_LOSS') || key.includes('TAKE_PROFIT')) {
|
||
category = 'risk'
|
||
} 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')) ? value / 100 : value,
|
||
type,
|
||
category,
|
||
description: `预设方案配置项:${key}`
|
||
}
|
||
}
|
||
return {
|
||
key,
|
||
value: (key.includes('PERCENT') || key.includes('PCT')) ? 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 <div className="loading">加载中...</div>
|
||
|
||
const configCategories = {
|
||
'scan': '市场扫描',
|
||
'position': '仓位控制',
|
||
'risk': '风险控制',
|
||
'strategy': '策略参数',
|
||
'api': 'API配置'
|
||
}
|
||
|
||
return (
|
||
<div className="config-panel">
|
||
<div className="config-header">
|
||
<div className="header-top">
|
||
<h2>交易配置</h2>
|
||
<Link to="/config/guide" className="guide-link">📖 配置说明</Link>
|
||
</div>
|
||
<div className="config-info">
|
||
<p>修改配置后,交易系统将在下次扫描时自动使用新配置</p>
|
||
</div>
|
||
|
||
{/* 预设方案快速切换 */}
|
||
<div className="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[currentPreset].name : '自定义'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="preset-buttons">
|
||
{Object.entries(presets).map(([key, preset]) => (
|
||
<button
|
||
key={key}
|
||
className={`preset-btn ${currentPreset === key ? 'active' : ''}`}
|
||
onClick={() => applyPreset(key)}
|
||
disabled={saving}
|
||
title={preset.desc}
|
||
>
|
||
<div className="preset-name">
|
||
{preset.name}
|
||
{currentPreset === key && <span className="active-indicator">✓</span>}
|
||
</div>
|
||
<div className="preset-desc">{preset.desc}</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 配置可行性检查提示 */}
|
||
{feasibilityCheck && (
|
||
<div className={`feasibility-check ${feasibilityCheck.feasible ? 'feasible' : 'infeasible'}`}>
|
||
<div className="feasibility-header">
|
||
<h4>
|
||
{feasibilityCheck.feasible ? '✓ 配置可行' : '⚠ 配置冲突'}
|
||
</h4>
|
||
<button
|
||
onClick={checkFeasibility}
|
||
disabled={checkingFeasibility}
|
||
className="refresh-btn"
|
||
title="重新检查"
|
||
>
|
||
{checkingFeasibility ? '检查中...' : '🔄 刷新'}
|
||
</button>
|
||
</div>
|
||
<div className="feasibility-info">
|
||
<p>
|
||
账户余额: <strong>{feasibilityCheck.account_balance?.toFixed(2) || 'N/A'}</strong> USDT |
|
||
杠杆: <strong>{feasibilityCheck.leverage}x</strong> |
|
||
最小保证金: <strong>{feasibilityCheck.current_config?.min_margin_usdt?.toFixed(2) || 'N/A'}</strong> USDT
|
||
</p>
|
||
{!feasibilityCheck.feasible && (
|
||
<p className="warning-text">
|
||
需要仓位价值: <strong>{feasibilityCheck.calculated_values?.required_position_percent?.toFixed(1)}%</strong> |
|
||
最大允许: <strong>{feasibilityCheck.calculated_values?.max_allowed_position_percent?.toFixed(1)}%</strong>
|
||
</p>
|
||
)}
|
||
</div>
|
||
{feasibilityCheck.suggestions && feasibilityCheck.suggestions.length > 0 && (
|
||
<div className="feasibility-suggestions">
|
||
<h5>建议方案:</h5>
|
||
{feasibilityCheck.suggestions.map((suggestion, index) => (
|
||
<div key={index} className="suggestion-item">
|
||
<div className="suggestion-header">
|
||
<strong>{suggestion.title}</strong>
|
||
</div>
|
||
<p className="suggestion-desc">{suggestion.description}</p>
|
||
{suggestion.config_key && suggestion.suggested_value !== null && (
|
||
<button
|
||
className="apply-suggestion-btn"
|
||
onClick={async () => {
|
||
try {
|
||
await api.updateConfig(suggestion.config_key, {
|
||
value: suggestion.suggested_value,
|
||
type: 'number',
|
||
category: 'position'
|
||
})
|
||
setMessage(`已应用建议: ${suggestion.title}`)
|
||
await loadConfigs()
|
||
await checkFeasibility()
|
||
} catch (error) {
|
||
setMessage('应用建议失败: ' + error.message)
|
||
}
|
||
}}
|
||
>
|
||
应用此建议
|
||
</button>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{message && (
|
||
<div className={`message ${message.includes('失败') || message.includes('错误') ? 'error' : 'success'}`}>
|
||
{message}
|
||
</div>
|
||
)}
|
||
|
||
{Object.entries(configCategories).map(([category, label]) => (
|
||
<section key={category} className="config-section">
|
||
<h3>{label}</h3>
|
||
<div className="config-grid">
|
||
{Object.entries(configs)
|
||
.filter(([key, config]) => config.category === category)
|
||
.map(([key, config]) => (
|
||
<ConfigItem
|
||
key={key}
|
||
label={key}
|
||
config={config}
|
||
onUpdate={(value) => handleUpdate(key, value, config.type, config.category)}
|
||
disabled={saving}
|
||
/>
|
||
))}
|
||
</div>
|
||
</section>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||
// 初始化显示值:百分比配置转换为整数形式显示
|
||
const getInitialDisplayValue = (val) => {
|
||
if (config.type === 'number' && label.includes('PERCENT')) {
|
||
if (val === null || val === undefined || val === '') {
|
||
return ''
|
||
}
|
||
const numVal = typeof val === 'string' ? parseFloat(val) : val
|
||
if (isNaN(numVal)) {
|
||
return ''
|
||
}
|
||
// 如果是小数形式(0.05),转换为整数显示(5)
|
||
if (numVal < 1) {
|
||
return Math.round(numVal * 100)
|
||
}
|
||
// 如果已经是整数形式(5),直接显示
|
||
return numVal
|
||
}
|
||
return val === null || val === undefined ? '' : val
|
||
}
|
||
|
||
const [value, setValue] = useState(config.value)
|
||
const [localValue, setLocalValue] = useState(getInitialDisplayValue(config.value))
|
||
const [isEditing, setIsEditing] = useState(false)
|
||
const [showDetail, setShowDetail] = useState(false)
|
||
|
||
useEffect(() => {
|
||
setValue(config.value)
|
||
// 当配置值更新时,重置编辑状态和本地值
|
||
setIsEditing(false)
|
||
setLocalValue(getInitialDisplayValue(config.value))
|
||
}, [config.value])
|
||
|
||
const handleChange = (newValue) => {
|
||
setLocalValue(newValue)
|
||
setIsEditing(true)
|
||
}
|
||
|
||
const handleSave = () => {
|
||
setIsEditing(false)
|
||
|
||
// 处理localValue可能是字符串的情况
|
||
let processedValue = localValue
|
||
if (config.type === 'number') {
|
||
// 如果是空字符串,恢复原值
|
||
if (localValue === '' || localValue === null || localValue === undefined) {
|
||
setLocalValue(value)
|
||
return
|
||
}
|
||
|
||
const numValue = typeof localValue === 'string' ? parseFloat(localValue) : localValue
|
||
if (isNaN(numValue)) {
|
||
// 如果输入无效,恢复原值
|
||
const restoreValue = label.includes('PERCENT')
|
||
? (typeof value === 'number' && value < 1 ? Math.round(value * 100) : value)
|
||
: value
|
||
setLocalValue(restoreValue)
|
||
return
|
||
}
|
||
|
||
processedValue = numValue
|
||
// 百分比配置需要转换:用户输入的是整数(如5),需要转换为小数(0.05)
|
||
if (label.includes('PERCENT')) {
|
||
// 用户输入的是整数形式(如5表示5%),需要转换为小数(0.05)
|
||
// 如果输入值大于等于1,说明是百分比形式,需要除以100
|
||
// 如果输入值小于1,说明已经是小数形式,直接使用
|
||
if (processedValue >= 1) {
|
||
processedValue = processedValue / 100
|
||
}
|
||
// 如果小于1,说明已经是小数形式,直接使用
|
||
}
|
||
} else if (config.type === 'boolean') {
|
||
processedValue = localValue === 'true' || localValue === true
|
||
}
|
||
|
||
// 只有当值真正发生变化时才保存
|
||
if (processedValue !== value) {
|
||
onUpdate(processedValue)
|
||
} else {
|
||
// 值没变化,但需要更新显示值
|
||
setValue(processedValue)
|
||
}
|
||
}
|
||
|
||
const handleBlur = () => {
|
||
// 移动端不自动保存,需要点击保存按钮
|
||
if (window.innerWidth > 768) {
|
||
handleSave()
|
||
}
|
||
}
|
||
|
||
const handleKeyPress = (e) => {
|
||
if (e.key === 'Enter') {
|
||
handleSave()
|
||
}
|
||
}
|
||
|
||
// 显示值:百分比配置显示为整数(如5),其他保持原样
|
||
// 处理localValue可能是字符串的情况
|
||
const getDisplayValue = () => {
|
||
if (config.type === 'number' && label.includes('PERCENT')) {
|
||
if (localValue === '' || localValue === null || localValue === undefined) {
|
||
return ''
|
||
}
|
||
const numValue = typeof localValue === 'string' ? parseFloat(localValue) : localValue
|
||
if (isNaN(numValue)) {
|
||
return ''
|
||
}
|
||
// localValue已经是显示值(整数形式),直接返回
|
||
return numValue
|
||
}
|
||
return localValue === null || localValue === undefined ? '' : localValue
|
||
}
|
||
|
||
const displayValue = getDisplayValue()
|
||
|
||
if (config.type === 'boolean') {
|
||
return (
|
||
<div className="config-item">
|
||
<div className="config-item-header">
|
||
<label>{label}</label>
|
||
{config.description && (
|
||
<button
|
||
className="help-btn"
|
||
onClick={() => setShowDetail(!showDetail)}
|
||
type="button"
|
||
>
|
||
{showDetail ? '收起' : '说明'}
|
||
</button>
|
||
)}
|
||
</div>
|
||
{showDetail && (
|
||
<div className="config-detail-popup">
|
||
{getConfigDetail(label)}
|
||
</div>
|
||
)}
|
||
<select
|
||
value={localValue ? 'true' : 'false'}
|
||
onChange={(e) => {
|
||
handleChange(e.target.value)
|
||
// 布尔值立即保存
|
||
onUpdate(e.target.value === 'true')
|
||
}}
|
||
disabled={disabled}
|
||
>
|
||
<option value="true">是</option>
|
||
<option value="false">否</option>
|
||
</select>
|
||
{config.description && (
|
||
<span className="description">
|
||
{config.description}
|
||
</span>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
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 (
|
||
<div className="config-item">
|
||
<div className="config-item-header">
|
||
<label>{label}</label>
|
||
{config.description && (
|
||
<button
|
||
className="help-btn"
|
||
onClick={() => setShowDetail(!showDetail)}
|
||
type="button"
|
||
>
|
||
{showDetail ? '收起' : '说明'}
|
||
</button>
|
||
)}
|
||
</div>
|
||
{showDetail && (
|
||
<div className="config-detail-popup">
|
||
{getConfigDetail(label)}
|
||
</div>
|
||
)}
|
||
<select
|
||
value={localValue}
|
||
onChange={(e) => {
|
||
handleChange(e.target.value)
|
||
// 下拉框立即保存
|
||
onUpdate(e.target.value)
|
||
}}
|
||
disabled={disabled}
|
||
>
|
||
{options.map(opt => (
|
||
<option key={opt} value={opt}>{opt}</option>
|
||
))}
|
||
</select>
|
||
{config.description && (
|
||
<span className="description">
|
||
{config.description}
|
||
</span>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="config-item">
|
||
<div className="config-item-header">
|
||
<label>{label}</label>
|
||
{config.description && (
|
||
<button
|
||
className="help-btn"
|
||
onClick={() => setShowDetail(!showDetail)}
|
||
type="button"
|
||
>
|
||
{showDetail ? '收起' : '说明'}
|
||
</button>
|
||
)}
|
||
</div>
|
||
{showDetail && (
|
||
<div className="config-detail-popup">
|
||
{getConfigDetail(label)}
|
||
</div>
|
||
)}
|
||
<div className="config-input-wrapper">
|
||
<input
|
||
type="text"
|
||
inputMode={config.type === 'number' ? 'decimal' : 'text'}
|
||
value={label.includes('PERCENT')
|
||
? (displayValue === '' || displayValue === null || displayValue === undefined ? '' : String(displayValue))
|
||
: (localValue === '' || localValue === null || localValue === undefined ? '' : String(localValue))}
|
||
onChange={(e) => {
|
||
// 使用文本输入,避免number类型的自动补0问题
|
||
let newValue = e.target.value
|
||
|
||
// 对于数字类型,只允许数字、小数点和负号
|
||
if (config.type === 'number') {
|
||
// 允许空字符串、数字、小数点和负号
|
||
const validPattern = /^-?\d*\.?\d*$/
|
||
if (newValue !== '' && !validPattern.test(newValue)) {
|
||
// 无效输入,不更新
|
||
return
|
||
}
|
||
|
||
// 如果是百分比配置,限制输入范围(0-100)
|
||
if (label.includes('PERCENT')) {
|
||
const numValue = parseFloat(newValue)
|
||
if (newValue !== '' && !isNaN(numValue) && (numValue < 0 || numValue > 100)) {
|
||
// 超出范围,不更新
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新本地值
|
||
if (config.type === 'number' && label.includes('PERCENT')) {
|
||
// 百分比配置:保持数字形式(整数),允许空值
|
||
if (newValue === '') {
|
||
handleChange('')
|
||
} else {
|
||
const numValue = parseFloat(newValue)
|
||
if (!isNaN(numValue)) {
|
||
handleChange(numValue)
|
||
}
|
||
// 如果无效,不更新(保持原值)
|
||
}
|
||
} else if (config.type === 'number') {
|
||
// 其他数字配置:保持数字形式,允许空值
|
||
if (newValue === '') {
|
||
handleChange('')
|
||
} else {
|
||
const numValue = parseFloat(newValue)
|
||
if (!isNaN(numValue)) {
|
||
handleChange(numValue)
|
||
}
|
||
// 如果无效,不更新(保持原值)
|
||
}
|
||
} else {
|
||
handleChange(newValue)
|
||
}
|
||
}}
|
||
onBlur={handleBlur}
|
||
onKeyPress={handleKeyPress}
|
||
onKeyDown={(e) => {
|
||
// 允许删除、退格、方向键等
|
||
if (['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {
|
||
return
|
||
}
|
||
// 对于数字输入,只允许数字、小数点和负号
|
||
if (config.type === 'number') {
|
||
const validKeys = /^[0-9.-]$/
|
||
if (!validKeys.test(e.key) && !['Enter', 'Escape'].includes(e.key)) {
|
||
e.preventDefault()
|
||
}
|
||
}
|
||
}}
|
||
disabled={disabled}
|
||
className={isEditing ? 'editing' : ''}
|
||
placeholder={label.includes('PERCENT') ? '输入百分比' : '输入数值'}
|
||
/>
|
||
{label.includes('PERCENT') && <span className="percent-suffix">%</span>}
|
||
{isEditing && (
|
||
<button
|
||
className="save-btn"
|
||
onClick={handleSave}
|
||
disabled={disabled}
|
||
type="button"
|
||
>
|
||
保存
|
||
</button>
|
||
)}
|
||
</div>
|
||
{isEditing && window.innerWidth > 768 && <span className="edit-hint">按Enter保存</span>}
|
||
{config.description && (
|
||
<span className="description">
|
||
{config.description}
|
||
</span>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 配置项详细说明
|
||
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个。',
|
||
'MAX_SCAN_SYMBOLS': '扫描的最大交易对数量(0表示扫描所有)。限制每次扫描时处理的交易对总数,减少API请求和计算量。值越小扫描越快,但可能错过一些机会。值越大覆盖更全面,但API请求和计算量增加。建议:保守策略100-200个,平衡策略200-300个,激进策略300-500个。设置为0会扫描所有交易对(约500+个)。',
|
||
'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.08表示8%,相对于保证金)。当亏损达到此百分比时自动平仓止损,限制单笔交易的最大亏损。值越小止损更严格,单笔损失更小但可能被正常波动触发。值越大允许更大的回撤,但单笔损失可能较大。建议:保守策略10-15%,平衡策略8-10%,激进策略5-8%。注意:止损应该小于止盈,建议盈亏比至少1:1.5。系统会结合最小价格变动保护,取更宽松的一个。',
|
||
'TAKE_PROFIT_PERCENT': '止盈百分比(如0.15表示15%,相对于保证金)。当盈利达到此百分比时自动平仓止盈,锁定利润。值越大目标利润更高,但可能错过及时止盈的机会,持仓时间更长。值越小能更快锁定利润,但可能错过更大的趋势。建议:保守策略20-30%,平衡策略15-20%,激进策略10-15%。注意:应该大于止损,建议盈亏比至少1:1.5。系统会结合最小价格变动保护,取更宽松的一个。',
|
||
'MIN_STOP_LOSS_PRICE_PCT': '最小止损价格变动百分比(如0.02表示2%)。防止止损过紧,即使基于保证金的止损更紧,也会使用至少此百分比的价格变动。建议:保守策略2-3%,平衡策略2%,激进策略1.5-2%。',
|
||
'MIN_TAKE_PROFIT_PRICE_PCT': '最小止盈价格变动百分比(如0.03表示3%)。防止止盈过紧,即使基于保证金的止盈更紧,也会使用至少此百分比的价格变动。建议:保守策略3-4%,平衡策略3%,激进策略2-3%。',
|
||
|
||
// 策略参数
|
||
'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%。',
|
||
|
||
// 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
|