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:
WrBug
2026-01-09 08:35:05 +08:00
parent b13aef944c
commit 0327eaffe3
19 changed files with 511 additions and 842 deletions
@@ -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']