a
This commit is contained in:
parent
2d47ca46e0
commit
2d18d92069
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,8 +481,13 @@ const ConfigPanel = () => {
|
|||
// 获取当前值(处理百分比转换)
|
||||
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') {
|
||||
|
|
@ -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-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">
|
||||
{Object.entries(presets).map(([key, preset]) => (
|
||||
{g.presetKeys
|
||||
.filter((k) => presets[k])
|
||||
.map((k) => {
|
||||
const preset = presets[k]
|
||||
const meta = presetUiMeta[k] || { group: g.key, tag: '' }
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
className={`preset-btn ${currentPreset === key ? 'active' : ''}`}
|
||||
onClick={() => applyPreset(key)}
|
||||
key={k}
|
||||
className={`preset-btn ${currentPreset === k ? 'active' : ''}`}
|
||||
onClick={() => applyPreset(k)}
|
||||
disabled={saving}
|
||||
title={preset.desc}
|
||||
>
|
||||
<div className="preset-name">
|
||||
{preset.name}
|
||||
{currentPreset === key && <span className="active-indicator">✓</span>}
|
||||
{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,15 +1095,23 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
|||
}
|
||||
|
||||
processedValue = numValue
|
||||
// 百分比配置:前端统一按“百分比”输入(支持小数),存储为 0~1
|
||||
// 百分比配置
|
||||
if (isPercentKey) {
|
||||
// 限制范围 0-100
|
||||
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
|
||||
}
|
||||
|
|
@ -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 && (
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user