a
This commit is contained in:
parent
11e6361235
commit
c28400e51e
|
|
@ -47,7 +47,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
const [snapshotText, setSnapshotText] = useState('')
|
const [snapshotText, setSnapshotText] = useState('')
|
||||||
const [snapshotIncludeSecrets, setSnapshotIncludeSecrets] = useState(false)
|
const [snapshotIncludeSecrets, setSnapshotIncludeSecrets] = useState(false)
|
||||||
const [snapshotBusy, setSnapshotBusy] = useState(false)
|
const [snapshotBusy, setSnapshotBusy] = useState(false)
|
||||||
|
|
||||||
// 预设方案配置
|
// 预设方案配置
|
||||||
// 注意:百分比配置使用整数形式(如8.0表示8%),在应用时会转换为小数(0.08)
|
// 注意:百分比配置使用整数形式(如8.0表示8%),在应用时会转换为小数(0.08)
|
||||||
const presets = {
|
const presets = {
|
||||||
|
|
@ -351,12 +351,16 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadConfigs()
|
loadConfigs()
|
||||||
checkFeasibility()
|
checkFeasibility()
|
||||||
loadSystemStatus()
|
if (isAdmin) {
|
||||||
|
loadSystemStatus()
|
||||||
|
}
|
||||||
loadBackendStatus()
|
loadBackendStatus()
|
||||||
loadAccountTradingStatus()
|
loadAccountTradingStatus()
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
loadSystemStatus()
|
if (isAdmin) {
|
||||||
|
loadSystemStatus()
|
||||||
|
}
|
||||||
loadBackendStatus()
|
loadBackendStatus()
|
||||||
loadAccountTradingStatus()
|
loadAccountTradingStatus()
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
|
@ -401,7 +405,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
}, 1000)
|
}, 1000)
|
||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
}, [accountId])
|
}, [accountId])
|
||||||
|
|
||||||
const checkFeasibility = async () => {
|
const checkFeasibility = async () => {
|
||||||
setCheckingFeasibility(true)
|
setCheckingFeasibility(true)
|
||||||
try {
|
try {
|
||||||
|
|
@ -525,21 +529,21 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
preset_detected: detectCurrentPreset(),
|
preset_detected: detectCurrentPreset(),
|
||||||
system_status: systemStatus
|
system_status: systemStatus
|
||||||
? {
|
? {
|
||||||
running: !!systemStatus.running,
|
running: !!systemStatus.running,
|
||||||
pid: systemStatus.pid || null,
|
pid: systemStatus.pid || null,
|
||||||
program: systemStatus.program || null,
|
program: systemStatus.program || null,
|
||||||
state: systemStatus.state || null,
|
state: systemStatus.state || null,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
feasibility_check_summary: feasibilityCheck
|
feasibility_check_summary: feasibilityCheck
|
||||||
? {
|
? {
|
||||||
feasible: !!feasibilityCheck.feasible,
|
feasible: !!feasibilityCheck.feasible,
|
||||||
account_balance: feasibilityCheck.account_balance ?? null,
|
account_balance: feasibilityCheck.account_balance ?? null,
|
||||||
base_leverage: feasibilityCheck.base_leverage ?? feasibilityCheck.leverage ?? null,
|
base_leverage: feasibilityCheck.base_leverage ?? feasibilityCheck.leverage ?? null,
|
||||||
max_leverage: feasibilityCheck.max_leverage ?? null,
|
max_leverage: feasibilityCheck.max_leverage ?? null,
|
||||||
use_dynamic_leverage: feasibilityCheck.use_dynamic_leverage ?? null,
|
use_dynamic_leverage: feasibilityCheck.use_dynamic_leverage ?? null,
|
||||||
min_margin_usdt: feasibilityCheck.current_config?.min_margin_usdt ?? null,
|
min_margin_usdt: feasibilityCheck.current_config?.min_margin_usdt ?? null,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
configs: entries,
|
configs: entries,
|
||||||
}
|
}
|
||||||
|
|
@ -593,7 +597,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
// 检测当前配置匹配哪个预设方案
|
// 检测当前配置匹配哪个预设方案
|
||||||
const detectCurrentPreset = () => {
|
const detectCurrentPreset = () => {
|
||||||
if (!configs || Object.keys(configs).length === 0) return null
|
if (!configs || Object.keys(configs).length === 0) return null
|
||||||
|
|
||||||
for (const [presetKey, preset] of Object.entries(presets)) {
|
for (const [presetKey, preset] of Object.entries(presets)) {
|
||||||
let match = true
|
let match = true
|
||||||
for (const [key, expectedValue] of Object.entries(preset.configs)) {
|
for (const [key, expectedValue] of Object.entries(preset.configs)) {
|
||||||
|
|
@ -602,7 +606,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
match = false
|
match = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前值(处理百分比转换)
|
// 获取当前值(处理百分比转换)
|
||||||
let currentValue = currentConfig.value
|
let currentValue = currentConfig.value
|
||||||
if (key.includes('PERCENT') || key.includes('PCT')) {
|
if (key.includes('PERCENT') || key.includes('PCT')) {
|
||||||
|
|
@ -613,7 +617,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
currentValue = currentValue * 100
|
currentValue = currentValue * 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 比较值(允许小的浮点数误差)
|
// 比较值(允许小的浮点数误差)
|
||||||
if (typeof expectedValue === 'number' && typeof currentValue === 'number') {
|
if (typeof expectedValue === 'number' && typeof currentValue === 'number') {
|
||||||
if (Math.abs(currentValue - expectedValue) > 0.01) {
|
if (Math.abs(currentValue - expectedValue) > 0.01) {
|
||||||
|
|
@ -625,19 +629,19 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
return presetKey
|
return presetKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyPreset = async (presetKey) => {
|
const applyPreset = async (presetKey) => {
|
||||||
const preset = presets[presetKey]
|
const preset = presets[presetKey]
|
||||||
if (!preset) return
|
if (!preset) return
|
||||||
|
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
setMessage('')
|
setMessage('')
|
||||||
try {
|
try {
|
||||||
|
|
@ -677,7 +681,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
type = 'number'
|
type = 'number'
|
||||||
category = 'scan'
|
category = 'scan'
|
||||||
}
|
}
|
||||||
|
|
||||||
const detail = typeof getConfigDetail === 'function' ? getConfigDetail(key) : ''
|
const detail = typeof getConfigDetail === 'function' ? getConfigDetail(key) : ''
|
||||||
const desc =
|
const desc =
|
||||||
detail && typeof detail === 'string' && !detail.includes('暂无详细说明')
|
detail && typeof detail === 'string' && !detail.includes('暂无详细说明')
|
||||||
|
|
@ -706,7 +710,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
description: config.description
|
description: config.description
|
||||||
}
|
}
|
||||||
}).filter(Boolean)
|
}).filter(Boolean)
|
||||||
|
|
||||||
const response = await api.updateConfigsBatch(configItems)
|
const response = await api.updateConfigsBatch(configItems)
|
||||||
setMessage(response.message || `已应用${preset.name}`)
|
setMessage(response.message || `已应用${preset.name}`)
|
||||||
if (response.note) {
|
if (response.note) {
|
||||||
|
|
@ -795,345 +799,345 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
|
|
||||||
{/* 系统控制:清缓存 / 启停 / 重启(supervisor) */}
|
{/* 系统控制:清缓存 / 启停 / 重启(supervisor) */}
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<div className="system-section">
|
<div className="system-section">
|
||||||
<div className="system-header">
|
<div className="system-header">
|
||||||
<h3>系统控制</h3>
|
<h3>系统控制</h3>
|
||||||
<div className="system-status">
|
<div className="system-status">
|
||||||
<span className={`system-status-badge ${systemStatus?.running ? 'running' : 'stopped'}`}>
|
<span className={`system-status-badge ${systemStatus?.running ? 'running' : 'stopped'}`}>
|
||||||
{systemStatus?.running ? '运行中' : '未运行'}
|
{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={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>
|
</span>
|
||||||
{systemStatus?.pid ? <span className="system-status-meta">PID: {systemStatus.pid}</span> : null}
|
{backendStatus?.pid ? <span className="system-status-meta">PID: {backendStatus.pid}</span> : null}
|
||||||
{systemStatus?.program ? <span className="system-status-meta">程序: {systemStatus.program}</span> : null}
|
{backendStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {backendStatus.meta.requested_at}</span> : null}
|
||||||
{systemStatus?.meta?.requested_at ? <span className="system-status-meta">上次重启: {systemStatus.meta.requested_at}</span> : null}
|
</div>
|
||||||
|
<div className="system-hint">
|
||||||
|
建议流程:先更新配置里的 Key → 点击“清除缓存” → 点击“重启交易系统”,确保不再使用旧账号下单。
|
||||||
</div>
|
</div>
|
||||||
</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={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}
|
) : null}
|
||||||
|
|
||||||
{/* 账号管理(超管) */}
|
{/* 账号管理(超管) */}
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<div className="accounts-admin-section">
|
<div className="accounts-admin-section">
|
||||||
<div className="accounts-admin-header">
|
<div className="accounts-admin-header">
|
||||||
<h3>账号管理(多账号)</h3>
|
<h3>账号管理(多账号)</h3>
|
||||||
<div className="accounts-admin-actions">
|
<div className="accounts-admin-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="system-btn"
|
className="system-btn"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setAccountsBusy(true)
|
setAccountsBusy(true)
|
||||||
try {
|
try {
|
||||||
await loadAccountsAdmin()
|
await loadAccountsAdmin()
|
||||||
setShowAccountsAdmin((v) => !v)
|
setShowAccountsAdmin((v) => !v)
|
||||||
} finally {
|
} finally {
|
||||||
setAccountsBusy(false)
|
setAccountsBusy(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={accountsBusy}
|
disabled={accountsBusy}
|
||||||
title="创建/禁用账号;为每个账号配置独立 API KEY/SECRET;交易/配置/统计会按账号隔离"
|
title="创建/禁用账号;为每个账号配置独立 API KEY/SECRET;交易/配置/统计会按账号隔离"
|
||||||
>
|
>
|
||||||
{showAccountsAdmin ? '收起' : '管理账号'}
|
{showAccountsAdmin ? '收起' : '管理账号'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="system-btn"
|
className="system-btn"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setAccountsBusy(true)
|
setAccountsBusy(true)
|
||||||
try {
|
try {
|
||||||
await loadAccountsAdmin()
|
await loadAccountsAdmin()
|
||||||
notifyAccountsUpdated()
|
notifyAccountsUpdated()
|
||||||
setMessage('账号列表已刷新')
|
setMessage('账号列表已刷新')
|
||||||
} finally {
|
} finally {
|
||||||
setAccountsBusy(false)
|
setAccountsBusy(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={accountsBusy}
|
disabled={accountsBusy}
|
||||||
>
|
>
|
||||||
刷新
|
刷新
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{showAccountsAdmin ? (
|
{showAccountsAdmin ? (
|
||||||
<div className="accounts-admin-body">
|
<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">
|
||||||
<div className="accounts-admin-card-title">更新账号 #{credEditId} 的密钥</div>
|
<div className="accounts-admin-card-title">新增账号</div>
|
||||||
<div className="accounts-form">
|
<div className="accounts-form">
|
||||||
<label>
|
<label>
|
||||||
API KEY(留空=不改)
|
名称
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="text"
|
||||||
value={credForm.api_key}
|
value={newAccount.name}
|
||||||
onChange={(e) => setCredForm({ ...credForm, api_key: e.target.value })}
|
onChange={(e) => setNewAccount({ ...newAccount, name: e.target.value })}
|
||||||
|
placeholder="例如:user_a"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
API SECRET(留空=不改)
|
API KEY
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={credForm.api_secret}
|
value={newAccount.api_key}
|
||||||
onChange={(e) => setCredForm({ ...credForm, api_secret: e.target.value })}
|
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>
|
||||||
<label className="accounts-inline">
|
<label className="accounts-inline">
|
||||||
<span>测试网</span>
|
<span>测试网</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={!!credForm.use_testnet}
|
checked={!!newAccount.use_testnet}
|
||||||
onChange={(e) => setCredForm({ ...credForm, use_testnet: e.target.checked })}
|
onChange={(e) => setNewAccount({ ...newAccount, use_testnet: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
</label>
|
</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">
|
<div className="accounts-form-actions">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="system-btn"
|
|
||||||
disabled={accountsBusy}
|
|
||||||
onClick={() => setCredEditId(null)}
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="system-btn primary"
|
className="system-btn primary"
|
||||||
disabled={accountsBusy}
|
disabled={accountsBusy || !newAccount.name.trim()}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setAccountsBusy(true)
|
setAccountsBusy(true)
|
||||||
setMessage('')
|
setMessage('')
|
||||||
try {
|
try {
|
||||||
const payload = {}
|
await api.createAccount(newAccount)
|
||||||
if (credForm.api_key) payload.api_key = credForm.api_key
|
setMessage('账号已创建')
|
||||||
if (credForm.api_secret) payload.api_secret = credForm.api_secret
|
setNewAccount({ name: '', api_key: '', api_secret: '', use_testnet: false, status: 'active' })
|
||||||
payload.use_testnet = !!credForm.use_testnet
|
|
||||||
await api.updateAccountCredentials(credEditId, payload)
|
|
||||||
setMessage(`账号 #${credEditId} 密钥已更新(建议重启该账号交易进程)`)
|
|
||||||
setCredEditId(null)
|
|
||||||
await loadAccountsAdmin()
|
await loadAccountsAdmin()
|
||||||
notifyAccountsUpdated()
|
notifyAccountsUpdated()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setMessage('更新密钥失败: ' + (e?.message || '未知错误'))
|
setMessage('创建账号失败: ' + (e?.message || '未知错误'))
|
||||||
} finally {
|
} finally {
|
||||||
setAccountsBusy(false)
|
setAccountsBusy(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
保存
|
创建账号
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
</div>
|
<div className="accounts-admin-card">
|
||||||
) : null}
|
<div className="accounts-admin-card-title">账号列表</div>
|
||||||
</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}
|
) : null}
|
||||||
|
|
||||||
{/* 预设方案快速切换 */}
|
{/* 预设方案快速切换 */}
|
||||||
<div className="preset-section">
|
<div className="preset-section">
|
||||||
<div className="preset-header">
|
<div className="preset-header">
|
||||||
|
|
@ -1233,7 +1237,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 配置可行性检查提示 */}
|
{/* 配置可行性检查提示 */}
|
||||||
{feasibilityCheck && (
|
{feasibilityCheck && (
|
||||||
<div className={`feasibility-check ${feasibilityCheck.feasible ? 'feasible' : 'infeasible'}`}>
|
<div className={`feasibility-check ${feasibilityCheck.feasible ? 'feasible' : 'infeasible'}`}>
|
||||||
|
|
@ -1241,8 +1245,8 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
<h4>
|
<h4>
|
||||||
{feasibilityCheck.feasible ? '✓ 配置可行' : '⚠ 配置冲突'}
|
{feasibilityCheck.feasible ? '✓ 配置可行' : '⚠ 配置冲突'}
|
||||||
</h4>
|
</h4>
|
||||||
<button
|
<button
|
||||||
onClick={checkFeasibility}
|
onClick={checkFeasibility}
|
||||||
disabled={checkingFeasibility}
|
disabled={checkingFeasibility}
|
||||||
className="refresh-btn"
|
className="refresh-btn"
|
||||||
title="重新检查"
|
title="重新检查"
|
||||||
|
|
@ -1252,23 +1256,23 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="feasibility-info">
|
<div className="feasibility-info">
|
||||||
<p>
|
<p>
|
||||||
账户余额: <strong>{feasibilityCheck.account_balance?.toFixed(2) || 'N/A'}</strong> USDT |
|
账户余额: <strong>{feasibilityCheck.account_balance?.toFixed(2) || 'N/A'}</strong> USDT |
|
||||||
基础杠杆: <strong>{feasibilityCheck.base_leverage || feasibilityCheck.leverage || 'N/A'}x</strong>
|
基础杠杆: <strong>{feasibilityCheck.base_leverage || feasibilityCheck.leverage || 'N/A'}x</strong>
|
||||||
{feasibilityCheck.use_dynamic_leverage && feasibilityCheck.max_leverage && (
|
{feasibilityCheck.use_dynamic_leverage && feasibilityCheck.max_leverage && (
|
||||||
<> | 最大杠杆: <strong>{feasibilityCheck.max_leverage}x</strong></>
|
<> | 最大杠杆: <strong>{feasibilityCheck.max_leverage}x</strong></>
|
||||||
)} |
|
)} |
|
||||||
最小保证金: <strong>{feasibilityCheck.current_config?.min_margin_usdt?.toFixed(2) || 'N/A'}</strong> USDT
|
最小保证金: <strong>{feasibilityCheck.current_config?.min_margin_usdt?.toFixed(2) || 'N/A'}</strong> USDT
|
||||||
</p>
|
</p>
|
||||||
{!feasibilityCheck.feasible && (
|
{!feasibilityCheck.feasible && (
|
||||||
<div className="warning-text">
|
<div className="warning-text">
|
||||||
<p>
|
<p>
|
||||||
需要保证金占比: <strong>{feasibilityCheck.calculated_values?.required_position_percent?.toFixed(1)}%</strong> |
|
需要保证金占比: <strong>{feasibilityCheck.calculated_values?.required_position_percent?.toFixed(1)}%</strong> |
|
||||||
最大允许保证金占比: <strong>{feasibilityCheck.calculated_values?.max_allowed_position_percent?.toFixed(1)}%</strong>
|
最大允许保证金占比: <strong>{feasibilityCheck.calculated_values?.max_allowed_position_percent?.toFixed(1)}%</strong>
|
||||||
</p>
|
</p>
|
||||||
{feasibilityCheck.calculated_values?.actual_min_margin !== undefined && (
|
{feasibilityCheck.calculated_values?.actual_min_margin !== undefined && (
|
||||||
<p>
|
<p>
|
||||||
最小保证金占比可提供保证金: <strong>{feasibilityCheck.calculated_values.actual_min_margin.toFixed(2)} USDT</strong>
|
最小保证金占比可提供保证金: <strong>{feasibilityCheck.calculated_values.actual_min_margin.toFixed(2)} USDT</strong>
|
||||||
(MIN_POSITION_PERCENT={feasibilityCheck.calculated_values.min_position_percent?.toFixed(1)}%) |
|
(MIN_POSITION_PERCENT={feasibilityCheck.calculated_values.min_position_percent?.toFixed(1)}%) |
|
||||||
最小保证金要求: <strong>{feasibilityCheck.current_config?.min_margin_usdt?.toFixed(2)} USDT</strong>
|
最小保证金要求: <strong>{feasibilityCheck.current_config?.min_margin_usdt?.toFixed(2)} USDT</strong>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1317,7 +1321,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 普通建议
|
// 普通建议
|
||||||
return (
|
return (
|
||||||
<div key={index} className="suggestion-item">
|
<div key={index} className="suggestion-item">
|
||||||
|
|
@ -1353,13 +1357,13 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{message && (
|
{message && (
|
||||||
<div className={`message ${message.includes('失败') || message.includes('错误') ? 'error' : 'success'}`}>
|
<div className={`message ${message.includes('失败') || message.includes('错误') ? 'error' : 'success'}`}>
|
||||||
{message}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{Object.entries(configCategories).map(([category, label]) => (
|
{Object.entries(configCategories).map(([category, label]) => (
|
||||||
<section key={category} className="config-section">
|
<section key={category} className="config-section">
|
||||||
<h3>{label}</h3>
|
<h3>{label}</h3>
|
||||||
|
|
@ -1465,7 +1469,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
}
|
}
|
||||||
return val === null || val === undefined ? '' : val
|
return val === null || val === undefined ? '' : val
|
||||||
}
|
}
|
||||||
|
|
||||||
const [value, setValue] = useState(config.value)
|
const [value, setValue] = useState(config.value)
|
||||||
const [localValue, setLocalValue] = useState(getInitialDisplayValue(config.value))
|
const [localValue, setLocalValue] = useState(getInitialDisplayValue(config.value))
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
|
@ -1485,7 +1489,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
|
|
||||||
// 处理localValue可能是字符串的情况
|
// 处理localValue可能是字符串的情况
|
||||||
let processedValue = localValue
|
let processedValue = localValue
|
||||||
if (config.type === 'number') {
|
if (config.type === 'number') {
|
||||||
|
|
@ -1494,7 +1498,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
setLocalValue(value)
|
setLocalValue(value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理字符串形式的数字输入(包括部分输入如 "0." 或 ".")
|
// 处理字符串形式的数字输入(包括部分输入如 "0." 或 ".")
|
||||||
let numValue
|
let numValue
|
||||||
if (typeof localValue === 'string') {
|
if (typeof localValue === 'string') {
|
||||||
|
|
@ -1515,7 +1519,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
} else {
|
} else {
|
||||||
numValue = localValue
|
numValue = localValue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNaN(numValue)) {
|
if (isNaN(numValue)) {
|
||||||
// 如果输入无效,恢复原值
|
// 如果输入无效,恢复原值
|
||||||
const restoreValue = isPercentKey
|
const restoreValue = isPercentKey
|
||||||
|
|
@ -1524,7 +1528,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
setLocalValue(restoreValue)
|
setLocalValue(restoreValue)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
processedValue = numValue
|
processedValue = numValue
|
||||||
// 百分比配置
|
// 百分比配置
|
||||||
if (isPercentKey) {
|
if (isPercentKey) {
|
||||||
|
|
@ -1546,7 +1550,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
} else if (config.type === 'boolean') {
|
} else if (config.type === 'boolean') {
|
||||||
processedValue = localValue === 'true' || localValue === true
|
processedValue = localValue === 'true' || localValue === true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只有当值真正发生变化时才保存
|
// 只有当值真正发生变化时才保存
|
||||||
if (processedValue !== value) {
|
if (processedValue !== value) {
|
||||||
onUpdate(processedValue)
|
onUpdate(processedValue)
|
||||||
|
|
@ -1577,7 +1581,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
<div className="config-item-header">
|
<div className="config-item-header">
|
||||||
<label>{label}</label>
|
<label>{label}</label>
|
||||||
{config.description && (
|
{config.description && (
|
||||||
<button
|
<button
|
||||||
className="help-btn"
|
className="help-btn"
|
||||||
onClick={() => setShowDetail(!showDetail)}
|
onClick={() => setShowDetail(!showDetail)}
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -1591,8 +1595,8 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
{getConfigDetail(label)}
|
{getConfigDetail(label)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<select
|
<select
|
||||||
value={localValue ? 'true' : 'false'}
|
value={localValue ? 'true' : 'false'}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
handleChange(e.target.value)
|
handleChange(e.target.value)
|
||||||
// 布尔值立即保存
|
// 布尔值立即保存
|
||||||
|
|
@ -1619,7 +1623,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
<div className="config-item-header">
|
<div className="config-item-header">
|
||||||
<label>{label}</label>
|
<label>{label}</label>
|
||||||
{config.description && (
|
{config.description && (
|
||||||
<button
|
<button
|
||||||
className="help-btn"
|
className="help-btn"
|
||||||
onClick={() => setShowDetail(!showDetail)}
|
onClick={() => setShowDetail(!showDetail)}
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -1633,8 +1637,8 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
{getConfigDetail(label)}
|
{getConfigDetail(label)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<select
|
<select
|
||||||
value={localValue}
|
value={localValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
handleChange(e.target.value)
|
handleChange(e.target.value)
|
||||||
// 下拉框立即保存
|
// 下拉框立即保存
|
||||||
|
|
@ -1660,7 +1664,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
<div className="config-item-header">
|
<div className="config-item-header">
|
||||||
<label>{label}</label>
|
<label>{label}</label>
|
||||||
{config.description && (
|
{config.description && (
|
||||||
<button
|
<button
|
||||||
className="help-btn"
|
className="help-btn"
|
||||||
onClick={() => setShowDetail(!showDetail)}
|
onClick={() => setShowDetail(!showDetail)}
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -1682,7 +1686,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
// 使用文本输入,避免number类型的自动补0问题
|
// 使用文本输入,避免number类型的自动补0问题
|
||||||
let newValue = e.target.value
|
let newValue = e.target.value
|
||||||
|
|
||||||
// 对于数字类型,只允许数字、小数点和负号
|
// 对于数字类型,只允许数字、小数点和负号
|
||||||
if (config.type === 'number') {
|
if (config.type === 'number') {
|
||||||
// 允许空字符串、数字、小数点和负号
|
// 允许空字符串、数字、小数点和负号
|
||||||
|
|
@ -1691,7 +1695,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
// // 无效输入,不更新
|
// // 无效输入,不更新
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// 如果是百分比配置,限制输入范围(0-100)
|
// 如果是百分比配置,限制输入范围(0-100)
|
||||||
if (isPercentKey) {
|
if (isPercentKey) {
|
||||||
const numValue = parseFloat(newValue)
|
const numValue = parseFloat(newValue)
|
||||||
|
|
@ -1702,7 +1706,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新本地值
|
// 更新本地值
|
||||||
if (config.type === 'number' && isPercentKey) {
|
if (config.type === 'number' && isPercentKey) {
|
||||||
// 百分比配置:允许字符串中间态(如 "2."),并支持小数
|
// 百分比配置:允许字符串中间态(如 "2."),并支持小数
|
||||||
|
|
@ -1764,7 +1768,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
/>
|
/>
|
||||||
{isPercentKey && <span className="percent-suffix">%</span>}
|
{isPercentKey && <span className="percent-suffix">%</span>}
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<button
|
<button
|
||||||
className="save-btn"
|
className="save-btn"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
@ -1799,18 +1803,18 @@ const getConfigDetail = (key) => {
|
||||||
'PRIMARY_INTERVAL': '主周期。策略分析的主要时间周期,用于计算技术指标(RSI、MACD、布林带等)。决定策略主要关注的市场趋势级别。周期越短反应越快,周期越长趋势更明确。建议:保守策略4h-1d,平衡策略1h-4h,激进策略15m-1h。',
|
'PRIMARY_INTERVAL': '主周期。策略分析的主要时间周期,用于计算技术指标(RSI、MACD、布林带等)。决定策略主要关注的市场趋势级别。周期越短反应越快,周期越长趋势更明确。建议:保守策略4h-1d,平衡策略1h-4h,激进策略15m-1h。',
|
||||||
'CONFIRM_INTERVAL': '确认周期。用于确认交易信号的更长时间周期,增加信号的可靠性,减少假信号。通常比主周期更长。建议:保守策略1d,平衡策略4h-1d,激进策略1h-4h。',
|
'CONFIRM_INTERVAL': '确认周期。用于确认交易信号的更长时间周期,增加信号的可靠性,减少假信号。通常比主周期更长。建议:保守策略1d,平衡策略4h-1d,激进策略1h-4h。',
|
||||||
'ENTRY_INTERVAL': '入场周期。用于精确入场的短时间周期,优化入场点,提高单笔交易的盈亏比。通常比主周期更短。建议:保守策略1h,平衡策略15m-30m,激进策略5m-15m。',
|
'ENTRY_INTERVAL': '入场周期。用于精确入场的短时间周期,优化入场点,提高单笔交易的盈亏比。通常比主周期更短。建议:保守策略1h,平衡策略15m-30m,激进策略5m-15m。',
|
||||||
|
|
||||||
// 仓位控制参数
|
// 仓位控制参数
|
||||||
'MAX_POSITION_PERCENT': '单笔最大保证金占用(账户余额的百分比,如0.02表示2%)。系统会先按该比例分配“保证金”,再根据杠杆换算名义价值下单。值越小越保守,适合回归波段交易、降低单笔波动对账户的伤害。注意:币安合约有最小名义价值要求(通常≥5 USDT),如果保证金过小且杠杆不够,系统会提示无法满足最小名义价值。',
|
'MAX_POSITION_PERCENT': '单笔最大保证金占用(账户余额的百分比,如0.02表示2%)。系统会先按该比例分配“保证金”,再根据杠杆换算名义价值下单。值越小越保守,适合回归波段交易、降低单笔波动对账户的伤害。注意:币安合约有最小名义价值要求(通常≥5 USDT),如果保证金过小且杠杆不够,系统会提示无法满足最小名义价值。',
|
||||||
'MAX_TOTAL_POSITION_PERCENT': '总保证金上限(账户余额的百分比,如0.20表示20%)。所有持仓占用的保证金之和不能超过该上限,防止过度分散和风险集中。值越小越易控仓,便于管理持仓数量与节奏。',
|
'MAX_TOTAL_POSITION_PERCENT': '总保证金上限(账户余额的百分比,如0.20表示20%)。所有持仓占用的保证金之和不能超过该上限,防止过度分散和风险集中。值越小越易控仓,便于管理持仓数量与节奏。',
|
||||||
'MIN_POSITION_PERCENT': '单笔最小保证金占用(账户余额的百分比,如0.01表示1%)。用于避免过小仓位(意义不大、易受手续费影响)。如果你希望取消这一限制,可设置为0(等价于关闭最小占比)。',
|
'MIN_POSITION_PERCENT': '单笔最小保证金占用(账户余额的百分比,如0.01表示1%)。用于避免过小仓位(意义不大、易受手续费影响)。如果你希望取消这一限制,可设置为0(等价于关闭最小占比)。',
|
||||||
|
|
||||||
// 风险控制参数
|
// 风险控制参数
|
||||||
'STOP_LOSS_PERCENT': '止损百分比(如0.08表示8%,相对于保证金)。当亏损达到此百分比时自动平仓止损,限制单笔交易的最大亏损。值越小止损更严格,单笔损失更小但可能被正常波动触发。值越大允许更大的回撤,但单笔损失可能较大。建议:保守策略10-15%,平衡策略8-10%,激进策略5-8%。注意:止损应该小于止盈,建议盈亏比至少1:1.5。系统会结合最小价格变动保护,取更宽松的一个。',
|
'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。系统会结合最小价格变动保护,取更宽松的一个。',
|
'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_STOP_LOSS_PRICE_PCT': '最小止损价格变动百分比(存储为0~1,前端输入按%显示,支持小数,如1.5%)。用于防止止损过紧:即使基于保证金的止损更紧,也会使用至少该百分比的价格变动。建议:1.5%-3%。',
|
||||||
'MIN_TAKE_PROFIT_PRICE_PCT': '最小止盈价格变动百分比(存储为0~1,前端输入按%显示,支持小数,如2.5%)。用于防止止盈过紧:即使基于保证金的止盈更紧,也会使用至少该百分比的价格变动。建议:2%-4%。',
|
'MIN_TAKE_PROFIT_PRICE_PCT': '最小止盈价格变动百分比(存储为0~1,前端输入按%显示,支持小数,如2.5%)。用于防止止盈过紧:即使基于保证金的止盈更紧,也会使用至少该百分比的价格变动。建议:2%-4%。',
|
||||||
|
|
||||||
// 策略参数
|
// 策略参数
|
||||||
'LEVERAGE': '交易杠杆倍数。放大资金利用率,同时放大收益和风险。杠杆越高,相同仓位下需要的保证金越少,但风险越大。建议:保守策略5-10倍,平衡策略10倍,激进策略10-15倍。注意:高杠杆会增加爆仓风险,请谨慎使用。',
|
'LEVERAGE': '交易杠杆倍数。放大资金利用率,同时放大收益和风险。杠杆越高,相同仓位下需要的保证金越少,但风险越大。建议:保守策略5-10倍,平衡策略10倍,激进策略10-15倍。注意:高杠杆会增加爆仓风险,请谨慎使用。',
|
||||||
'USE_TRAILING_STOP': '是否启用移动止损(true/false)。启用后,当盈利达到激活阈值时,止损会自动跟踪价格,保护利润。适合趋势行情,可以捕捉更大的利润空间。建议:平衡和激进策略启用,保守策略可关闭。',
|
'USE_TRAILING_STOP': '是否启用移动止损(true/false)。启用后,当盈利达到激活阈值时,止损会自动跟踪价格,保护利润。适合趋势行情,可以捕捉更大的利润空间。建议:平衡和激进策略启用,保守策略可关闭。',
|
||||||
|
|
@ -1826,7 +1830,7 @@ const getConfigDetail = (key) => {
|
||||||
'ENTRY_MAX_DRIFT_PCT_RANGING': '震荡/弱趋势最大追价偏离(%)。例如 0.15 表示 0.15%。越小越保守;建议 0.1%-0.3%。',
|
'ENTRY_MAX_DRIFT_PCT_RANGING': '震荡/弱趋势最大追价偏离(%)。例如 0.15 表示 0.15%。越小越保守;建议 0.1%-0.3%。',
|
||||||
'TRAILING_STOP_ACTIVATION': '移动止损激活阈值(百分比,如0.01表示1%)。当盈利达到此百分比时,移动止损开始跟踪价格,将止损移至成本价(保本)。值越小激活越早,更早保护利润但可能过早退出。值越大激活越晚,给价格更多波动空间。建议:1-2%。',
|
'TRAILING_STOP_ACTIVATION': '移动止损激活阈值(百分比,如0.01表示1%)。当盈利达到此百分比时,移动止损开始跟踪价格,将止损移至成本价(保本)。值越小激活越早,更早保护利润但可能过早退出。值越大激活越晚,给价格更多波动空间。建议:1-2%。',
|
||||||
'TRAILING_STOP_PROTECT': '移动止损保护利润(百分比,如0.01表示1%)。当价格从最高点回撤达到此百分比时,触发止损平仓,锁定利润。值越小保护更严格,能锁定更多利润但可能过早退出。值越大允许更大回撤,可能捕捉更大趋势但利润可能回吐。建议:1-2%。',
|
'TRAILING_STOP_PROTECT': '移动止损保护利润(百分比,如0.01表示1%)。当价格从最高点回撤达到此百分比时,触发止损平仓,锁定利润。值越小保护更严格,能锁定更多利润但可能过早退出。值越大允许更大回撤,可能捕捉更大趋势但利润可能回吐。建议:1-2%。',
|
||||||
|
|
||||||
// API配置
|
// API配置
|
||||||
'BINANCE_API_KEY': '币安API密钥。用于访问币安账户的凭证,需要启用"合约交易"权限。请妥善保管,不要泄露。',
|
'BINANCE_API_KEY': '币安API密钥。用于访问币安账户的凭证,需要启用"合约交易"权限。请妥善保管,不要泄露。',
|
||||||
'BINANCE_API_SECRET': '币安API密钥。与API Key配对使用,用于签名验证。请妥善保管,不要泄露。',
|
'BINANCE_API_SECRET': '币安API密钥。与API Key配对使用,用于签名验证。请妥善保管,不要泄露。',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user