This commit is contained in:
薇薇安 2026-01-19 23:55:18 +08:00
parent 2d47ca46e0
commit 2d18d92069
2 changed files with 252 additions and 31 deletions

View File

@ -184,6 +184,66 @@
font-size: 1rem;
}
.preset-guide {
background: #fff;
border: 1px solid #e9ecef;
border-radius: 10px;
padding: 0.9rem 1rem;
margin-bottom: 1rem;
}
.preset-guide-title {
font-weight: 800;
color: #2c3e50;
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.preset-guide-list {
margin: 0;
padding-left: 1.1rem;
color: #555;
font-size: 0.9rem;
line-height: 1.6;
}
.preset-guide code {
background: #f5f7ff;
border: 1px solid #e7ecff;
padding: 0.05rem 0.35rem;
border-radius: 6px;
font-size: 0.85em;
}
.preset-groups {
display: flex;
flex-direction: column;
gap: 1rem;
}
.preset-group {
background: rgba(255, 255, 255, 0.6);
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 0.9rem;
}
.preset-group-header {
margin-bottom: 0.75rem;
}
.preset-group-title {
font-weight: 800;
color: #2c3e50;
margin-bottom: 0.25rem;
}
.preset-group-desc {
color: #666;
font-size: 0.9rem;
line-height: 1.5;
}
.current-preset-status {
display: flex;
align-items: center;
@ -282,6 +342,35 @@
gap: 0.5rem;
}
.preset-tag {
display: inline-flex;
align-items: center;
padding: 0.15rem 0.55rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 800;
border: 1px solid transparent;
white-space: nowrap;
}
.preset-tag--limit {
background: #f5f7ff;
color: #2b4cff;
border-color: #dfe6ff;
}
.preset-tag--smart {
background: #e7f7ff;
color: #006c9c;
border-color: #cfefff;
}
.preset-tag--legacy {
background: #fff7e6;
color: #a35b00;
border-color: #ffe2b5;
}
.active-indicator {
color: #4CAF50;
font-size: 1.2rem;

View File

@ -14,6 +14,14 @@ const ConfigPanel = () => {
const [backendStatus, setBackendStatus] = useState(null)
const [systemBusy, setSystemBusy] = useState(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('')
@ -339,7 +347,12 @@ const ConfigPanel = () => {
if (value === null || value === undefined) return value
// /
if (typeof value === 'number' && (key.includes('PERCENT') || key.includes('PCT'))) {
// 0~1 1 100 >=1
//
// - 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
@ -468,7 +481,12 @@ const ConfigPanel = () => {
//
let currentValue = currentConfig.value
if (key.includes('PERCENT') || key.includes('PCT')) {
currentValue = currentValue * 100
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
}
}
//
@ -509,10 +527,21 @@ const ConfigPanel = () => {
type = 'boolean'
category = 'strategy'
}
if (key.includes('PERCENT') || key.includes('PCT')) {
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'
}
@ -524,17 +553,29 @@ const ConfigPanel = () => {
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')) ? value / 100 : value,
value:
(key.includes('PERCENT') || key.includes('PCT')) && !PCT_LIKE_KEYS.has(key)
? value / 100
: value,
type,
category,
description: `预设方案配置项:${key}`
description: desc
}
}
return {
key,
value: (key.includes('PERCENT') || key.includes('PCT')) ? value / 100 : value,
value:
(key.includes('PERCENT') || key.includes('PCT')) && !PCT_LIKE_KEYS.has(key)
? value / 100
: value,
type: config.type,
category: config.category,
description: config.description
@ -673,23 +714,92 @@ const ConfigPanel = () => {
</span>
</div>
</div>
<div className="preset-buttons">
{Object.entries(presets).map(([key, preset]) => (
<button
key={key}
className={`preset-btn ${currentPreset === key ? 'active' : ''}`}
onClick={() => applyPreset(key)}
disabled={saving}
title={preset.desc}
>
<div className="preset-name">
{preset.name}
{currentPreset === key && <span className="active-indicator"></span>}
</div>
<div className="preset-desc">{preset.desc}</div>
</button>
))}
<div 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>
@ -888,6 +998,13 @@ const ConfigPanel = () => {
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
@ -905,7 +1022,13 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
if (isNaN(numVal)) {
return ''
}
// 0~1 1
//
// 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)
}
@ -972,14 +1095,22 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
}
processedValue = numValue
// 0~1
//
if (isPercentKey) {
// 0-100
if (processedValue < 0 || processedValue > 100) {
setLocalValue(getInitialDisplayValue(value))
return
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
}
processedValue = processedValue / 100
}
} else if (config.type === 'boolean') {
processedValue = localValue === 'true' || localValue === true
@ -1133,7 +1264,8 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
// 0-100
if (isPercentKey) {
const numValue = parseFloat(newValue)
if (newValue !== '' && !isNaN(numValue) && (numValue < 0 || numValue > 100)) {
const maxAllowed = isPctLike ? 1 : 100
if (newValue !== '' && !isNaN(numValue) && (numValue < 0 || numValue > maxAllowed)) {
//
return
}
@ -1197,7 +1329,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
}}
disabled={disabled}
className={isEditing ? 'editing' : ''}
placeholder={isPercentKey ? '输入百分比(可小数)' : '输入数值'}
placeholder={isPercentKey ? (isPctLike ? '输入0~1表示0%~1%' : '输入百分比(可小数)') : '输入数值'}
/>
{isPercentKey && <span className="percent-suffix">%</span>}
{isEditing && (