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:
@@ -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']
|
||||
|
||||
Reference in New Issue
Block a user