auto_trade_sys/frontend/src/components/ConfigPanel.jsx
薇薇安 84c1b2889f a
2026-01-14 10:40:38 +08:00

476 lines
19 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 { 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 <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>
{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 [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 (
<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={config.type === 'number' ? 'number' : 'text'}
value={label.includes('PERCENT') ? displayValue : localValue}
onChange={(e) => {
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 && (
<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个。',
'MIN_VOLATILITY': '最小波动率小数形式如0.02表示2%。过滤掉波动率低于此值的交易对确保只交易有足够波动的币种。值越小允许更多交易对但可能包含波动不足的币种。值越大只交易高波动币种但可能错过一些机会。建议0.015-0.0251.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 APItrue/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