auto_trade_sys/frontend/src/components/ConfigPanel.jsx
薇薇安 dc49c2717b a
2026-01-22 08:50:42 +08:00

2104 lines
91 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, getCurrentAccountId, setCurrentAccountId } from '../services/api'
import './ConfigPanel.css'
const ConfigPanel = ({ currentUser }) => {
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)
const [systemStatus, setSystemStatus] = useState(null)
const [backendStatus, setBackendStatus] = useState(null)
const [systemBusy, setSystemBusy] = useState(false)
const [accountTradingStatus, setAccountTradingStatus] = useState(null)
const [accountTradingErr, setAccountTradingErr] = useState('')
const [currentAccountMeta, setCurrentAccountMeta] = useState(null)
const [configMeta, setConfigMeta] = useState(null)
// 多账号:当前账号(仅用于配置页提示;全局切换器在顶部导航)
const [accountId, setAccountId] = useState(getCurrentAccountId())
const isAdmin = (currentUser?.role || '') === 'admin'
const globalStrategyAccountId = parseInt(String(configMeta?.global_strategy_account_id || '1'), 10) || 1
const isGlobalStrategyAccount = isAdmin && accountId === globalStrategyAccountId
const loadConfigMeta = async () => {
try {
const m = await api.getConfigMeta()
setConfigMeta(m || null)
} catch (e) {
setConfigMeta(null)
}
}
// 账号管理(超管)
const [accountsAdmin, setAccountsAdmin] = useState([])
const [accountsBusy, setAccountsBusy] = useState(false)
const [showAccountsAdmin, setShowAccountsAdmin] = useState(false)
const [newAccount, setNewAccount] = useState({
name: '',
api_key: '',
api_secret: '',
use_testnet: false,
status: 'active',
})
const [credEditId, setCredEditId] = useState(null)
const [credForm, setCredForm] = useState({ api_key: '', api_secret: '', use_testnet: false })
// “PCT”类配置里有少数是“百分比数值<=1表示<=1%而不是“0~1比例”
// 例如 LIMIT_ORDER_OFFSET_PCT=0.5 表示 0.5%(而不是 50%
const PCT_LIKE_KEYS = new Set([
'LIMIT_ORDER_OFFSET_PCT',
'ENTRY_MAX_DRIFT_PCT_TRENDING',
'ENTRY_MAX_DRIFT_PCT_RANGING',
])
// 配置快照(用于整体分析/导出)
const [showSnapshot, setShowSnapshot] = useState(false)
const [snapshotText, setSnapshotText] = useState('')
const [snapshotIncludeSecrets, setSnapshotIncludeSecrets] = useState(false)
const [snapshotBusy, setSnapshotBusy] = useState(false)
// 预设方案配置
// 注意百分比配置使用整数形式如8.0表示8%在应用时会转换为小数0.08
const presets = {
swing: {
name: '波段回归(推荐)',
desc: '根治高频与追价:关闭智能入场,回归“纯限价 + 30分钟扫描 + 更高信号门槛”的低频波段。建议先跑20-30单再评估。',
configs: {
// 操作频率
SCAN_INTERVAL: 1800, // 30分钟
TOP_N_SYMBOLS: 8,
// 仓位管理(重要语义:这些百分比均按“保证金占用比例”理解)
MAX_POSITION_PERCENT: 2.0, // 2%
MAX_TOTAL_POSITION_PERCENT: 20.0, // 20%
MIN_POSITION_PERCENT: 0.0, // 0%(等价于关闭最小仓位占比)
// 风控
MIN_SIGNAL_STRENGTH: 8,
USE_TRAILING_STOP: false, // 禁用移动止损,让利润奔跑
ATR_TAKE_PROFIT_MULTIPLIER: 4.5, // 放大ATR止盈倍数从3.0提升到4.5
TAKE_PROFIT_PERCENT: 25.0, // 放宽固定止盈从30%到25%配合ATR止盈放大盈亏比
MIN_HOLD_TIME_SEC: 1800, // 最小持仓时间30分钟强制波段持仓纪律
// 根治:关闭智能入场(回归纯限价,不追价/不市价兜底)
SMART_ENTRY_ENABLED: false,
},
},
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, // 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, // 0.3%
ENTRY_MAX_DRIFT_PCT_RANGING: 0.15, // 0.15%
// 风控:默认关闭移动止损(避免“保本价”过早触发)
USE_TRAILING_STOP: false, // 禁用移动止损,让利润奔跑
ATR_TAKE_PROFIT_MULTIPLIER: 4.5, // 放大ATR止盈倍数从3.0提升到4.5
TAKE_PROFIT_PERCENT: 25.0, // 放宽固定止盈从30%到25%配合ATR止盈放大盈亏比
MIN_HOLD_TIME_SEC: 1800, // 最小持仓时间30分钟强制波段持仓纪律
},
},
strict: {
name: '精选低频(高胜率倾向)',
desc: '更偏“少单、质量优先”:仅趋势行情自动交易 + 4H中性不自动下单 + 更高信号门槛。仍保持较贴近的限价偏移,减少“完全成交不了”。',
configs: {
SCAN_INTERVAL: 1800,
TOP_N_SYMBOLS: 6,
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_TAKE_PROFIT_MULTIPLIER: 4.5, // 放大ATR止盈倍数从3.0提升到4.5
TAKE_PROFIT_PERCENT: 25.0, // 放宽固定止盈从30%到25%配合ATR止盈放大盈亏比
MIN_HOLD_TIME_SEC: 1800, // 最小持仓时间30分钟强制波段持仓纪律
},
},
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_TAKE_PROFIT_MULTIPLIER: 4.5, // 放大ATR止盈倍数从3.0提升到4.5
TAKE_PROFIT_PERCENT: 25.0, // 放宽固定止盈从30%到25%配合ATR止盈放大盈亏比
MIN_HOLD_TIME_SEC: 1800, // 最小持仓时间30分钟强制波段持仓纪律
},
},
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: 25.0, // 25%相对于保证金从20%提升到25%,放大盈亏比)
MIN_STOP_LOSS_PRICE_PCT: 2.0, // 2%最小价格变动保护
MIN_TAKE_PROFIT_PRICE_PCT: 3.0, // 3%最小价格变动保护
USE_TRAILING_STOP: false, // 禁用移动止损,让利润奔跑
ATR_TAKE_PROFIT_MULTIPLIER: 4.5, // 放大ATR止盈倍数从3.0提升到4.5
MIN_HOLD_TIME_SEC: 1800 // 最小持仓时间30分钟强制波段持仓纪律
}
},
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: 25.0, // 25%相对于保证金从20%提升到25%,放大盈亏比)
MIN_STOP_LOSS_PRICE_PCT: 2.0, // 2%最小价格变动保护
MIN_TAKE_PROFIT_PRICE_PCT: 3.0, // 3%最小价格变动保护
USE_TRAILING_STOP: false, // 禁用移动止损,让利润奔跑
ATR_TAKE_PROFIT_MULTIPLIER: 4.5, // 放大ATR止盈倍数从3.0提升到4.5
MIN_HOLD_TIME_SEC: 1800 // 最小持仓时间30分钟强制波段持仓纪律
}
},
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: 25.0, // 25%相对于保证金从15%大幅提升到25%放大盈亏比到5:1
MIN_STOP_LOSS_PRICE_PCT: 1.5, // 1.5%最小价格变动保护
MIN_TAKE_PROFIT_PRICE_PCT: 2.0, // 2%最小价格变动保护
USE_TRAILING_STOP: false, // 禁用移动止损,让利润奔跑
ATR_TAKE_PROFIT_MULTIPLIER: 4.5, // 放大ATR止盈倍数从3.0提升到4.5
MIN_HOLD_TIME_SEC: 1800 // 最小持仓时间30分钟强制波段持仓纪律
}
}
}
const loadSystemStatus = async () => {
try {
const res = await api.getTradingSystemStatus()
setSystemStatus(res)
} catch (error) {
// 静默失败:不影响配置页使用
}
}
const loadBackendStatus = async () => {
try {
const res = await api.getBackendStatus()
setBackendStatus(res)
} catch (error) {
// 静默失败:不影响配置页使用
}
}
const loadAccountTradingStatus = async () => {
try {
const res = await api.getAccountTradingStatus(accountId)
setAccountTradingStatus(res)
setAccountTradingErr('')
} catch (error) {
setAccountTradingStatus(null)
setAccountTradingErr(error?.message || '获取交易进程状态失败')
}
}
const loadCurrentAccountMeta = async () => {
try {
const list = await api.getAccounts()
const accounts = Array.isArray(list) ? list : []
const meta = accounts.find((a) => parseInt(String(a?.id || '0'), 10) === parseInt(String(accountId || '0'), 10))
setCurrentAccountMeta(meta || null)
// 同步 testnet 开关到表单(仅在未输入时同步,避免打断正在输入)
if (meta && !credForm.api_key && !credForm.api_secret) {
setCredForm((prev) => ({ ...prev, use_testnet: !!meta.use_testnet }))
}
} catch (e) {
setCurrentAccountMeta(null)
}
}
const handleAccountTradingEnsure = async () => {
setSystemBusy(true)
setMessage('')
try {
const res = await api.ensureAccountTradingProgram(accountId)
setMessage(`已生成/刷新 supervisor 配置:${res.program || ''}`)
await loadAccountTradingStatus()
} catch (error) {
setMessage('生成 supervisor 配置失败: ' + (error.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
const handleAccountTradingStart = async () => {
const hasKey = !!currentAccountMeta?.has_api_key
const hasSecret = !!currentAccountMeta?.has_api_secret
if (!hasKey || !hasSecret) {
setMessage('请先在“账号密钥(当前账号)”中配置 BINANCE_API_KEY 与 BINANCE_API_SECRET然后再启动交易进程')
return
}
setSystemBusy(true)
setMessage('')
try {
const res = await api.startAccountTrading(accountId)
setMessage(res.message || '交易进程已启动')
await loadAccountTradingStatus()
} catch (error) {
setMessage('启动交易进程失败: ' + (error.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
const handleAccountTradingStop = async () => {
setSystemBusy(true)
setMessage('')
try {
const res = await api.stopAccountTrading(accountId)
setMessage(res.message || '交易进程已停止')
await loadAccountTradingStatus()
} catch (error) {
setMessage('停止交易进程失败: ' + (error.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
const handleAccountTradingRestart = async () => {
const hasKey = !!currentAccountMeta?.has_api_key
const hasSecret = !!currentAccountMeta?.has_api_secret
if (!hasKey || !hasSecret) {
setMessage('请先在“账号密钥(当前账号)”中配置 BINANCE_API_KEY 与 BINANCE_API_SECRET然后再重启交易进程')
return
}
setSystemBusy(true)
setMessage('')
try {
const res = await api.restartAccountTrading(accountId)
setMessage(res.message || '交易进程已重启')
await loadAccountTradingStatus()
} catch (error) {
setMessage('重启交易进程失败: ' + (error.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
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 handleStartTrading = async () => {
setSystemBusy(true)
setMessage('')
try {
const res = await api.startTradingSystem()
setMessage(res.message || '交易系统已启动')
await loadSystemStatus()
} catch (error) {
setMessage('启动失败: ' + (error.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
const handleStopTrading = async () => {
setSystemBusy(true)
setMessage('')
try {
const res = await api.stopTradingSystem()
setMessage(res.message || '交易系统已停止')
await loadSystemStatus()
} catch (error) {
setMessage('停止失败: ' + (error.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}
const handleRestartTrading = async () => {
setSystemBusy(true)
setMessage('')
try {
const res = await api.restartTradingSystem()
setMessage(res.message || '交易系统已重启')
await loadSystemStatus()
} 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)
}
}
useEffect(() => {
loadConfigMeta()
loadConfigs()
checkFeasibility()
if (isAdmin) {
loadSystemStatus()
loadBackendStatus()
}
loadAccountTradingStatus()
loadCurrentAccountMeta()
const timer = setInterval(() => {
if (isAdmin) {
loadSystemStatus()
loadBackendStatus()
}
loadAccountTradingStatus()
loadCurrentAccountMeta()
}, 3000)
return () => clearInterval(timer)
}, [])
const loadAccountsAdmin = async () => {
try {
const list = await api.getAccounts()
setAccountsAdmin(Array.isArray(list) ? list : [])
} catch (e) {
setAccountsAdmin([])
}
}
const notifyAccountsUpdated = () => {
try {
window.dispatchEvent(new Event('ats:accounts:updated'))
} catch (e) {
// ignore
}
}
// 切换账号时,刷新页面数据
useEffect(() => {
setCurrentAccountId(accountId)
setMessage('')
setLoading(true)
loadConfigs()
checkFeasibility()
if (isAdmin) {
loadSystemStatus()
loadBackendStatus()
}
loadAccountTradingStatus()
loadCurrentAccountMeta()
}, [accountId])
// 顶部导航切换账号时:即时同步(比 setInterval 更及时)
useEffect(() => {
const onChanged = (e) => {
const next = parseInt(String(e?.detail?.accountId || ''), 10)
if (Number.isFinite(next) && next > 0) setAccountId(next)
}
window.addEventListener('ats:account:changed', onChanged)
return () => window.removeEventListener('ats:account:changed', onChanged)
}, [])
// 顶部导航切换账号时localStorage更新这里做一个轻量同步
useEffect(() => {
const timer = setInterval(() => {
const cur = getCurrentAccountId()
if (cur !== accountId) setAccountId(cur)
}, 1000)
return () => clearInterval(timer)
}, [accountId])
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 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'))) {
// 兼容两种:
// - 常规 PERCENT/PCT存储为 0~1比例展示为 0~100%
// - PCT_LIKE_KEYS存储可能是 0.006(=0.6%) 或 0.6(=0.6%),展示统一为“百分比数值”
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) => {
// 为了确保是最新值,这里点击时再拉一次
const 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 || '',
}
})
// 排序:先分类再按 key
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 snapshot = {
fetched_at: now.toISOString(),
note: 'display_value 对 PERCENT/PCT 做了百分比换算;敏感字段可选择脱敏/明文。',
preset_detected: detectCurrentPreset(),
system_status: systemStatus
? {
running: !!systemStatus.running,
pid: systemStatus.pid || null,
program: systemStatus.program || null,
state: systemStatus.state || null,
}
: null,
feasibility_check_summary: feasibilityCheck
? {
feasible: !!feasibilityCheck.feasible,
account_balance: feasibilityCheck.account_balance ?? null,
base_leverage: feasibilityCheck.base_leverage ?? feasibilityCheck.leverage ?? null,
max_leverage: feasibilityCheck.max_leverage ?? null,
use_dynamic_leverage: feasibilityCheck.use_dynamic_leverage ?? null,
min_margin_usdt: feasibilityCheck.current_config?.min_margin_usdt ?? 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 || '未知错误'))
}
}
// 检测当前配置匹配哪个预设方案
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')) {
if (PCT_LIKE_KEYS.has(key)) {
// 兼容旧值0.006(=0.6%) 或 0.6(=0.6%)
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) {
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 (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'
}
const detail = typeof getConfigDetail === 'function' ? getConfigDetail(key) : ''
const desc =
detail && typeof detail === 'string' && !detail.includes('暂无详细说明')
? detail
: `预设方案配置项:${key}`
return {
key,
value:
(key.includes('PERCENT') || key.includes('PCT')) && !PCT_LIKE_KEYS.has(key)
? value / 100
: value,
type,
category,
description: desc
}
}
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)
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">
<div className="header-left">
<h2>交易配置</h2>
<div className="account-switch">
<span className="account-hint">当前账号#{accountId}在顶部导航切换</span>
</div>
{isAdmin ? (
<div
className="system-hint"
style={{
marginTop: '8px',
padding: '10px',
border: '1px solid #e0b400',
background: '#fff8e1',
}}
>
<div style={{ fontWeight: 700 }}>
{isGlobalStrategyAccount ? (
<>
你正在编辑 <span style={{ fontWeight: 900 }}>全局策略账号 #{globalStrategyAccountId}</span>
此处修改将影响所有用户的策略核心
</>
) : (
<>
你正在编辑账号 #{accountId}这里只允许调整 <span style={{ fontWeight: 900 }}>风险旋钮</span>//
策略核心统一来自 <span style={{ fontWeight: 900 }}>全局策略账号 #{globalStrategyAccountId}</span>
</>
)}
</div>
{!isGlobalStrategyAccount ? (
<div style={{ marginTop: '8px' }}>
<button
type="button"
className="system-btn primary"
onClick={() => {
setCurrentAccountId(globalStrategyAccountId)
setAccountId(globalStrategyAccountId)
setMessage('已切换到全局策略账号(策略核心统一在这里维护)')
}}
>
切换到全局策略账号 #{globalStrategyAccountId}
</button>
</div>
) : null}
</div>
) : null}
</div>
<div className="header-actions">
<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 className="config-info">
<p>修改配置后交易系统将在下次扫描时自动使用新配置</p>
</div>
{/* 我的交易进程按账号owner/admin 可启停) */}
<div className="system-section">
<div className="system-header">
<h3>我的交易进程当前账号 #{accountId}</h3>
<div className="system-status">
<span className={`system-status-badge ${accountTradingStatus?.running ? 'running' : 'stopped'}`}>
{accountTradingStatus?.running ? '运行中' : '未运行/未知'}
</span>
{accountTradingStatus?.pid ? <span className="system-status-meta">PID: {accountTradingStatus.pid}</span> : null}
{accountTradingStatus?.program ? <span className="system-status-meta">程序: {accountTradingStatus.program}</span> : null}
{accountTradingStatus?.state ? <span className="system-status-meta">状态: {accountTradingStatus.state}</span> : null}
</div>
</div>
<div className="system-actions">
<button type="button" className="system-btn" onClick={handleAccountTradingEnsure} disabled={systemBusy} title="为该账号生成/刷新 supervisor program 配置(需要 owner/admin">
生成配置
</button>
<button type="button" className="system-btn" onClick={handleAccountTradingStop} disabled={systemBusy} title="停止该账号交易进程(需要 owner/admin">
停止
</button>
<button type="button" className="system-btn" onClick={handleAccountTradingStart} disabled={systemBusy} title="启动该账号交易进程(需要 owner/admin">
启动
</button>
<button type="button" className="system-btn primary" onClick={handleAccountTradingRestart} disabled={systemBusy} title="重启该账号交易进程(需要 owner/admin">
重启
</button>
</div>
<div className="system-hint">
提示若按钮报无权限请让管理员在用户授权里把该账号分配为 owner若报 supervisor 相关错误请检查后端对 `/www/server/panel/plugin/supervisor` 的写权限与 supervisorctl 可执行权限
</div>
{accountTradingErr ? (
<div className="system-hint" style={{ color: '#b00020' }}>
{accountTradingErr}
</div>
) : null}
{accountTradingStatus?.raw ? (
<details style={{ marginTop: '8px' }}>
<summary style={{ cursor: 'pointer' }}>查看原始状态输出</summary>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>{String(accountTradingStatus.raw || '')}</pre>
</details>
) : null}
{accountTradingStatus?.stderr_tail ? (
<details style={{ marginTop: '8px' }}>
<summary style={{ cursor: 'pointer' }}>查看最近错误日志stderr</summary>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>{String(accountTradingStatus.stderr_tail || '')}</pre>
</details>
) : null}
{accountTradingStatus?.stdout_tail ? (
<details style={{ marginTop: '8px' }}>
<summary style={{ cursor: 'pointer' }}>查看最近输出日志stdout常见原因在这里</summary>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>{String(accountTradingStatus.stdout_tail || '')}</pre>
</details>
) : null}
{accountTradingStatus?.stderr_tail_error ? (
<details style={{ marginTop: '8px' }}>
<summary style={{ cursor: 'pointer' }}>读取 stderr 失败原因用于排障</summary>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>{String(accountTradingStatus.stderr_tail_error || '')}</pre>
</details>
) : null}
{['FATAL', 'EXITED', 'BACKOFF'].includes(String(accountTradingStatus?.state || '')) ? (
<details style={{ marginTop: '8px' }}>
<summary style={{ cursor: 'pointer' }}>supervisord 主日志尾部spawn error 常见原因在这里</summary>
<pre style={{ whiteSpace: 'pre-wrap', marginTop: '8px' }}>
{String(accountTradingStatus?.supervisord_tail || '(暂无:可能未配置 supervisord logfile 或当前用户无权限读取。可在后端配置 SUPERVISOR_LOGFILE 指向 supervisord 主日志路径)')}
</pre>
</details>
) : null}
</div>
{/* 账号密钥当前账号owner/admin 可改) */}
<div className="system-section">
<div className="system-header">
<h3>账号密钥当前账号</h3>
<div className="system-status">
<span className="system-status-meta">说明为安全起见页面不会回显 Secret填入后保存即可</span>
</div>
</div>
<div className="accounts-form">
<label>
API KEY留空=不改
<div className="system-status-meta" style={{ marginTop: '4px' }}>
当前状态{currentAccountMeta?.has_api_key ? '已配置(******' : '未配置(必须先配置才能启动交易)'}
</div>
<input
type="password"
value={credForm.api_key}
onChange={(e) => setCredForm({ ...credForm, api_key: e.target.value })}
placeholder={currentAccountMeta?.has_api_key ? '已配置(******),如需更换请重新填写' : '粘贴你的 Binance API KEY'}
/>
</label>
<label>
API SECRET留空=不改
<div className="system-status-meta" style={{ marginTop: '4px' }}>
当前状态{currentAccountMeta?.has_api_secret ? '已配置(******' : '未配置(必须先配置才能启动交易)'}
</div>
<input
type="password"
value={credForm.api_secret}
onChange={(e) => setCredForm({ ...credForm, api_secret: e.target.value })}
placeholder={currentAccountMeta?.has_api_secret ? '已配置(******),如需更换请重新填写' : '粘贴你的 Binance API SECRET'}
/>
</label>
<label className="accounts-inline">
<span>测试网</span>
<input
type="checkbox"
checked={!!credForm.use_testnet}
onChange={(e) => setCredForm({ ...credForm, use_testnet: e.target.checked })}
/>
</label>
<div className="accounts-form-actions">
<button
type="button"
className="system-btn primary"
disabled={systemBusy}
onClick={async () => {
setSystemBusy(true)
setMessage('')
try {
const payload = {}
if (credForm.api_key) payload.api_key = credForm.api_key
if (credForm.api_secret) payload.api_secret = credForm.api_secret
payload.use_testnet = !!credForm.use_testnet
await api.updateAccountCredentials(accountId, payload)
setMessage('密钥已更新(建议重启该账号交易进程)')
setCredForm({ api_key: '', api_secret: '', use_testnet: !!credForm.use_testnet })
} catch (e) {
setMessage('更新密钥失败: ' + (e?.message || '未知错误'))
} finally {
setSystemBusy(false)
}
}}
>
保存密钥
</button>
</div>
</div>
</div>
{/* 系统控制:清缓存 / 启停 / 重启supervisor */}
{isAdmin ? (
<div className="system-section">
<div className="system-header">
<h3>系统控制</h3>
<div className="system-status">
<span className={`system-status-badge ${systemStatus?.running ? 'running' : 'stopped'}`}>
{systemStatus?.running ? '运行中' : '未运行'}
</span>
{systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null}
{systemStatus?.program ? <span className="system-status-meta">程序: {systemStatus.program}</span> : null}
{systemStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {systemStatus.meta.requested_at}</span> : null}
</div>
</div>
<div className="system-actions">
<button
type="button"
className="system-btn"
onClick={handleClearCache}
disabled={systemBusy}
title="清理Redis配置缓存并从数据库回灌。切换API Key后建议先点这里再重启交易系统。"
>
清除缓存
</button>
<button
type="button"
className="system-btn"
onClick={handleStopTrading}
disabled={systemBusy || systemStatus?.running === false}
title="通过 supervisorctl 停止交易系统"
>
停止
</button>
<button
type="button"
className="system-btn"
onClick={handleStartTrading}
disabled={systemBusy || systemStatus?.running === true}
title="通过 supervisorctl 启动交易系统"
>
启动
</button>
<button
type="button"
className="system-btn primary"
onClick={handleRestartTrading}
disabled={systemBusy}
title="通过 supervisorctl 重启交易系统建议切换API Key后使用"
>
重启交易系统
</button>
<button
type="button"
className="system-btn primary"
onClick={handleRestartAllTrading}
disabled={systemBusy}
title="批量重启所有账号交易进程auto_sys_acc*),用于代码升级后统一生效"
>
重启所有账号交易
</button>
<button
type="button"
className="system-btn primary"
onClick={handleRestartBackend}
disabled={systemBusy}
title="通过 backend/restart.sh 重启后端uvicorn。重启期间接口会短暂不可用。"
>
重启后端服务
</button>
</div>
<div className="system-status" style={{ marginTop: '10px' }}>
<span className={`system-status-badge ${backendStatus?.running ? 'running' : 'stopped'}`}>
后端 {backendStatus?.running ? '运行中' : '未知'}
</span>
{backendStatus?.pid ? <span className="system-status-meta">PID: {backendStatus.pid}</span> : null}
{backendStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {backendStatus.meta.requested_at}</span> : null}
</div>
<div className="system-hint">
建议流程先更新配置里的 Key 点击清除缓存 点击重启交易系统确保不再使用旧账号下单
</div>
</div>
) : null}
{/* 账号管理(超管) */}
{isAdmin ? (
<div className="accounts-admin-section">
<div className="accounts-admin-header">
<h3>账号管理多账号</h3>
<div className="accounts-admin-actions">
<button
type="button"
className="system-btn"
onClick={async () => {
setAccountsBusy(true)
try {
await loadAccountsAdmin()
setShowAccountsAdmin((v) => !v)
} finally {
setAccountsBusy(false)
}
}}
disabled={accountsBusy}
title="创建/禁用账号;为每个账号配置独立 API KEY/SECRET交易/配置/统计会按账号隔离"
>
{showAccountsAdmin ? '收起' : '管理账号'}
</button>
<button
type="button"
className="system-btn"
onClick={async () => {
setAccountsBusy(true)
try {
await loadAccountsAdmin()
notifyAccountsUpdated()
setMessage('账号列表已刷新')
} finally {
setAccountsBusy(false)
}
}}
disabled={accountsBusy}
>
刷新
</button>
</div>
</div>
{showAccountsAdmin ? (
<div className="accounts-admin-body">
<div className="accounts-admin-card">
<div className="accounts-admin-card-title">新增账号</div>
<div className="accounts-form">
<label>
名称
<input
type="text"
value={newAccount.name}
onChange={(e) => setNewAccount({ ...newAccount, name: e.target.value })}
placeholder="例如user_a"
/>
</label>
<label>
API KEY
<input
type="password"
value={newAccount.api_key}
onChange={(e) => setNewAccount({ ...newAccount, api_key: e.target.value })}
placeholder="可先留空,后续再填"
/>
</label>
<label>
API SECRET
<input
type="password"
value={newAccount.api_secret}
onChange={(e) => setNewAccount({ ...newAccount, api_secret: e.target.value })}
placeholder="可先留空,后续再填"
/>
</label>
<label className="accounts-inline">
<span>测试网</span>
<input
type="checkbox"
checked={!!newAccount.use_testnet}
onChange={(e) => setNewAccount({ ...newAccount, use_testnet: e.target.checked })}
/>
</label>
<label>
状态
<select
value={newAccount.status}
onChange={(e) => setNewAccount({ ...newAccount, status: e.target.value })}
>
<option value="active">启用</option>
<option value="disabled">禁用</option>
</select>
</label>
<div className="accounts-form-actions">
<button
type="button"
className="system-btn primary"
disabled={accountsBusy || !newAccount.name.trim()}
onClick={async () => {
setAccountsBusy(true)
setMessage('')
try {
await api.createAccount(newAccount)
setMessage('账号已创建')
setNewAccount({ name: '', api_key: '', api_secret: '', use_testnet: false, status: 'active' })
await loadAccountsAdmin()
notifyAccountsUpdated()
} catch (e) {
setMessage('创建账号失败: ' + (e?.message || '未知错误'))
} finally {
setAccountsBusy(false)
}
}}
>
创建账号
</button>
</div>
</div>
</div>
<div className="accounts-admin-card">
<div className="accounts-admin-card-title">账号列表</div>
<div className="accounts-table">
{(accountsAdmin || []).length ? (
<table>
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>状态</th>
<th>测试网</th>
<th>API KEY</th>
<th>SECRET</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{accountsAdmin.map((a) => (
<tr key={a.id}>
<td>#{a.id}</td>
<td>{a.name || '-'}</td>
<td>
<span className={`acct-badge ${a.status === 'active' ? 'ok' : 'off'}`}>
{a.status === 'active' ? '启用' : '禁用'}
</span>
</td>
<td>{a.use_testnet ? '是' : '否'}</td>
<td>{a.api_key_masked || (a.has_api_key ? '已配置' : '未配置')}</td>
<td>{a.has_api_secret ? '已配置' : '未配置'}</td>
<td className="accounts-actions-cell">
<button
type="button"
className="system-btn"
disabled={accountsBusy || a.id === 1}
title={a.id === 1 ? '默认账号建议保留' : '切换启用/禁用'}
onClick={async () => {
setAccountsBusy(true)
setMessage('')
try {
const next = a.status === 'active' ? 'disabled' : 'active'
await api.updateAccount(a.id, { status: next })
await loadAccountsAdmin()
notifyAccountsUpdated()
setMessage(`账号 #${a.id}${next === 'active' ? '启用' : '禁用'}`)
} catch (e) {
setMessage('更新账号失败: ' + (e?.message || '未知错误'))
} finally {
setAccountsBusy(false)
}
}}
>
{a.status === 'active' ? '禁用' : '启用'}
</button>
<button
type="button"
className="system-btn"
disabled={accountsBusy}
onClick={() => {
setCredEditId(a.id)
setCredForm({ api_key: '', api_secret: '', use_testnet: !!a.use_testnet })
}}
>
更新密钥
</button>
</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="accounts-empty">暂无账号默认账号 #1 会自动存在</div>
)}
</div>
</div>
{credEditId ? (
<div className="accounts-admin-card">
<div className="accounts-admin-card-title">更新账号 #{credEditId} 的密钥</div>
<div className="accounts-form">
<label>
API KEY留空=不改
<input
type="password"
value={credForm.api_key}
onChange={(e) => setCredForm({ ...credForm, api_key: e.target.value })}
/>
</label>
<label>
API SECRET留空=不改
<input
type="password"
value={credForm.api_secret}
onChange={(e) => setCredForm({ ...credForm, api_secret: e.target.value })}
/>
</label>
<label className="accounts-inline">
<span>测试网</span>
<input
type="checkbox"
checked={!!credForm.use_testnet}
onChange={(e) => setCredForm({ ...credForm, use_testnet: e.target.checked })}
/>
</label>
<div className="accounts-form-actions">
<button
type="button"
className="system-btn"
disabled={accountsBusy}
onClick={() => setCredEditId(null)}
>
取消
</button>
<button
type="button"
className="system-btn primary"
disabled={accountsBusy}
onClick={async () => {
setAccountsBusy(true)
setMessage('')
try {
const payload = {}
if (credForm.api_key) payload.api_key = credForm.api_key
if (credForm.api_secret) payload.api_secret = credForm.api_secret
payload.use_testnet = !!credForm.use_testnet
await api.updateAccountCredentials(credEditId, payload)
setMessage(`账号 #${credEditId} 密钥已更新(建议重启该账号交易进程)`)
setCredEditId(null)
await loadAccountsAdmin()
notifyAccountsUpdated()
} catch (e) {
setMessage('更新密钥失败: ' + (e?.message || '未知错误'))
} finally {
setAccountsBusy(false)
}
}}
>
保存
</button>
</div>
</div>
</div>
) : null}
</div>
) : null}
</div>
) : null}
{/* 预设方案快速切换(仅管理员 + 全局策略账号:策略核心统一管理) */}
{isAdmin && isGlobalStrategyAccount ? (
<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-guide">
<div className="preset-guide-title">怎么选更不迷糊</div>
<ul className="preset-guide-list">
<li>
<strong>先选入场机制</strong> vs
</li>
<li>
<strong>再看会不会下单</strong> <code>AUTO_TRADE_ONLY_TRENDING</code> <code>AUTO_TRADE_ALLOW_4H_NEUTRAL</code>
</li>
<li>
<strong>最后再微调</strong> <code>LIMIT_ORDER_OFFSET_PCT</code> <code>ENTRY_CONFIRM_TIMEOUT_SEC</code>
</li>
</ul>
</div>
{(() => {
const presetUiMeta = {
swing: { group: 'limit', tag: '纯限价' },
strict: { group: 'limit', tag: '纯限价' },
fill: { group: 'smart', tag: '智能入场' },
steady: { group: 'smart', tag: '智能入场' },
conservative: { group: 'legacy', tag: '传统' },
balanced: { group: 'legacy', tag: '传统' },
aggressive: { group: 'legacy', tag: '高频实验' },
}
const groups = [
{
key: 'limit',
title: 'A. 纯限价SMART_ENTRY_ENABLED=false',
desc: '只下 1 次限价单,未在确认时间内成交就撤单跳过。更控频、更接近“波段”,但更容易出现 NEW→撤单。',
presetKeys: ['swing', 'strict'],
},
{
key: 'smart',
title: 'B. 智能入场SMART_ENTRY_ENABLED=true',
desc: '限价回调 + 受限追价 +(趋势强时)可控市价兜底。更少漏单,但必须限制追价步数与偏离上限,避免回到高频追价。',
presetKeys: ['fill', 'steady'],
},
{
key: 'legacy',
title: 'C. 传统 / 实验(不建议长期)',
desc: '这组更多用于对比或临时实验(频率更高/更容易过度交易),建议在稳定盈利前谨慎使用。',
presetKeys: ['conservative', 'balanced', 'aggressive'],
},
]
return (
<div className="preset-groups">
{groups.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[k])
.map((k) => {
const preset = presets[k]
const meta = presetUiMeta[k] || { group: g.key, tag: '' }
return (
<button
key={k}
className={`preset-btn ${currentPreset === k ? 'active' : ''}`}
onClick={() => 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>
)
})}
</div>
</div>
))}
</div>
)
})()}
</div>
) : (
<div className="system-hint" style={{ marginTop: '12px' }}>
平台已开启傻瓜化模式策略核心由管理员统一管理你只需要配置密钥充值余额并调整少量风控参数如最小/最大仓位每日开仓次数等
</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.base_leverage || feasibilityCheck.leverage || 'N/A'}x</strong>
{feasibilityCheck.use_dynamic_leverage && feasibilityCheck.max_leverage && (
<> | 最大杠杆: <strong>{feasibilityCheck.max_leverage}x</strong></>
)} |
最小保证金: <strong>{feasibilityCheck.current_config?.min_margin_usdt?.toFixed(2) || 'N/A'}</strong> USDT
</p>
{!feasibilityCheck.feasible && (
<div className="warning-text">
<p>
需要保证金占比: <strong>{feasibilityCheck.calculated_values?.required_position_percent?.toFixed(1)}%</strong> |
最大允许保证金占比: <strong>{feasibilityCheck.calculated_values?.max_allowed_position_percent?.toFixed(1)}%</strong>
</p>
{feasibilityCheck.calculated_values?.actual_min_margin !== undefined && (
<p>
最小保证金占比可提供保证金: <strong>{feasibilityCheck.calculated_values.actual_min_margin.toFixed(2)} USDT</strong>
(MIN_POSITION_PERCENT={feasibilityCheck.calculated_values.min_position_percent?.toFixed(1)}%) |
最小保证金要求: <strong>{feasibilityCheck.current_config?.min_margin_usdt?.toFixed(2)} USDT</strong>
</p>
)}
</div>
)}
</div>
{feasibilityCheck.suggestions && feasibilityCheck.suggestions.length > 0 && (
<div className="feasibility-suggestions">
<h5>建议方案</h5>
{feasibilityCheck.suggestions.map((suggestion, index) => {
// 如果是杠杆分析结果,显示详细表格
if (suggestion.type === 'leverage_analysis' && suggestion.leverage_results) {
return (
<div key={index} className="suggestion-item leverage-analysis">
<div className="suggestion-header">
<strong>{suggestion.title}</strong>
</div>
<div className="leverage-table">
<table>
<thead>
<tr>
<th>杠杆倍数</th>
<th>需要保证金(USDT)</th>
<th>需要保证金%</th>
<th>实际最小保证金</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{suggestion.leverage_results.map((result, idx) => (
<tr key={idx} className={result.feasible ? 'feasible-row' : 'infeasible-row'}>
<td><strong>{result.leverage}x</strong></td>
<td>{result.required_position_value.toFixed(2)} USDT</td>
<td>{result.required_position_percent.toFixed(1)}%</td>
<td>{result.actual_min_margin.toFixed(2)} USDT</td>
<td>
<span className={result.feasible ? 'status-ok' : 'status-error'}>
{result.feasible ? '✓ 可行' : '✗ 不可行'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
// 普通建议
return (
<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]) => {
if (config.category !== category) return false
// 密钥相关配置统一在“账号密钥(当前账号)”模块管理,这里不重复展示
if (key === 'BINANCE_API_KEY' || key === 'BINANCE_API_SECRET' || key === 'USE_TESTNET') return false
return true
})
.map(([key, config]) => (
<ConfigItem
key={key}
label={key}
config={config}
onUpdate={(value) => handleUpdate(key, value, config.type, config.category)}
disabled={saving}
/>
))}
</div>
</section>
))}
{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>
)
}
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',
])
const isPctLike = PCT_LIKE_KEYS.has(label)
const isRatioPercentKey = isPercentKey && !isPctLike
const formatPercent = (n) => {
// 最多 4 位小数,去掉尾随 0例如 2.3000 -> 2.3
if (typeof n !== 'number' || isNaN(n)) return ''
return n.toFixed(4).replace(/\.?0+$/, '')
}
// 初始化显示值:百分比配置转为“百分比数值”(支持小数)
const getInitialDisplayValue = (val) => {
if (config.type === 'number' && isPercentKey) {
if (val === null || val === undefined || val === '') {
return ''
}
const numVal = typeof val === 'string' ? parseFloat(val) : val
if (isNaN(numVal)) {
return ''
}
// 两类:
// 1) 常规比例型(如 STOP_LOSS_PERCENT=0.08):展示为 8%
// 2) pct-like如 LIMIT_ORDER_OFFSET_PCT=0.5 表示0.5%):展示为 0.5%
if (isPctLike) {
const pctNum = numVal <= 0.05 ? numVal * 100 : numVal
return formatPercent(pctNum)
}
const percent = numVal <= 1 ? numVal * 100 : numVal
return formatPercent(percent)
}
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
}
// 处理字符串形式的数字输入(包括部分输入如 "0." 或 "."
let numValue
if (typeof localValue === 'string') {
// 如果字符串以 "." 结尾(如 "0." 或 "."),尝试补全为有效数字
if (localValue.endsWith('.') && localValue.length > 1) {
// "0." -> 0, "1." -> 1
numValue = parseFloat(localValue)
} else if (localValue === '.' || localValue === '-.') {
// "." 或 "-." 视为无效,恢复原值
const restoreValue = isPercentKey
? (typeof value === 'number' ? formatPercent((value <= 1 ? value * 100 : value)) : value)
: value
setLocalValue(restoreValue)
return
} else {
numValue = parseFloat(localValue)
}
} else {
numValue = localValue
}
if (isNaN(numValue)) {
// 如果输入无效,恢复原值
const restoreValue = isPercentKey
? (typeof value === 'number' ? formatPercent((value <= 1 ? value * 100 : value)) : value)
: value
setLocalValue(restoreValue)
return
}
processedValue = numValue
// 百分比配置
if (isPercentKey) {
if (isPctLike) {
// pct-like值本身就是“百分比数值”<=1表示<=1%),不做 /100
if (processedValue < 0 || processedValue > 1) {
setLocalValue(getInitialDisplayValue(value))
return
}
} else {
// 常规比例型前端按“百分比”输入0~100存储为 0~1
if (processedValue < 0 || processedValue > 100) {
setLocalValue(getInitialDisplayValue(value))
return
}
processedValue = processedValue / 100
}
}
} 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()
}
}
const displayValue = localValue === null || localValue === undefined ? '' : 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="text"
inputMode={config.type === 'number' ? 'decimal' : 'text'}
value={displayValue === '' ? '' : String(displayValue)}
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 (isPercentKey) {
const numValue = parseFloat(newValue)
const maxAllowed = isPctLike ? 1 : 100
if (newValue !== '' && !isNaN(numValue) && (numValue < 0 || numValue > maxAllowed)) {
// 超出范围,不更新
return
}
}
}
// 更新本地值
if (config.type === 'number' && isPercentKey) {
// 百分比配置:允许字符串中间态(如 "2."),并支持小数
if (newValue === '') {
handleChange('')
} else {
const validPattern = /^-?\d*\.?\d*$/
if (validPattern.test(newValue)) {
handleChange(newValue)
}
}
} else if (config.type === 'number') {
// 其他数字配置:允许字符串形式的中间状态(如 "0." 或 "."),支持小数输入
if (newValue === '') {
handleChange('')
} else {
// 允许部分输入状态(如 "0.", ".5", "-" 等)
// 使用正则验证是否为有效的数字格式(包括部分输入)
const validPattern = /^-?\d*\.?\d*$/
if (validPattern.test(newValue)) {
// 如果是有效的数字格式(包括部分输入),直接保存字符串
handleChange(newValue)
}
// 如果无效,不更新(保持原值)
}
} else {
handleChange(newValue)
}
}}
onBlur={handleBlur}
onKeyPress={handleKeyPress}
onKeyDown={(e) => {
// 允许删除、退格、方向键等
if (['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {
return
}
// 对于数字输入,只允许数字、小数点和负号
if (config.type === 'number') {
const currentValue = String(localValue || '')
// 如果已经包含小数点,不允许再次输入小数点
if (e.key === '.' && currentValue.includes('.')) {
e.preventDefault()
return
}
// 如果已经包含负号,不允许再次输入负号(除非是删除操作)
if (e.key === '-' && currentValue.includes('-')) {
e.preventDefault()
return
}
const validKeys = /^[0-9.-]$/
if (!validKeys.test(e.key) && !['Enter', 'Escape'].includes(e.key)) {
e.preventDefault()
}
}
}}
disabled={disabled}
className={isEditing ? 'editing' : ''}
placeholder={isPercentKey ? (isPctLike ? '输入0~1表示0%~1%' : '输入百分比(可小数)') : '输入数值'}
/>
{isPercentKey && <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': '扫描间隔。系统每隔多长时间扫描一次市场寻找交易机会。值越小扫描越频繁能更快捕捉波动但更容易产生噪音与过度交易。建议低频波段1800秒(30分钟)或更长;稳健/保守可3600秒(1小时);高频不建议长期使用。',
'MIN_CHANGE_PERCENT': '最小涨跌幅阈值(%。只有24小时涨跌幅达到此值的交易对才会被考虑交易。值越小捕捉机会越多但可能包含噪音和假信号。值越大只捕捉大幅波动信号质量更高但机会更少。建议保守策略2.0-3.0%平衡策略1.5-2.0%激进策略1.0-1.5%。',
'MIN_SIGNAL_STRENGTH': '最小信号强度0-10。技术指标综合评分只有达到此强度的信号才会执行交易。值越小机会越多但噪音更大值越大更偏“精选高质量”。建议低频波段建议≥8一般保守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.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.02表示2%。系统会先按该比例分配“保证金”再根据杠杆换算名义价值下单。值越小越保守适合回归波段交易、降低单笔波动对账户的伤害。注意币安合约有最小名义价值要求通常≥5 USDT如果保证金过小且杠杆不够系统会提示无法满足最小名义价值。',
'MAX_TOTAL_POSITION_PERCENT': '总保证金上限账户余额的百分比如0.20表示20%)。所有持仓占用的保证金之和不能超过该上限,防止过度分散和风险集中。值越小越易控仓,便于管理持仓数量与节奏。',
'MIN_POSITION_PERCENT': '单笔最小保证金占用账户余额的百分比如0.01表示1%。用于避免过小仓位意义不大、易受手续费影响。如果你希望取消这一限制可设置为0等价于关闭最小占比。',
// 风险控制参数
'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~1前端输入按%显示支持小数如1.5%。用于防止止损过紧即使基于保证金的止损更紧也会使用至少该百分比的价格变动。建议1.5%-3%。',
'MIN_TAKE_PROFIT_PRICE_PCT': '最小止盈价格变动百分比存储为0~1前端输入按%显示支持小数如2.5%。用于防止止盈过紧即使基于保证金的止盈更紧也会使用至少该百分比的价格变动。建议2%-4%。',
// 策略参数
'LEVERAGE': '交易杠杆倍数。放大资金利用率同时放大收益和风险。杠杆越高相同仓位下需要的保证金越少但风险越大。建议保守策略5-10倍平衡策略10倍激进策略10-15倍。注意高杠杆会增加爆仓风险请谨慎使用。',
'USE_TRAILING_STOP': '是否启用移动止损true/false。启用后当盈利达到激活阈值时止损会自动跟踪价格保护利润。适合趋势行情可以捕捉更大的利润空间。建议平衡和激进策略启用保守策略可关闭。',
'SMART_ENTRY_ENABLED': '智能入场开关true/false。开启时会进行“限价回调 + 追价 +(趋势强时)市价兜底”,以减少错过;关闭时回归“纯限价单模式”:只下一次限价单,未在确认时间内成交则撤单跳过,更适合低频波段与控频。',
'AUTO_TRADE_ONLY_TRENDING': '自动交易仅在市场状态=trending时执行ranging/unknown只生成推荐不自动下单。开启能显著降低震荡扫损但也会减少出单如果你发现“几乎不下单”可先关闭。',
'AUTO_TRADE_ALLOW_4H_NEUTRAL': '是否允许4H趋势=neutral时自动交易。关闭可减少震荡扫损开启会显著增加出单但需要配合更严格的信号门槛/控频)。',
'LIMIT_ORDER_OFFSET_PCT': '限价入场偏移(%。BUY 会挂在当前价下方回调位置SELL 会挂在当前价上方反弹位置。值越小越贴近当前价更容易成交但可能更“追价”值越大更保守但更容易出现“挂单NEW→超时撤单”。建议0.05%-0.20%。',
'ENTRY_CONFIRM_TIMEOUT_SEC': '下单后等待成交的确认超时(秒)。在“纯限价模式”下,超时未成交会撤单并跳过;在“智能入场”下则进入后续追价/兜底流程。建议60-180秒。',
'ENTRY_CHASE_MAX_STEPS': '智能入场最大追价步数。步数越多越不容易错过,但也更接近追价;建议 2-4 步。',
'ENTRY_STEP_WAIT_SEC': '智能入场每一步等待成交时间(秒)。过短会频繁撤改单,过长会错过行情;建议 15-25 秒。',
'ENTRY_MARKET_FALLBACK_AFTER_SEC': '趋势强时市价兜底延迟(秒)。用于减少错过(在追价上限内才会触发),建议 45-90 秒。',
'ENTRY_MAX_DRIFT_PCT_TRENDING': '趋势强时最大追价偏离(%)。例如 0.3 表示 0.3%。越小越保守、越不追价;建议 0.2%-0.6%。',
'ENTRY_MAX_DRIFT_PCT_RANGING': '震荡/弱趋势最大追价偏离(%)。例如 0.15 表示 0.15%。越小越保守;建议 0.1%-0.3%。',
'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