feat(crypto-tail): 策略最小价差(无/固定/自动) + 前端默认与文案

- 后端: 最小价差 DB/Entity/DTO、Binance K线 REST+WS、自动价差 IQR 预计算与执行时校验
- 前端: 最小价差(自动-固定-无),默认自动,label 旁 info 说明,选择自动不展示建议约
- i18n: minSpreadModeTip 说明不写死标的
- 文档: crypto-tail-strategy-min-spread-flow.md
- scripts: Binance K线拉取与 WS 示例

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WrBug
2026-02-14 15:16:44 +08:00
co-authored by Cursor
parent 7ec9311df2
commit b50e43c239
22 changed files with 930 additions and 14 deletions
+8 -1
View File
@@ -1445,7 +1445,14 @@
"create": "Create",
"update": "Update",
"timeWindowStartLEEnd": "Window start must not be greater than end",
"timeWindowExceed": "Time window must not exceed period length"
"timeWindowExceed": "Time window must not exceed period length",
"minSpreadMode": "Min spread",
"minSpreadModeTip": "Whether to place an order is based on the spread between open and close in the current period. Auto: system computes a suggested spread from the last 30 klines (updated each period); Fixed: you enter a value (e.g. 30), order only when spread ≥ that value; None: no spread check, order when price is in range.",
"minSpreadModeNone": "None",
"minSpreadModeFixed": "Fixed",
"minSpreadModeAuto": "Auto",
"minSpreadValue": "Min spread value (USDC)",
"minSpreadValuePlaceholder": "e.g. 30"
},
"redeemRequiredModal": {
"title": "Configure Auto Redeem First",
+8 -1
View File
@@ -1444,7 +1444,14 @@
"create": "创建",
"update": "更新",
"timeWindowStartLEEnd": "时间区间开始不能大于结束",
"timeWindowExceed": "时间区间不能超过周期长度"
"timeWindowExceed": "时间区间不能超过周期长度",
"minSpreadMode": "最小价差",
"minSpreadModeTip": "根据当前周期开盘价与收盘价的价差决定是否下单。自动:系统按历史 30 根 K 线计算建议价差(每周期更新);固定:您输入一个数值(如 30),仅当价差 ≥ 该值时才下单;无:不校验价差,满足价格区间即下单。",
"minSpreadModeNone": "无",
"minSpreadModeFixed": "固定",
"minSpreadModeAuto": "自动",
"minSpreadValue": "最小价差数值 (USDC)",
"minSpreadValuePlaceholder": "如 30"
},
"redeemRequiredModal": {
"title": "请先配置自动赎回",
+8 -1
View File
@@ -1445,7 +1445,14 @@
"create": "創建",
"update": "更新",
"timeWindowStartLEEnd": "時間區間開始不能大於結束",
"timeWindowExceed": "時間區間不能超過週期長度"
"timeWindowExceed": "時間區間不能超過週期長度",
"minSpreadMode": "最小價差",
"minSpreadModeTip": "依當前週期開盤價與收盤價的價差決定是否下單。自動:系統依歷史 30 根 K 線計算建議價差(每週期更新);固定:您輸入一個數值(如 30),僅當價差 ≥ 該值時才下單;無:不校驗價差,滿足價格區間即下單。",
"minSpreadModeNone": "無",
"minSpreadModeFixed": "固定",
"minSpreadModeAuto": "自動",
"minSpreadValue": "最小價差數值 (USDC)",
"minSpreadValuePlaceholder": "如 30"
},
"redeemRequiredModal": {
"title": "請先配置自動贖回",
+54 -4
View File
@@ -16,9 +16,10 @@ import {
Input,
InputNumber,
Radio,
Spin
Spin,
Tooltip
} from 'antd'
import { PlusOutlined, EditOutlined, UnorderedListOutlined } from '@ant-design/icons'
import { PlusOutlined, EditOutlined, UnorderedListOutlined, InfoCircleOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { useMediaQuery } from 'react-responsive'
import { apiService } from '../services/api'
@@ -107,6 +108,7 @@ const CryptoTailStrategyList: React.FC = () => {
enabled: true,
amountMode: 'RATIO',
maxPrice: '1',
minSpreadMode: 'AUTO',
windowStartMinutes: 0,
windowStartSeconds: 0
})
@@ -127,6 +129,8 @@ const CryptoTailStrategyList: React.FC = () => {
maxPrice: record.maxPrice,
amountMode: record.amountMode,
amountValue: record.amountValue,
minSpreadMode: record.minSpreadMode ?? 'AUTO',
minSpreadValue: record.minSpreadValue ?? undefined,
enabled: record.enabled
})
setFormModalOpen(true)
@@ -159,6 +163,8 @@ const CryptoTailStrategyList: React.FC = () => {
maxPrice: v.maxPrice != null ? String(v.maxPrice) : undefined,
amountMode: v.amountMode as string,
amountValue: String(v.amountValue ?? 0),
minSpreadMode: (v.minSpreadMode as string) || 'AUTO',
minSpreadValue: v.minSpreadMode === 'FIXED' && v.minSpreadValue != null ? String(v.minSpreadValue) : (v.minSpreadMode === 'AUTO' && v.minSpreadValue != null ? String(v.minSpreadValue) : undefined),
enabled: v.enabled !== false
}
if (editingId) {
@@ -171,6 +177,8 @@ const CryptoTailStrategyList: React.FC = () => {
maxPrice: payload.maxPrice,
amountMode: payload.amountMode,
amountValue: payload.amountValue,
minSpreadMode: payload.minSpreadMode,
minSpreadValue: payload.minSpreadValue,
enabled: payload.enabled
})
if (res.data.code === 0) {
@@ -181,7 +189,10 @@ const CryptoTailStrategyList: React.FC = () => {
message.error(res.data.msg || t('common.failed'))
}
} else {
const res = await apiService.cryptoTailStrategy.create(payload)
const res = await apiService.cryptoTailStrategy.create({
...payload,
minSpreadValue: payload.minSpreadMode === 'FIXED' ? payload.minSpreadValue : undefined
})
if (res.data.code === 0) {
message.success(t('common.success'))
setFormModalOpen(false)
@@ -532,7 +543,7 @@ const CryptoTailStrategyList: React.FC = () => {
destroyOnClose
>
<Alert type="warning" showIcon message={t('cryptoTailStrategy.form.walletTip')} style={{ marginBottom: 16 }} />
<Form form={form} layout="vertical" initialValues={{ amountMode: 'RATIO', maxPrice: '1', enabled: true }}>
<Form form={form} layout="vertical" initialValues={{ amountMode: 'RATIO', maxPrice: '1', minSpreadMode: 'AUTO', enabled: true }}>
<Form.Item name="accountId" label={t('cryptoTailStrategy.form.selectAccount')} rules={[{ required: true }]}>
<Select
placeholder={t('cryptoTailStrategy.form.selectAccount')}
@@ -624,6 +635,45 @@ const CryptoTailStrategyList: React.FC = () => {
)
}
</Form.Item>
<Form.Item
name="minSpreadMode"
label={
<Space size={4}>
<span>{t('cryptoTailStrategy.form.minSpreadMode')}</span>
<Tooltip title={t('cryptoTailStrategy.form.minSpreadModeTip')}>
<InfoCircleOutlined style={{ color: '#999', cursor: 'help', fontSize: 14 }} />
</Tooltip>
</Space>
}
>
<Radio.Group>
<Radio value="AUTO">{t('cryptoTailStrategy.form.minSpreadModeAuto')}</Radio>
<Radio value="FIXED">{t('cryptoTailStrategy.form.minSpreadModeFixed')}</Radio>
<Radio value="NONE">{t('cryptoTailStrategy.form.minSpreadModeNone')}</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prev, curr) => prev.minSpreadMode !== curr.minSpreadMode}
>
{({ getFieldValue }) =>
getFieldValue('minSpreadMode') === 'FIXED' ? (
<Form.Item
name="minSpreadValue"
label={t('cryptoTailStrategy.form.minSpreadValue')}
rules={[{ required: true }]}
>
<InputNumber
min={0}
step={1}
placeholder={t('cryptoTailStrategy.form.minSpreadValuePlaceholder')}
style={{ width: '100%' }}
stringMode
/>
</Form.Item>
) : null
}
</Form.Item>
<Form.Item name="enabled" valuePropName="checked">
<Switch checkedChildren={t('common.enabled')} unCheckedChildren={t('common.disabled')} />
</Form.Item>
+7 -1
View File
@@ -447,6 +447,8 @@ export const apiService = {
maxPrice?: string
amountMode: string
amountValue: string
minSpreadMode?: string
minSpreadValue?: string | null
enabled?: boolean
}) =>
apiClient.post<ApiResponse<import('../types').CryptoTailStrategyDto>>('/crypto-tail-strategy/create', data),
@@ -459,6 +461,8 @@ export const apiService = {
maxPrice?: string
amountMode?: string
amountValue?: string
minSpreadMode?: string
minSpreadValue?: string | null
enabled?: boolean
}) =>
apiClient.post<ApiResponse<import('../types').CryptoTailStrategyDto>>('/crypto-tail-strategy/update', data),
@@ -467,7 +471,9 @@ export const apiService = {
triggers: (data: { strategyId: number; page?: number; pageSize?: number; status?: string }) =>
apiClient.post<ApiResponse<{ list: import('../types').CryptoTailStrategyTriggerDto[]; total: number }>>('/crypto-tail-strategy/triggers', data),
marketOptions: () =>
apiClient.post<ApiResponse<import('../types').CryptoTailMarketOptionDto[]>>('/crypto-tail-strategy/market-options', {})
apiClient.post<ApiResponse<import('../types').CryptoTailMarketOptionDto[]>>('/crypto-tail-strategy/market-options', {}),
autoMinSpread: (data: { intervalSeconds: number }) =>
apiClient.post<ApiResponse<import('../types').CryptoTailAutoMinSpreadResponse>>('/crypto-tail-strategy/auto-min-spread', data)
},
/**
+10
View File
@@ -1051,6 +1051,10 @@ export interface CryptoTailStrategyDto {
maxPrice: string
amountMode: string
amountValue: string
/** 最小价差模式: NONE, FIXED, AUTO */
minSpreadMode?: string
/** 最小价差数值(FIXED 时必填;AUTO 时可为计算值) */
minSpreadValue?: string | null
enabled: boolean
lastTriggerAt?: number
/** 已实现总收益 USDC */
@@ -1063,6 +1067,12 @@ export interface CryptoTailStrategyDto {
updatedAt: number
}
/** 自动最小价差计算响应 */
export interface CryptoTailAutoMinSpreadResponse {
minSpreadUp: string
minSpreadDown: string
}
/**
* 尾盘策略触发记录
*/