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:
@@ -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",
|
||||
|
||||
@@ -1444,7 +1444,14 @@
|
||||
"create": "创建",
|
||||
"update": "更新",
|
||||
"timeWindowStartLEEnd": "时间区间开始不能大于结束",
|
||||
"timeWindowExceed": "时间区间不能超过周期长度"
|
||||
"timeWindowExceed": "时间区间不能超过周期长度",
|
||||
"minSpreadMode": "最小价差",
|
||||
"minSpreadModeTip": "根据当前周期开盘价与收盘价的价差决定是否下单。自动:系统按历史 30 根 K 线计算建议价差(每周期更新);固定:您输入一个数值(如 30),仅当价差 ≥ 该值时才下单;无:不校验价差,满足价格区间即下单。",
|
||||
"minSpreadModeNone": "无",
|
||||
"minSpreadModeFixed": "固定",
|
||||
"minSpreadModeAuto": "自动",
|
||||
"minSpreadValue": "最小价差数值 (USDC)",
|
||||
"minSpreadValuePlaceholder": "如 30"
|
||||
},
|
||||
"redeemRequiredModal": {
|
||||
"title": "请先配置自动赎回",
|
||||
|
||||
@@ -1445,7 +1445,14 @@
|
||||
"create": "創建",
|
||||
"update": "更新",
|
||||
"timeWindowStartLEEnd": "時間區間開始不能大於結束",
|
||||
"timeWindowExceed": "時間區間不能超過週期長度"
|
||||
"timeWindowExceed": "時間區間不能超過週期長度",
|
||||
"minSpreadMode": "最小價差",
|
||||
"minSpreadModeTip": "依當前週期開盤價與收盤價的價差決定是否下單。自動:系統依歷史 30 根 K 線計算建議價差(每週期更新);固定:您輸入一個數值(如 30),僅當價差 ≥ 該值時才下單;無:不校驗價差,滿足價格區間即下單。",
|
||||
"minSpreadModeNone": "無",
|
||||
"minSpreadModeFixed": "固定",
|
||||
"minSpreadModeAuto": "自動",
|
||||
"minSpreadValue": "最小價差數值 (USDC)",
|
||||
"minSpreadValuePlaceholder": "如 30"
|
||||
},
|
||||
"redeemRequiredModal": {
|
||||
"title": "請先配置自動贖回",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
* 尾盘策略触发记录
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user