a
This commit is contained in:
parent
2d47ca46e0
commit
2d18d92069
|
|
@ -184,6 +184,66 @@
|
||||||
font-size: 1rem;
|
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 {
|
.current-preset-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -282,6 +342,35 @@
|
||||||
gap: 0.5rem;
|
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 {
|
.active-indicator {
|
||||||
color: #4CAF50;
|
color: #4CAF50;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,14 @@ const ConfigPanel = () => {
|
||||||
const [backendStatus, setBackendStatus] = useState(null)
|
const [backendStatus, setBackendStatus] = useState(null)
|
||||||
const [systemBusy, setSystemBusy] = useState(false)
|
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 [showSnapshot, setShowSnapshot] = useState(false)
|
||||||
const [snapshotText, setSnapshotText] = useState('')
|
const [snapshotText, setSnapshotText] = useState('')
|
||||||
|
|
@ -339,7 +347,12 @@ const ConfigPanel = () => {
|
||||||
if (value === null || value === undefined) return value
|
if (value === null || value === undefined) return value
|
||||||
// 百分比/比例:尽量转成 “百分比值”
|
// 百分比/比例:尽量转成 “百分比值”
|
||||||
if (typeof value === 'number' && (key.includes('PERCENT') || key.includes('PCT'))) {
|
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 < 1 ? value * 100 : value
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
|
|
@ -468,8 +481,13 @@ const ConfigPanel = () => {
|
||||||
// 获取当前值(处理百分比转换)
|
// 获取当前值(处理百分比转换)
|
||||||
let currentValue = currentConfig.value
|
let currentValue = currentConfig.value
|
||||||
if (key.includes('PERCENT') || key.includes('PCT')) {
|
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
|
currentValue = currentValue * 100
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 比较值(允许小的浮点数误差)
|
// 比较值(允许小的浮点数误差)
|
||||||
if (typeof expectedValue === 'number' && typeof currentValue === 'number') {
|
if (typeof expectedValue === 'number' && typeof currentValue === 'number') {
|
||||||
|
|
@ -509,10 +527,21 @@ const ConfigPanel = () => {
|
||||||
type = 'boolean'
|
type = 'boolean'
|
||||||
category = 'strategy'
|
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'
|
type = 'number'
|
||||||
if (key.includes('STOP_LOSS') || key.includes('TAKE_PROFIT')) {
|
if (key.includes('STOP_LOSS') || key.includes('TAKE_PROFIT')) {
|
||||||
category = 'risk'
|
category = 'risk'
|
||||||
|
} else if (key.includes('POSITION')) {
|
||||||
|
category = 'position'
|
||||||
} else {
|
} else {
|
||||||
category = 'scan'
|
category = 'scan'
|
||||||
}
|
}
|
||||||
|
|
@ -524,17 +553,29 @@ const ConfigPanel = () => {
|
||||||
category = 'scan'
|
category = 'scan'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const detail = typeof getConfigDetail === 'function' ? getConfigDetail(key) : ''
|
||||||
|
const desc =
|
||||||
|
detail && typeof detail === 'string' && !detail.includes('暂无详细说明')
|
||||||
|
? detail
|
||||||
|
: `预设方案配置项:${key}`
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key,
|
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,
|
type,
|
||||||
category,
|
category,
|
||||||
description: `预设方案配置项:${key}`
|
description: desc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
key,
|
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,
|
type: config.type,
|
||||||
category: config.category,
|
category: config.category,
|
||||||
description: config.description
|
description: config.description
|
||||||
|
|
@ -673,23 +714,92 @@ const ConfigPanel = () => {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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
|
<button
|
||||||
key={key}
|
key={k}
|
||||||
className={`preset-btn ${currentPreset === key ? 'active' : ''}`}
|
className={`preset-btn ${currentPreset === k ? 'active' : ''}`}
|
||||||
onClick={() => applyPreset(key)}
|
onClick={() => applyPreset(k)}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
title={preset.desc}
|
title={preset.desc}
|
||||||
>
|
>
|
||||||
<div className="preset-name">
|
<div className="preset-name">
|
||||||
{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>
|
||||||
<div className="preset-desc">{preset.desc}</div>
|
<div className="preset-desc">{preset.desc}</div>
|
||||||
</button>
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -888,6 +998,13 @@ const ConfigPanel = () => {
|
||||||
|
|
||||||
const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
const isPercentKey = label.includes('PERCENT') || label.includes('PCT')
|
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) => {
|
const formatPercent = (n) => {
|
||||||
// 最多 4 位小数,去掉尾随 0(例如 2.3000 -> 2.3)
|
// 最多 4 位小数,去掉尾随 0(例如 2.3000 -> 2.3)
|
||||||
|
|
@ -905,7 +1022,13 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
if (isNaN(numVal)) {
|
if (isNaN(numVal)) {
|
||||||
return ''
|
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
|
const percent = numVal <= 1 ? numVal * 100 : numVal
|
||||||
return formatPercent(percent)
|
return formatPercent(percent)
|
||||||
}
|
}
|
||||||
|
|
@ -972,15 +1095,23 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
processedValue = numValue
|
processedValue = numValue
|
||||||
// 百分比配置:前端统一按“百分比”输入(支持小数),存储为 0~1
|
// 百分比配置
|
||||||
if (isPercentKey) {
|
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) {
|
if (processedValue < 0 || processedValue > 100) {
|
||||||
setLocalValue(getInitialDisplayValue(value))
|
setLocalValue(getInitialDisplayValue(value))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
processedValue = processedValue / 100
|
processedValue = processedValue / 100
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (config.type === 'boolean') {
|
} else if (config.type === 'boolean') {
|
||||||
processedValue = localValue === 'true' || localValue === true
|
processedValue = localValue === 'true' || localValue === true
|
||||||
}
|
}
|
||||||
|
|
@ -1133,7 +1264,8 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
// 如果是百分比配置,限制输入范围(0-100)
|
// 如果是百分比配置,限制输入范围(0-100)
|
||||||
if (isPercentKey) {
|
if (isPercentKey) {
|
||||||
const numValue = parseFloat(newValue)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -1197,7 +1329,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
}}
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={isEditing ? 'editing' : ''}
|
className={isEditing ? 'editing' : ''}
|
||||||
placeholder={isPercentKey ? '输入百分比(可小数)' : '输入数值'}
|
placeholder={isPercentKey ? (isPctLike ? '输入0~1(表示0%~1%)' : '输入百分比(可小数)') : '输入数值'}
|
||||||
/>
|
/>
|
||||||
{isPercentKey && <span className="percent-suffix">%</span>}
|
{isPercentKey && <span className="percent-suffix">%</span>}
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user