feat: 添加市场截止时间筛选功能
后端: - 为 copy_trading 表添加 max_market_end_date 字段(毫秒时间戳) - 为 markets 表添加 end_date 字段,迁移时清空已有数据 - 实现市场截止时间过滤逻辑(仅跟单截止时间小于设置时间的订单) - 更新 CopyTradingService 支持 maxMarketEndDate 的创建和更新 - 更新 FilterResult 添加 FAILED_MARKET_END_DATE 状态 - 更新 CopyOrderTrackingService 传递 marketEndDate 参数 前端: - 添加市场截止时间筛选 UI(支持小时和天两种单位) - 输入限制:仅支持整数输入(precision=0) - 更新 AddModal 和 EditModal 支持市场截止时间配置 - 更新过滤类型显示映射(MARKET_END_DATE) - 添加多语言支持(中文、英文、繁体中文) 数据库迁移: - V22: 合并添加 max_market_end_date 和 end_date 字段 - 迁移时清空 markets 表数据,确保所有数据都包含 end_date
This commit is contained in:
@@ -768,6 +768,14 @@
|
||||
"keywordExists": "Keyword already exists",
|
||||
"whitelistTooltip": "💡 Whitelist mode: only copy markets whose titles contain any of the above keywords (case-insensitive)",
|
||||
"blacklistTooltip": "💡 Blacklist mode: do not copy markets whose titles contain any of the above keywords (case-insensitive)",
|
||||
"marketEndDateFilter": "Market End Date Limit",
|
||||
"maxMarketEndDate": "Max Market End Date",
|
||||
"maxMarketEndDateTooltip": "Only copy orders with market end time less than the set time. For example: 24 hours means only copy markets with less than 24 hours remaining until settlement",
|
||||
"maxMarketEndDatePlaceholder": "Enter time value (optional)",
|
||||
"timeUnit": "Unit",
|
||||
"hour": "Hours",
|
||||
"day": "Days",
|
||||
"maxMarketEndDateNote": "💡 Note: Leave blank to disable this filter",
|
||||
"configName": "Configuration Name",
|
||||
"configNameRequired": "Please enter configuration name",
|
||||
"configNamePlaceholder": "e.g., Copy Trading Config 1",
|
||||
@@ -865,6 +873,14 @@
|
||||
"keywordExists": "Keyword already exists",
|
||||
"whitelistTooltip": "💡 Whitelist mode: only copy markets whose titles contain any of the above keywords (case-insensitive)",
|
||||
"blacklistTooltip": "💡 Blacklist mode: do not copy markets whose titles contain any of the above keywords (case-insensitive)",
|
||||
"marketEndDateFilter": "Market End Date Limit",
|
||||
"maxMarketEndDate": "Max Market End Date",
|
||||
"maxMarketEndDateTooltip": "Only copy orders with market end time less than the set time. For example: 24 hours means only copy markets with less than 24 hours remaining until settlement",
|
||||
"maxMarketEndDatePlaceholder": "Enter time value (optional)",
|
||||
"timeUnit": "Unit",
|
||||
"hour": "Hours",
|
||||
"day": "Days",
|
||||
"maxMarketEndDateNote": "💡 Note: Leave blank to disable this filter",
|
||||
"configName": "Configuration Name",
|
||||
"configNameRequired": "Please enter configuration name",
|
||||
"configNamePlaceholder": "e.g., Copy Trading Config 1",
|
||||
@@ -909,6 +925,7 @@
|
||||
"priceRange": "Price Range Mismatch",
|
||||
"maxPositionValue": "Exceeds Max Position Value",
|
||||
"maxPositionCount": "Exceeds Max Position Count",
|
||||
"marketEndDate": "Market End Date Exceeds Limit",
|
||||
"unknown": "Unknown Reason"
|
||||
},
|
||||
"noData": "No filtered orders"
|
||||
|
||||
@@ -780,6 +780,14 @@
|
||||
"keywordExists": "关键字已存在",
|
||||
"whitelistTooltip": "💡 白名单模式:只跟单包含上述任意关键字的市场标题(不区分大小写)",
|
||||
"blacklistTooltip": "💡 黑名单模式:不跟单包含上述任意关键字的市场标题(不区分大小写)",
|
||||
"marketEndDateFilter": "市场截止时间限制",
|
||||
"maxMarketEndDate": "最大市场截止时间",
|
||||
"maxMarketEndDateTooltip": "仅跟单截止时间小于设定时间的订单。例如:24 小时表示只跟单距离结算还剩24小时以内的市场",
|
||||
"maxMarketEndDatePlaceholder": "输入时间值(可选)",
|
||||
"timeUnit": "单位",
|
||||
"hour": "小时",
|
||||
"day": "天",
|
||||
"maxMarketEndDateNote": "💡 说明:不填写表示不启用此限制",
|
||||
"supportSell": "跟单卖出",
|
||||
"supportSellTooltip": "是否跟单 Leader 的卖出订单",
|
||||
"create": "创建跟单配置",
|
||||
@@ -877,6 +885,14 @@
|
||||
"keywordExists": "关键字已存在",
|
||||
"whitelistTooltip": "💡 白名单模式:只跟单包含上述任意关键字的市场标题(不区分大小写)",
|
||||
"blacklistTooltip": "💡 黑名单模式:不跟单包含上述任意关键字的市场标题(不区分大小写)",
|
||||
"marketEndDateFilter": "市场截止时间限制",
|
||||
"maxMarketEndDate": "最大市场截止时间",
|
||||
"maxMarketEndDateTooltip": "仅跟单截止时间小于设定时间的订单。例如:24 小时表示只跟单距离结算还剩24小时以内的市场",
|
||||
"maxMarketEndDatePlaceholder": "输入时间值(可选)",
|
||||
"timeUnit": "单位",
|
||||
"hour": "小时",
|
||||
"day": "天",
|
||||
"maxMarketEndDateNote": "💡 说明:不填写表示不启用此限制",
|
||||
"supportSell": "跟单卖出",
|
||||
"supportSellTooltip": "是否跟单 Leader 的卖出订单",
|
||||
"save": "保存",
|
||||
@@ -909,6 +925,7 @@
|
||||
"priceRange": "价格区间不符",
|
||||
"maxPositionValue": "超过最大仓位金额",
|
||||
"maxPositionCount": "超过最大仓位数量",
|
||||
"marketEndDate": "市场截止时间超出限制",
|
||||
"unknown": "未知原因"
|
||||
},
|
||||
"noData": "暂无已过滤订单"
|
||||
|
||||
@@ -768,6 +768,14 @@
|
||||
"keywordExists": "關鍵字已存在",
|
||||
"whitelistTooltip": "💡 白名單模式:只跟單包含上述任意關鍵字的市場標題(不區分大小寫)",
|
||||
"blacklistTooltip": "💡 黑名單模式:不跟單包含上述任意關鍵字的市場標題(不區分大小寫)",
|
||||
"marketEndDateFilter": "市場截止時間限制",
|
||||
"maxMarketEndDate": "最大市場截止時間",
|
||||
"maxMarketEndDateTooltip": "僅跟單截止時間小於設定時間的訂單。例如:24 小時表示只跟單距離結算還剩24小時內的市場",
|
||||
"maxMarketEndDatePlaceholder": "輸入時間值(可選)",
|
||||
"timeUnit": "單位",
|
||||
"hour": "小時",
|
||||
"day": "天",
|
||||
"maxMarketEndDateNote": "💡 說明:不填寫表示不啟用此限制",
|
||||
"configName": "配置名",
|
||||
"configNameRequired": "請輸入配置名",
|
||||
"configNamePlaceholder": "例如:跟單配置1",
|
||||
@@ -865,6 +873,14 @@
|
||||
"keywordExists": "關鍵字已存在",
|
||||
"whitelistTooltip": "💡 白名單模式:只跟單包含上述任意關鍵字的市場標題(不區分大小寫)",
|
||||
"blacklistTooltip": "💡 黑名單模式:不跟單包含上述任意關鍵字的市場標題(不區分大小寫)",
|
||||
"marketEndDateFilter": "市場截止時間限制",
|
||||
"maxMarketEndDate": "最大市場截止時間",
|
||||
"maxMarketEndDateTooltip": "僅跟單截止時間小於設定時間的訂單。例如:24 小時表示只跟單距離結算還剩24小時內的市場",
|
||||
"maxMarketEndDatePlaceholder": "輸入時間值(可選)",
|
||||
"timeUnit": "單位",
|
||||
"hour": "小時",
|
||||
"day": "天",
|
||||
"maxMarketEndDateNote": "💡 說明:不填寫表示不啟用此限制",
|
||||
"configName": "配置名",
|
||||
"configNameRequired": "請輸入配置名",
|
||||
"configNamePlaceholder": "例如:跟單配置1",
|
||||
@@ -909,6 +925,7 @@
|
||||
"priceRange": "價格區間不符",
|
||||
"maxPositionValue": "超過最大倉位金額",
|
||||
"maxPositionCount": "超過最大倉位數量",
|
||||
"marketEndDate": "市場截止時間超出限制",
|
||||
"unknown": "未知原因"
|
||||
},
|
||||
"noData": "暫無已過濾訂單"
|
||||
|
||||
@@ -34,6 +34,8 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
const [copyMode, setCopyMode] = useState<'RATIO' | 'FIXED'>('RATIO')
|
||||
const [keywords, setKeywords] = useState<string[]>([])
|
||||
const keywordInputRef = useRef<InputRef>(null)
|
||||
const [maxMarketEndDateValue, setMaxMarketEndDateValue] = useState<number | undefined>()
|
||||
const [maxMarketEndDateUnit, setMaxMarketEndDateUnit] = useState<'HOUR' | 'DAY'>('HOUR')
|
||||
|
||||
// 导入账户modal相关状态
|
||||
const [accountImportModalVisible, setAccountImportModalVisible] = useState(false)
|
||||
@@ -198,7 +200,7 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
setKeywords(newKeywords)
|
||||
}
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
const handleSubmit = async (values: any) => {
|
||||
// 前端校验
|
||||
if (values.copyMode === 'FIXED') {
|
||||
if (!values.fixedAmount || Number(values.fixedAmount) < 1) {
|
||||
@@ -212,6 +214,15 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
// 计算市场截止时间(毫秒)
|
||||
let maxMarketEndDate: number | undefined
|
||||
if (maxMarketEndDateValue !== undefined && maxMarketEndDateValue > 0) {
|
||||
const multiplier = maxMarketEndDateUnit === 'HOUR'
|
||||
? 60 * 60 * 1000 // 小时转毫秒
|
||||
: 24 * 60 * 60 * 1000 // 天转毫秒
|
||||
maxMarketEndDate = maxMarketEndDateValue * multiplier
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const request: CopyTradingCreateRequest = {
|
||||
@@ -243,7 +254,8 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
? keywords
|
||||
: undefined,
|
||||
configName: values.configName?.trim(),
|
||||
pushFailedOrders: values.pushFailedOrders ?? false
|
||||
pushFailedOrders: values.pushFailedOrders ?? false,
|
||||
maxMarketEndDate
|
||||
}
|
||||
|
||||
const response = await apiService.copyTrading.create(request)
|
||||
@@ -783,6 +795,51 @@ const AddModal: React.FC<AddModalProps> = ({
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* 市场截止时间限制 */}
|
||||
<Divider>{t('copyTradingAdd.marketEndDateFilter') || '市场截止时间限制'}</Divider>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.maxMarketEndDate') || '最大市场截止时间'}
|
||||
tooltip={t('copyTradingAdd.maxMarketEndDateTooltip') || '仅跟单截止时间小于设定时间的订单。例如:24 小时表示只跟单距离结算还剩24小时以内的市场'}
|
||||
>
|
||||
<Input.Group compact style={{ display: 'flex' }}>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={9999}
|
||||
step={1}
|
||||
precision={0}
|
||||
value={maxMarketEndDateValue}
|
||||
onChange={(value) => setMaxMarketEndDateValue(value !== null && value !== undefined ? Math.floor(value) : undefined)}
|
||||
style={{ width: '60%' }}
|
||||
placeholder={t('copyTradingAdd.maxMarketEndDatePlaceholder') || '输入时间值(可选)'}
|
||||
parser={(value) => {
|
||||
if (!value) return ''
|
||||
const num = parseInt(value.replace(/\D/g, ''), 10)
|
||||
return isNaN(num) ? '' : num.toString()
|
||||
}}
|
||||
formatter={(value) => {
|
||||
if (!value && value !== 0) return ''
|
||||
return Math.floor(value).toString()
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
value={maxMarketEndDateUnit}
|
||||
onChange={(value) => setMaxMarketEndDateUnit(value)}
|
||||
style={{ width: '40%' }}
|
||||
placeholder={t('copyTradingAdd.timeUnit') || '单位'}
|
||||
>
|
||||
<Option value="HOUR">{t('copyTradingAdd.hour') || '小时'}</Option>
|
||||
<Option value="DAY">{t('copyTradingAdd.day') || '天'}</Option>
|
||||
</Select>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>
|
||||
{t('copyTradingAdd.maxMarketEndDateNote') || '💡 说明:不填写表示不启用此限制'}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Divider>{t('copyTradingAdd.advancedSettings') || '高级设置'}</Divider>
|
||||
|
||||
{/* 跟单卖出 */}
|
||||
|
||||
@@ -29,6 +29,8 @@ const EditModal: React.FC<EditModalProps> = ({
|
||||
const [originalEnabled, setOriginalEnabled] = useState<boolean>(true)
|
||||
const [keywords, setKeywords] = useState<string[]>([])
|
||||
const keywordInputRef = useRef<InputRef>(null)
|
||||
const [maxMarketEndDateValue, setMaxMarketEndDateValue] = useState<number | undefined>()
|
||||
const [maxMarketEndDateUnit, setMaxMarketEndDateUnit] = useState<'HOUR' | 'DAY'>('HOUR')
|
||||
|
||||
useEffect(() => {
|
||||
if (open && copyTradingId) {
|
||||
@@ -46,6 +48,24 @@ const EditModal: React.FC<EditModalProps> = ({
|
||||
setCopyTrading(found)
|
||||
setCopyMode(found.copyMode)
|
||||
setOriginalEnabled(found.enabled)
|
||||
|
||||
// 解析市场截止时间(毫秒转换为小时或天)
|
||||
if (found.maxMarketEndDate) {
|
||||
const hours = found.maxMarketEndDate / (60 * 60 * 1000)
|
||||
if (hours >= 24 && Number.isInteger(hours / 24)) {
|
||||
// 大于等于24小时且是24的整数倍,使用天作为单位
|
||||
setMaxMarketEndDateUnit('DAY')
|
||||
setMaxMarketEndDateValue(hours / 24)
|
||||
} else {
|
||||
// 使用小时作为单位
|
||||
setMaxMarketEndDateUnit('HOUR')
|
||||
setMaxMarketEndDateValue(hours)
|
||||
}
|
||||
} else {
|
||||
setMaxMarketEndDateValue(undefined)
|
||||
setMaxMarketEndDateUnit('HOUR')
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
accountId: found.accountId,
|
||||
leaderId: found.leaderId,
|
||||
@@ -147,6 +167,15 @@ const EditModal: React.FC<EditModalProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
// 计算市场截止时间(毫秒)
|
||||
let maxMarketEndDate: number | undefined
|
||||
if (maxMarketEndDateValue !== undefined && maxMarketEndDateValue > 0) {
|
||||
const multiplier = maxMarketEndDateUnit === 'HOUR'
|
||||
? 60 * 60 * 1000 // 小时转毫秒
|
||||
: 24 * 60 * 60 * 1000 // 天转毫秒
|
||||
maxMarketEndDate = maxMarketEndDateValue * multiplier
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const request: CopyTradingUpdateRequest = {
|
||||
@@ -177,7 +206,8 @@ const EditModal: React.FC<EditModalProps> = ({
|
||||
? keywords
|
||||
: undefined,
|
||||
configName: values.configName?.trim() || undefined,
|
||||
pushFailedOrders: values.pushFailedOrders
|
||||
pushFailedOrders: values.pushFailedOrders,
|
||||
maxMarketEndDate
|
||||
}
|
||||
|
||||
const response = await apiService.copyTrading.update(request)
|
||||
@@ -683,6 +713,51 @@ const EditModal: React.FC<EditModalProps> = ({
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* 市场截止时间限制 */}
|
||||
<Divider>{t('copyTradingEdit.marketEndDateFilter') || '市场截止时间限制'}</Divider>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.maxMarketEndDate') || '最大市场截止时间'}
|
||||
tooltip={t('copyTradingEdit.maxMarketEndDateTooltip') || '仅跟单截止时间小于设定时间的订单。例如:24 小时表示只跟单距离结算还剩24小时以内的市场'}
|
||||
>
|
||||
<Input.Group compact style={{ display: 'flex' }}>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={9999}
|
||||
step={1}
|
||||
precision={0}
|
||||
value={maxMarketEndDateValue}
|
||||
onChange={(value) => setMaxMarketEndDateValue(value !== null && value !== undefined ? Math.floor(value) : undefined)}
|
||||
style={{ width: '60%' }}
|
||||
placeholder={t('copyTradingEdit.maxMarketEndDatePlaceholder') || '输入时间值(可选)'}
|
||||
parser={(value) => {
|
||||
if (!value) return ''
|
||||
const num = parseInt(value.replace(/\D/g, ''), 10)
|
||||
return isNaN(num) ? '' : num.toString()
|
||||
}}
|
||||
formatter={(value) => {
|
||||
if (!value && value !== 0) return ''
|
||||
return Math.floor(value).toString()
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
value={maxMarketEndDateUnit}
|
||||
onChange={(value) => setMaxMarketEndDateUnit(value)}
|
||||
style={{ width: '40%' }}
|
||||
placeholder={t('copyTradingEdit.timeUnit') || '单位'}
|
||||
>
|
||||
<Option value="HOUR">{t('copyTradingEdit.hour') || '小时'}</Option>
|
||||
<Option value="DAY">{t('copyTradingEdit.day') || '天'}</Option>
|
||||
</Select>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>
|
||||
{t('copyTradingEdit.maxMarketEndDateNote') || '💡 说明:不填写表示不启用此限制'}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Divider>{t('copyTradingEdit.advancedSettings') || '高级设置'}</Divider>
|
||||
|
||||
<Form.Item
|
||||
|
||||
@@ -67,7 +67,11 @@ const FilteredOrdersModal: React.FC<FilteredOrdersModalProps> = ({
|
||||
MARKET_STATUS: { color: 'default', text: t('filteredOrdersList.filterTypes.marketStatus') || '市场状态不可交易' },
|
||||
ORDERBOOK_ERROR: { color: 'default', text: t('filteredOrdersList.filterTypes.orderbookError') || '订单簿获取失败' },
|
||||
ORDERBOOK_EMPTY: { color: 'default', text: t('filteredOrdersList.filterTypes.orderbookEmpty') || '订单簿为空' },
|
||||
PRICE_RANGE: { color: 'purple', text: t('filteredOrdersList.filterTypes.priceRange') || '价格区间不符' }
|
||||
PRICE_RANGE: { color: 'purple', text: t('filteredOrdersList.filterTypes.priceRange') || '价格区间不符' },
|
||||
MAX_POSITION_VALUE: { color: 'volcano', text: t('filteredOrdersList.filterTypes.maxPositionValue') || '超过最大仓位金额' },
|
||||
MAX_POSITION_COUNT: { color: 'volcano', text: t('filteredOrdersList.filterTypes.maxPositionCount') || '超过最大仓位数量' },
|
||||
MARKET_END_DATE: { color: 'cyan', text: t('filteredOrdersList.filterTypes.marketEndDate') || '市场截止时间超出限制' },
|
||||
KEYWORD_FILTER: { color: 'geekblue', text: t('filteredOrdersList.filterTypes.keywordFilter') || '关键字过滤' }
|
||||
}
|
||||
const config = typeMap[filterType] || { color: 'default', text: filterType }
|
||||
return <Tag color={config.color}>{config.text}</Tag>
|
||||
|
||||
@@ -65,6 +65,10 @@ const FilteredOrdersList: React.FC = () => {
|
||||
'ORDERBOOK_ERROR': { color: 'default', label: t('filteredOrdersList.filterTypes.orderbookError') || '订单簿获取失败' },
|
||||
'ORDERBOOK_EMPTY': { color: 'default', label: t('filteredOrdersList.filterTypes.orderbookEmpty') || '订单簿为空' },
|
||||
'PRICE_RANGE': { color: 'purple', label: t('filteredOrdersList.filterTypes.priceRange') || '价格区间不符' },
|
||||
'MAX_POSITION_VALUE': { color: 'volcano', label: t('filteredOrdersList.filterTypes.maxPositionValue') || '超过最大仓位金额' },
|
||||
'MAX_POSITION_COUNT': { color: 'volcano', label: t('filteredOrdersList.filterTypes.maxPositionCount') || '超过最大仓位数量' },
|
||||
'MARKET_END_DATE': { color: 'cyan', label: t('filteredOrdersList.filterTypes.marketEndDate') || '市场截止时间超出限制' },
|
||||
'KEYWORD_FILTER': { color: 'geekblue', label: t('filteredOrdersList.filterTypes.keywordFilter') || '关键字过滤' },
|
||||
'UNKNOWN': { color: 'default', label: t('filteredOrdersList.filterTypes.unknown') || '未知原因' }
|
||||
}
|
||||
const config = typeMap[type] || typeMap['UNKNOWN']
|
||||
|
||||
@@ -216,6 +216,7 @@ export interface CopyTrading {
|
||||
// 新增配置字段
|
||||
configName?: string // 配置名(可选,但提供时必须非空)
|
||||
pushFailedOrders: boolean // 推送失败订单(默认关闭)
|
||||
maxMarketEndDate?: number // 市场截止时间限制(毫秒时间戳),仅跟单截止时间小于此时间的订单,NULL表示不启用
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
@@ -265,6 +266,7 @@ export interface CopyTradingCreateRequest {
|
||||
// 新增配置字段
|
||||
configName?: string // 配置名(可选,但提供时必须非空)
|
||||
pushFailedOrders?: boolean // 推送失败订单(可选)
|
||||
maxMarketEndDate?: number // 市场截止时间限制(毫秒时间戳),仅跟单截止时间小于此时间的订单,NULL表示不启用
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,6 +304,7 @@ export interface CopyTradingUpdateRequest {
|
||||
// 新增配置字段
|
||||
configName?: string // 配置名(可选,但提供时必须非空)
|
||||
pushFailedOrders?: boolean // 推送失败订单(可选)
|
||||
maxMarketEndDate?: number // 市场截止时间限制(毫秒时间戳),仅跟单截止时间小于此时间的订单,NULL表示不启用
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user