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:
@@ -42,7 +42,8 @@ data class CopyTradingCreateRequest(
|
||||
val keywords: List<String>? = null, // 关键字列表,当keywordFilterMode为DISABLED时为null
|
||||
// 新增配置字段
|
||||
val configName: String? = null, // 配置名(可选)
|
||||
val pushFailedOrders: Boolean? = null // 推送失败订单(可选)
|
||||
val pushFailedOrders: Boolean? = null, // 推送失败订单(可选)
|
||||
val maxMarketEndDate: Long? = null // 市场截止时间限制(毫秒时间戳),仅跟单截止时间小于此时间的订单,NULL表示不启用
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -79,7 +80,8 @@ data class CopyTradingUpdateRequest(
|
||||
val keywords: List<String>? = null, // 关键字列表,当keywordFilterMode为DISABLED时为null
|
||||
// 新增配置字段
|
||||
val configName: String? = null, // 配置名(可选,但提供时必须非空)
|
||||
val pushFailedOrders: Boolean? = null // 推送失败订单(可选)
|
||||
val pushFailedOrders: Boolean? = null, // 推送失败订单(可选)
|
||||
val maxMarketEndDate: Long? = null // 市场截止时间限制(毫秒时间戳),仅跟单截止时间小于此时间的订单,NULL表示不启用
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -154,6 +156,7 @@ data class CopyTradingDto(
|
||||
// 新增配置字段
|
||||
val configName: String? = null, // 配置名(可选)
|
||||
val pushFailedOrders: Boolean = false, // 推送失败订单(默认关闭)
|
||||
val maxMarketEndDate: Long? = null, // 市场截止时间限制(毫秒时间戳),仅跟单截止时间小于此时间的订单,NULL表示不启用
|
||||
val createdAt: Long,
|
||||
val updatedAt: Long
|
||||
)
|
||||
|
||||
@@ -105,6 +105,9 @@ data class CopyTrading(
|
||||
@Column(name = "push_failed_orders", nullable = false)
|
||||
val pushFailedOrders: Boolean = false, // 推送失败订单(默认关闭)
|
||||
|
||||
@Column(name = "max_market_end_date")
|
||||
val maxMarketEndDate: Long? = null, // 市场截止时间限制(毫秒时间戳),仅跟单截止时间小于此时间的订单,NULL表示不启用
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@ data class Market(
|
||||
@Column(name = "archived", nullable = false)
|
||||
val archived: Boolean = false, // 是否已归档
|
||||
|
||||
@Column(name = "end_date")
|
||||
val endDate: Long? = null, // 市场截止时间(毫秒时间戳)
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import com.wrbug.polymarketbot.util.getEventSlug
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
/**
|
||||
* 市场信息服务
|
||||
@@ -173,6 +175,7 @@ class MarketService(
|
||||
active = marketResponse.active ?: existingMarket.active,
|
||||
closed = marketResponse.closed ?: existingMarket.closed,
|
||||
archived = marketResponse.archived ?: existingMarket.archived,
|
||||
endDate = parseEndDate(marketResponse.endDate),
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
} else {
|
||||
@@ -189,6 +192,7 @@ class MarketService(
|
||||
active = marketResponse.active ?: true,
|
||||
closed = marketResponse.closed ?: false,
|
||||
archived = marketResponse.archived ?: false,
|
||||
endDate = parseEndDate(marketResponse.endDate),
|
||||
createdAt = System.currentTimeMillis(),
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
@@ -209,5 +213,22 @@ class MarketService(
|
||||
fun clearCache() {
|
||||
marketCache.invalidateAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析市场截止时间(ISO 8601 格式)
|
||||
*/
|
||||
private fun parseEndDate(endDate: String?): Long? {
|
||||
if (endDate.isNullOrBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
// ISO 8601 格式,例如:2025-03-15T12:00:00Z
|
||||
Instant.parse(endDate).toEpochMilli()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("解析市场截止时间失败: endDate=$endDate, error=${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+44
-1
@@ -35,6 +35,7 @@ class CopyTradingFilterService(
|
||||
* @param copyOrderAmount 跟单金额(USDC),用于仓位检查,如果为null则不进行仓位检查
|
||||
* @param marketId 市场ID,用于仓位检查(按市场过滤仓位)
|
||||
* @param marketTitle 市场标题,用于关键字过滤
|
||||
* @param marketEndDate 市场截止时间,用于市场截止时间检查
|
||||
* @return 过滤结果
|
||||
*/
|
||||
suspend fun checkFilters(
|
||||
@@ -43,7 +44,8 @@ class CopyTradingFilterService(
|
||||
tradePrice: BigDecimal? = null, // Leader 交易价格,用于价格区间检查
|
||||
copyOrderAmount: BigDecimal? = null, // 跟单金额(USDC),用于仓位检查
|
||||
marketId: String? = null, // 市场ID,用于仓位检查(按市场过滤仓位)
|
||||
marketTitle: String? = null // 市场标题,用于关键字过滤
|
||||
marketTitle: String? = null, // 市场标题,用于关键字过滤
|
||||
marketEndDate: Long? = null // 市场截止时间,用于市场截止时间检查
|
||||
): FilterResult {
|
||||
// 1. 关键字过滤检查(如果配置了关键字过滤)
|
||||
if (copyTrading.keywordFilterMode != null && copyTrading.keywordFilterMode != "DISABLED") {
|
||||
@@ -53,6 +55,14 @@ class CopyTradingFilterService(
|
||||
}
|
||||
}
|
||||
|
||||
// 1.5. 市场截止时间检查(如果配置了市场截止时间限制)
|
||||
if (copyTrading.maxMarketEndDate != null) {
|
||||
val marketEndDateCheck = checkMarketEndDate(copyTrading, marketEndDate)
|
||||
if (!marketEndDateCheck.isPassed) {
|
||||
return marketEndDateCheck
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 价格区间检查(如果配置了价格区间)
|
||||
if (tradePrice != null) {
|
||||
val priceRangeCheck = checkPriceRange(copyTrading, tradePrice)
|
||||
@@ -358,5 +368,38 @@ class CopyTradingFilterService(
|
||||
return FilterResult.maxPositionValueFailed("仓位检查异常: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查市场截止时间
|
||||
* @param copyTrading 跟单配置
|
||||
* @param marketEndDate 市场截止时间(毫秒时间戳)
|
||||
* @return 过滤结果
|
||||
*/
|
||||
private fun checkMarketEndDate(
|
||||
copyTrading: CopyTrading,
|
||||
marketEndDate: Long?
|
||||
): FilterResult {
|
||||
// 如果未配置市场截止时间限制,直接通过
|
||||
if (copyTrading.maxMarketEndDate == null) {
|
||||
return FilterResult.passed()
|
||||
}
|
||||
|
||||
// 如果没有市场截止时间,无法检查,为了安全起见,不通过
|
||||
if (marketEndDate == null) {
|
||||
return FilterResult.marketEndDateFailed("市场缺少截止时间信息,无法进行市场截止时间检查")
|
||||
}
|
||||
|
||||
// 检查:市场截止时间 - 当前时间 <= 最大限制时间
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val remainingTime = marketEndDate - currentTime
|
||||
|
||||
if (remainingTime > copyTrading.maxMarketEndDate) {
|
||||
return FilterResult.marketEndDateFailed(
|
||||
"市场截止时间超出限制: 剩余时间=${remainingTime}ms (${remainingTime / (1000 * 60 * 60)}小时) > 最大限制=${copyTrading.maxMarketEndDate}ms (${copyTrading.maxMarketEndDate / (1000 * 60 * 60)}小时)"
|
||||
)
|
||||
}
|
||||
|
||||
return FilterResult.passed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-4
@@ -94,7 +94,8 @@ class CopyTradingService(
|
||||
maxPositionValue = request.maxPositionValue?.toSafeBigDecimal(),
|
||||
maxPositionCount = request.maxPositionCount,
|
||||
keywordFilterMode = request.keywordFilterMode ?: "DISABLED",
|
||||
keywords = convertKeywordsToJson(request.keywords)
|
||||
keywords = convertKeywordsToJson(request.keywords),
|
||||
maxMarketEndDate = request.maxMarketEndDate
|
||||
)
|
||||
} else {
|
||||
// 手动输入(所有字段必须提供)
|
||||
@@ -124,7 +125,8 @@ class CopyTradingService(
|
||||
maxPositionValue = request.maxPositionValue?.toSafeBigDecimal(),
|
||||
maxPositionCount = request.maxPositionCount,
|
||||
keywordFilterMode = request.keywordFilterMode ?: "DISABLED",
|
||||
keywords = convertKeywordsToJson(request.keywords)
|
||||
keywords = convertKeywordsToJson(request.keywords),
|
||||
maxMarketEndDate = request.maxMarketEndDate
|
||||
)
|
||||
}
|
||||
|
||||
@@ -156,7 +158,8 @@ class CopyTradingService(
|
||||
keywordFilterMode = config.keywordFilterMode,
|
||||
keywords = config.keywords,
|
||||
configName = configName,
|
||||
pushFailedOrders = request.pushFailedOrders ?: false
|
||||
pushFailedOrders = request.pushFailedOrders ?: false,
|
||||
maxMarketEndDate = config.maxMarketEndDate
|
||||
)
|
||||
|
||||
val saved = copyTradingRepository.save(copyTrading)
|
||||
@@ -233,6 +236,7 @@ class CopyTradingService(
|
||||
},
|
||||
configName = configName,
|
||||
pushFailedOrders = request.pushFailedOrders ?: copyTrading.pushFailedOrders,
|
||||
maxMarketEndDate = request.maxMarketEndDate ?: copyTrading.maxMarketEndDate,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@@ -446,6 +450,7 @@ class CopyTradingService(
|
||||
keywords = convertJsonToKeywords(copyTrading.keywords),
|
||||
configName = copyTrading.configName,
|
||||
pushFailedOrders = copyTrading.pushFailedOrders,
|
||||
maxMarketEndDate = copyTrading.maxMarketEndDate,
|
||||
createdAt = copyTrading.createdAt,
|
||||
updatedAt = copyTrading.updatedAt
|
||||
)
|
||||
@@ -506,6 +511,7 @@ class CopyTradingService(
|
||||
val maxPositionValue: BigDecimal?,
|
||||
val maxPositionCount: Int?,
|
||||
val keywordFilterMode: String,
|
||||
val keywords: String? // JSON 字符串
|
||||
val keywords: String?, // JSON 字符串
|
||||
val maxMarketEndDate: Long? // 市场截止时间限制(毫秒时间戳)
|
||||
)
|
||||
}
|
||||
|
||||
+9
-1
@@ -23,7 +23,9 @@ enum class FilterStatus {
|
||||
/** 失败:超过最大仓位数量 */
|
||||
FAILED_MAX_POSITION_COUNT,
|
||||
/** 失败:关键字过滤 */
|
||||
FAILED_KEYWORD_FILTER
|
||||
FAILED_KEYWORD_FILTER,
|
||||
/** 失败:市场截止时间超出限制 */
|
||||
FAILED_MARKET_END_DATE
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,6 +99,12 @@ data class FilterResult(
|
||||
status = FilterStatus.FAILED_KEYWORD_FILTER,
|
||||
reason = reason
|
||||
)
|
||||
|
||||
/** 市场截止时间超出限制 */
|
||||
fun marketEndDateFailed(reason: String) = FilterResult(
|
||||
status = FilterStatus.FAILED_MARKET_END_DATE,
|
||||
reason = reason
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-4
@@ -266,14 +266,18 @@ open class CopyOrderTrackingService(
|
||||
// 计算跟单金额(USDC)= 买入数量 × 价格
|
||||
val copyOrderAmount = buyQuantity.multi(tradePrice)
|
||||
|
||||
// 如果启用了关键字过滤,需要先获取市场标题
|
||||
// 如果启用了关键字过滤或市场截止时间过滤,需要先获取市场信息
|
||||
var marketTitle: String? = null
|
||||
if (copyTrading.keywordFilterMode != null && copyTrading.keywordFilterMode != "DISABLED") {
|
||||
var marketEndDate: Long? = null
|
||||
val needMarketInfo = copyTrading.keywordFilterMode != "DISABLED" || copyTrading.maxMarketEndDate != null
|
||||
|
||||
if (needMarketInfo) {
|
||||
try {
|
||||
val market = marketService.getMarket(trade.market)
|
||||
marketTitle = market?.title
|
||||
marketEndDate = market?.endDate
|
||||
} catch (e: Exception) {
|
||||
logger.warn("获取市场标题失败(关键字过滤需要): ${e.message}", e)
|
||||
logger.warn("获取市场信息失败(关键字过滤/市场截止时间检查需要): ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +285,7 @@ open class CopyOrderTrackingService(
|
||||
// 传入 Leader 交易价格,用于价格区间检查
|
||||
// 传入跟单金额和市场ID,用于仓位检查(按市场检查仓位)
|
||||
// 传入市场标题,用于关键字过滤
|
||||
// 传入市场截止时间,用于市场截止时间检查
|
||||
// 订单簿只请求一次,返回给后续逻辑使用
|
||||
val filterResult = filterService.checkFilters(
|
||||
copyTrading,
|
||||
@@ -288,7 +293,8 @@ open class CopyOrderTrackingService(
|
||||
tradePrice = tradePrice,
|
||||
copyOrderAmount = copyOrderAmount,
|
||||
marketId = trade.market,
|
||||
marketTitle = marketTitle
|
||||
marketTitle = marketTitle,
|
||||
marketEndDate = marketEndDate
|
||||
)
|
||||
val orderbook = filterResult.orderbook // 获取订单簿(如果需要)
|
||||
if (!filterResult.isPassed) {
|
||||
@@ -1403,6 +1409,7 @@ open class CopyOrderTrackingService(
|
||||
FilterStatus.FAILED_MAX_POSITION_VALUE -> "MAX_POSITION_VALUE"
|
||||
FilterStatus.FAILED_MAX_POSITION_COUNT -> "MAX_POSITION_COUNT"
|
||||
FilterStatus.FAILED_KEYWORD_FILTER -> "KEYWORD_FILTER"
|
||||
FilterStatus.FAILED_MARKET_END_DATE -> "MARKET_END_DATE"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
-- ============================================
|
||||
-- V22: 添加市场截止时间筛选功能
|
||||
-- 1. 为 copy_trading 表添加 max_market_end_date 字段
|
||||
-- 2. 为 markets 表添加 end_date 字段并清空已有数据
|
||||
-- ============================================
|
||||
|
||||
-- 1. 添加市场截止时间筛选字段到 copy_trading 表
|
||||
-- 仅跟单截止时间小于设置时间的订单
|
||||
-- 存储毫秒时间戳,NULL 表示不启用此筛选
|
||||
ALTER TABLE copy_trading
|
||||
ADD COLUMN max_market_end_date BIGINT NULL COMMENT '市场截止时间限制(毫秒时间戳),仅跟单截止时间小于此时间的订单,NULL表示不启用';
|
||||
|
||||
-- 2. 为 markets 表添加 end_date 字段
|
||||
-- 删除已有数据,让系统重新从 API 获取,确保所有数据的 end_date 都正确填充
|
||||
DELETE FROM markets;
|
||||
|
||||
-- 添加市场截止时间字段
|
||||
ALTER TABLE markets
|
||||
ADD COLUMN end_date BIGINT NULL COMMENT '市场截止时间(毫秒时间戳),NULL表示未设置' AFTER archived;
|
||||
|
||||
Reference in New Issue
Block a user