From 3369dbb248d936f7c64deb48995a0c81bc8987bf Mon Sep 17 00:00:00 2001 From: WrBug Date: Fri, 5 Dec 2025 23:52:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E5=8C=BA=E9=97=B4=E8=BF=87=E6=BB=A4=E5=8A=9F=E8=83=BD=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加价格区间过滤功能(minPrice/maxPrice) - 数据库迁移:添加价格区间字段到 copy_trading 和 copy_trading_templates 表 - 后端:实体类、DTO、服务层支持价格区间配置和过滤 - 前端:模板和配置页面添加价格区间配置UI - 过滤逻辑:在订单创建前检查价格是否在配置区间内 - 优化价格区间文案,使其更易懂 - 明确说明是 Leader 交易价格 - 详细说明三种配置方式(区间、只填最低价、只填最高价) - 补充多语言翻译 - 为 zh-CN、zh-TW、en 添加价格区间相关翻译 - 添加价格区间过滤类型翻译 - 添加通用翻译(prev、next、items、total) - 修复复制模板功能 - 补充缺失的过滤条件字段(minOrderDepth、maxSpread、minOrderbookDepth、minPrice、maxPrice) - 在复制模板表单中添加所有过滤条件配置项 - 已过滤订单列表优化 - 添加移动端卡片样式布局 - 在筛选下拉菜单中添加价格区间过滤类型选项 - 订单页面返回按钮优化 - 买入订单、卖出订单、匹配关系页面的返回按钮改为返回上一页(navigate(-1)) - 使用多语言支持返回按钮文本 --- .../wrbug/polymarketbot/dto/CopyTradingDto.kt | 10 +- .../dto/CopyTradingTemplateDto.kt | 14 +- .../wrbug/polymarketbot/entity/CopyTrading.kt | 6 + .../entity/CopyTradingTemplate.kt | 6 + .../service/CopyOrderTrackingService.kt | 5 +- .../service/CopyTradingFilterService.kt | 49 ++++- .../service/CopyTradingService.kt | 20 +- .../service/CopyTradingStatisticsService.kt | 89 ++++++-- .../service/CopyTradingTemplateService.kt | 12 +- .../migration/V6__add_price_range_filter.sql | 15 ++ frontend/src/components/Layout.tsx | 10 +- frontend/src/locales/en/common.json | 30 ++- frontend/src/locales/zh-CN/common.json | 36 +++- frontend/src/locales/zh-TW/common.json | 30 ++- frontend/src/pages/CopyTradingAdd.tsx | 42 +++- frontend/src/pages/CopyTradingBuyOrders.tsx | 6 +- frontend/src/pages/CopyTradingEdit.tsx | 42 +++- frontend/src/pages/CopyTradingList.tsx | 46 +++-- .../src/pages/CopyTradingMatchedOrders.tsx | 6 +- frontend/src/pages/CopyTradingSellOrders.tsx | 6 +- frontend/src/pages/CopyTradingStatistics.tsx | 23 +-- frontend/src/pages/FilteredOrdersList.tsx | 195 ++++++++++++++++-- frontend/src/pages/TemplateAdd.tsx | 38 +++- frontend/src/pages/TemplateEdit.tsx | 42 +++- frontend/src/pages/TemplateList.tsx | 90 +++++++- frontend/src/types/index.ts | 12 +- 26 files changed, 753 insertions(+), 127 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V6__add_price_range_filter.sql diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingDto.kt index 11bc2bb..483eed1 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingDto.kt @@ -32,7 +32,9 @@ data class CopyTradingCreateRequest( // 过滤条件 val minOrderDepth: String? = null, // 最小订单深度(USDC金额),NULL表示不启用 val maxSpread: String? = null, // 最大价差(绝对价格),NULL表示不启用 - val minOrderbookDepth: String? = null // 最小订单簿深度(USDC金额),NULL表示不启用 + val minOrderbookDepth: String? = null, // 最小订单簿深度(USDC金额),NULL表示不启用 + val minPrice: String? = null, // 最低价格(可选),NULL表示不限制最低价 + val maxPrice: String? = null // 最高价格(可选),NULL表示不限制最高价 ) /** @@ -59,7 +61,9 @@ data class CopyTradingUpdateRequest( // 过滤条件 val minOrderDepth: String? = null, val maxSpread: String? = null, - val minOrderbookDepth: String? = null + val minOrderbookDepth: String? = null, + val minPrice: String? = null, // 最低价格(可选),NULL表示不限制最低价 + val maxPrice: String? = null // 最高价格(可选),NULL表示不限制最高价 ) /** @@ -124,6 +128,8 @@ data class CopyTradingDto( val minOrderDepth: String?, val maxSpread: String?, val minOrderbookDepth: String?, + val minPrice: String?, // 最低价格(可选),NULL表示不限制最低价 + val maxPrice: String?, // 最高价格(可选),NULL表示不限制最高价 val createdAt: Long, val updatedAt: Long ) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingTemplateDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingTemplateDto.kt index 796164e..8259736 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingTemplateDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingTemplateDto.kt @@ -22,7 +22,9 @@ data class TemplateCreateRequest( // 过滤条件 val minOrderDepth: String? = null, // 最小订单深度(USDC金额),NULL表示不启用 val maxSpread: String? = null, // 最大价差(绝对价格),NULL表示不启用 - val minOrderbookDepth: String? = null // 最小订单簿深度(USDC金额),NULL表示不启用 + val minOrderbookDepth: String? = null, // 最小订单簿深度(USDC金额),NULL表示不启用 + val minPrice: String? = null, // 最低价格(可选),NULL表示不限制最低价 + val maxPrice: String? = null // 最高价格(可选),NULL表示不限制最高价 ) /** @@ -48,7 +50,9 @@ data class TemplateUpdateRequest( // 过滤条件 val minOrderDepth: String? = null, // 最小订单深度(USDC金额),NULL表示不启用 val maxSpread: String? = null, // 最大价差(绝对价格),NULL表示不启用 - val minOrderbookDepth: String? = null // 最小订单簿深度(USDC金额),NULL表示不启用 + val minOrderbookDepth: String? = null, // 最小订单簿深度(USDC金额),NULL表示不启用 + val minPrice: String? = null, // 最低价格(可选),NULL表示不限制最低价 + val maxPrice: String? = null // 最高价格(可选),NULL表示不限制最高价 ) /** @@ -81,7 +85,9 @@ data class TemplateCopyRequest( // 过滤条件 val minOrderDepth: String? = null, // 最小订单深度(USDC金额),NULL表示不启用 val maxSpread: String? = null, // 最大价差(绝对价格),NULL表示不启用 - val minOrderbookDepth: String? = null // 最小订单簿深度(USDC金额),NULL表示不启用 + val minOrderbookDepth: String? = null, // 最小订单簿深度(USDC金额),NULL表示不启用 + val minPrice: String? = null, // 最低价格(可选),NULL表示不限制最低价 + val maxPrice: String? = null // 最高价格(可选),NULL表示不限制最高价 ) /** @@ -115,6 +121,8 @@ data class TemplateDto( val minOrderDepth: String?, val maxSpread: String?, val minOrderbookDepth: String?, + val minPrice: String?, // 最低价格(可选),NULL表示不限制最低价 + val maxPrice: String?, // 最高价格(可选),NULL表示不限制最高价 val createdAt: Long, val updatedAt: Long ) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTrading.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTrading.kt index 6065d5e..2acdbc0 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTrading.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTrading.kt @@ -81,6 +81,12 @@ data class CopyTrading( @Column(name = "min_orderbook_depth", precision = 20, scale = 8) val minOrderbookDepth: BigDecimal? = null, // 最小订单簿深度(USDC金额),NULL表示不启用 + @Column(name = "min_price", precision = 20, scale = 8) + val minPrice: BigDecimal? = null, // 最低价格(可选),NULL表示不限制最低价 + + @Column(name = "max_price", precision = 20, scale = 8) + val maxPrice: BigDecimal? = null, // 最高价格(可选),NULL表示不限制最高价 + @Column(name = "created_at", nullable = false) val createdAt: Long = System.currentTimeMillis(), diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTradingTemplate.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTradingTemplate.kt index ac0c665..7bd650a 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTradingTemplate.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTradingTemplate.kt @@ -69,6 +69,12 @@ data class CopyTradingTemplate( @Column(name = "min_orderbook_depth", precision = 20, scale = 8) val minOrderbookDepth: BigDecimal? = null, // 最小订单簿深度(USDC金额),NULL表示不启用 + @Column(name = "min_price", precision = 20, scale = 8) + val minPrice: BigDecimal? = null, // 最低价格(可选),NULL表示不限制最低价 + + @Column(name = "max_price", precision = 20, scale = 8) + val maxPrice: BigDecimal? = null, // 最高价格(可选),NULL表示不限制最高价 + @Column(name = "created_at", nullable = false) val createdAt: Long = System.currentTimeMillis(), diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyOrderTrackingService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyOrderTrackingService.kt index 79a9786..18bf107 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyOrderTrackingService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyOrderTrackingService.kt @@ -208,7 +208,9 @@ class CopyOrderTrackingService( val tokenId = tokenIdResult.getOrNull() ?: continue // 过滤条件检查(在计算订单参数之前) - val filterCheck = filterService.checkFilters(copyTrading, tokenId, isBuyOrder = true) + // 传入 Leader 交易价格,用于价格区间检查 + val tradePrice = trade.price.toSafeBigDecimal() + val filterCheck = filterService.checkFilters(copyTrading, tokenId, isBuyOrder = true, tradePrice = tradePrice) if (!filterCheck.first) { logger.warn("过滤条件检查失败,跳过创建订单: copyTradingId=${copyTrading.id}, reason=${filterCheck.second}") @@ -1139,6 +1141,7 @@ class CopyOrderTrackingService( */ private fun extractFilterType(filterReason: String): String { return when { + filterReason.contains("价格低于最低限制", ignoreCase = true) || filterReason.contains("价格高于最高限制", ignoreCase = true) -> "PRICE_RANGE" filterReason.contains("订单深度不足", ignoreCase = true) -> "ORDER_DEPTH" filterReason.contains("价差过大", ignoreCase = true) -> "SPREAD" filterReason.contains("订单簿深度不足", ignoreCase = true) -> "ORDERBOOK_DEPTH" diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingFilterService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingFilterService.kt index d551cb0..e9dbbd1 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingFilterService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingFilterService.kt @@ -30,12 +30,21 @@ class CopyTradingFilterService( suspend fun checkFilters( copyTrading: CopyTrading, tokenId: String, - isBuyOrder: Boolean + isBuyOrder: Boolean, + tradePrice: BigDecimal? = null // Leader 交易价格,用于价格区间检查 ): Pair { - // 1. 价格合理性检查(基础检查,无需配置) + // 1. 价格区间检查(如果配置了价格区间) + if (tradePrice != null) { + val priceRangeCheck = checkPriceRange(copyTrading, tradePrice) + if (!priceRangeCheck.first) { + return priceRangeCheck + } + } + + // 2. 价格合理性检查(基础检查,无需配置) // 这个检查在获取订单簿时进行,如果价格不在 0.01-0.99 范围内,订单簿获取会失败 - // 2. 获取订单簿 + // 3. 获取订单簿 val orderbookResult = clobService.getOrderbookByTokenId(tokenId) if (!orderbookResult.isSuccess) { val error = orderbookResult.exceptionOrNull() @@ -47,19 +56,19 @@ class CopyTradingFilterService( return Pair(false, "订单簿为空") } - // 3. 买一卖一价差过滤 + // 4. 买一卖一价差过滤 val spreadCheck = checkSpread(copyTrading, orderbook) if (!spreadCheck.first) { return spreadCheck } - // 4. 订单深度过滤 + // 5. 订单深度过滤 val depthCheck = checkOrderDepth(copyTrading, orderbook, isBuyOrder) if (!depthCheck.first) { return depthCheck } - // 5. 最小订单簿深度过滤(可选) + // 6. 最小订单簿深度过滤(可选) val orderbookDepthCheck = checkOrderbookDepth(copyTrading, orderbook, isBuyOrder) if (!orderbookDepthCheck.first) { return orderbookDepthCheck @@ -68,6 +77,34 @@ class CopyTradingFilterService( return Pair(true, "") } + /** + * 检查价格区间 + * @param copyTrading 跟单配置 + * @param tradePrice Leader 交易价格 + * @return Pair<是否通过, 失败原因> + */ + private fun checkPriceRange( + copyTrading: CopyTrading, + tradePrice: BigDecimal + ): Pair { + // 如果未配置价格区间,直接通过 + if (copyTrading.minPrice == null && copyTrading.maxPrice == null) { + return Pair(true, "") + } + + // 检查最低价格 + if (copyTrading.minPrice != null && tradePrice.lt(copyTrading.minPrice)) { + return Pair(false, "价格低于最低限制: $tradePrice < ${copyTrading.minPrice}") + } + + // 检查最高价格 + if (copyTrading.maxPrice != null && tradePrice.gt(copyTrading.maxPrice)) { + return Pair(false, "价格高于最高限制: $tradePrice > ${copyTrading.maxPrice}") + } + + return Pair(true, "") + } + /** * 检查买一卖一价差 * bestBid: 买盘中的最高价格(最大值) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingService.kt index c257090..0c38bac 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingService.kt @@ -76,7 +76,9 @@ class CopyTradingService( supportSell = request.supportSell ?: template.supportSell, minOrderDepth = request.minOrderDepth?.toSafeBigDecimal() ?: template.minOrderDepth, maxSpread = request.maxSpread?.toSafeBigDecimal() ?: template.maxSpread, - minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal() ?: template.minOrderbookDepth + minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal() ?: template.minOrderbookDepth, + minPrice = request.minPrice?.toSafeBigDecimal() ?: template.minPrice, + maxPrice = request.maxPrice?.toSafeBigDecimal() ?: template.maxPrice ) } else { // 手动输入(所有字段必须提供) @@ -101,7 +103,9 @@ class CopyTradingService( supportSell = request.supportSell ?: true, minOrderDepth = request.minOrderDepth?.toSafeBigDecimal(), maxSpread = request.maxSpread?.toSafeBigDecimal(), - minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal() + minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal(), + minPrice = request.minPrice?.toSafeBigDecimal(), + maxPrice = request.maxPrice?.toSafeBigDecimal() ) } @@ -126,7 +130,9 @@ class CopyTradingService( supportSell = config.supportSell, minOrderDepth = config.minOrderDepth, maxSpread = config.maxSpread, - minOrderbookDepth = config.minOrderbookDepth + minOrderbookDepth = config.minOrderbookDepth, + minPrice = config.minPrice, + maxPrice = config.maxPrice ) val saved = copyTradingRepository.save(copyTrading) @@ -178,6 +184,8 @@ class CopyTradingService( minOrderDepth = request.minOrderDepth?.toSafeBigDecimal() ?: copyTrading.minOrderDepth, maxSpread = request.maxSpread?.toSafeBigDecimal() ?: copyTrading.maxSpread, minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal() ?: copyTrading.minOrderbookDepth, + minPrice = request.minPrice?.toSafeBigDecimal() ?: copyTrading.minPrice, + maxPrice = request.maxPrice?.toSafeBigDecimal() ?: copyTrading.maxPrice, updatedAt = System.currentTimeMillis() ) @@ -380,6 +388,8 @@ class CopyTradingService( minOrderDepth = copyTrading.minOrderDepth?.toPlainString(), maxSpread = copyTrading.maxSpread?.toPlainString(), minOrderbookDepth = copyTrading.minOrderbookDepth?.toPlainString(), + minPrice = copyTrading.minPrice?.toPlainString(), + maxPrice = copyTrading.maxPrice?.toPlainString(), createdAt = copyTrading.createdAt, updatedAt = copyTrading.updatedAt ) @@ -405,6 +415,8 @@ class CopyTradingService( val supportSell: Boolean, val minOrderDepth: BigDecimal?, val maxSpread: BigDecimal?, - val minOrderbookDepth: BigDecimal? + val minOrderbookDepth: BigDecimal?, + val minPrice: BigDecimal?, + val maxPrice: BigDecimal? ) } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingStatisticsService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingStatisticsService.kt index 8cfa339..df837d9 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingStatisticsService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingStatisticsService.kt @@ -29,7 +29,8 @@ class CopyTradingStatisticsService( private val sellMatchDetailRepository: SellMatchDetailRepository, private val accountRepository: AccountRepository, private val leaderRepository: LeaderRepository, - private val accountService: AccountService + private val accountService: AccountService, + private val blockchainService: com.wrbug.polymarketbot.service.BlockchainService ) { private val logger = LoggerFactory.getLogger(CopyTradingStatisticsService::class.java) @@ -59,11 +60,14 @@ class CopyTradingStatisticsService( // 6. 计算统计信息 val statistics = calculateStatistics(buyOrders, sellRecords, matchDetails) - // 7. 获取当前市场价格(用于计算未实现盈亏) + // 7. 获取链上实际持仓(用于准确计算未实现盈亏,考虑手动卖出的情况) + val actualPositions = getActualPositions(account) + + // 8. 获取当前市场价格(用于计算未实现盈亏) val currentPrice = getCurrentMarketPrice(buyOrders) - // 8. 计算未实现盈亏 - val unrealizedPnl = calculateUnrealizedPnl(buyOrders, currentPrice) + // 9. 计算未实现盈亏(使用链上实际持仓,而不是 remainingQuantity) + val unrealizedPnl = calculateUnrealizedPnl(buyOrders, currentPrice, actualPositions) // 9. 构建响应 val response = CopyTradingStatisticsResponse( @@ -314,49 +318,104 @@ class CopyTradingStatisticsService( /** * 获取当前市场价格 + * 按 (marketId, outcomeIndex) 组合获取价格,支持多元市场 */ private suspend fun getCurrentMarketPrice(buyOrders: List): Map { val prices = mutableMapOf() - // 获取所有不同的市场ID - val marketIds = buyOrders.map { it.marketId }.distinct() + // 获取所有不同的 (marketId, outcomeIndex) 组合 + val marketOutcomePairs = buyOrders + .filter { it.outcomeIndex != null } + .map { Pair(it.marketId, it.outcomeIndex!!) } + .distinct() - for (marketId in marketIds) { + for ((marketId, outcomeIndex) in marketOutcomePairs) { try { - val result = accountService.getMarketPrice(marketId) + // 传递 outcomeIndex 参数,确保获取对应 outcome 的价格 + val result = accountService.getMarketPrice(marketId, outcomeIndex) result.onSuccess { response -> // 使用中间价,如果没有则使用最后价格 val price = response.midpoint ?: response.lastPrice if (price != null) { - prices[marketId] = price + // 使用 "marketId:outcomeIndex" 作为 key + val key = "$marketId:$outcomeIndex" + prices[key] = price } } } catch (e: Exception) { - logger.warn("获取市场价格失败: marketId=$marketId", e) + logger.warn("获取市场价格失败: marketId=$marketId, outcomeIndex=$outcomeIndex", e) } } return prices } + /** + * 获取链上实际持仓 + * 按 (marketId, outcomeIndex) 组合返回实际持仓数量 + */ + private suspend fun getActualPositions(account: com.wrbug.polymarketbot.entity.Account?): Map { + val positions = mutableMapOf() + + if (account == null || account.proxyAddress.isBlank()) { + return positions + } + + try { + val positionsResult = blockchainService.getPositions(account.proxyAddress) + if (positionsResult.isSuccess) { + val positionList = positionsResult.getOrNull() ?: emptyList() + for (pos in positionList) { + // 只处理有 conditionId 和 outcomeIndex 的仓位 + if (pos.conditionId != null && pos.outcomeIndex != null && pos.size != null) { + val key = "${pos.conditionId}:${pos.outcomeIndex}" + val size = pos.size.toSafeBigDecimal() + // 如果 size > 0,表示有持仓;如果 size < 0,表示做空(取绝对值) + positions[key] = size.abs() + } + } + } + } catch (e: Exception) { + logger.warn("获取链上持仓失败: accountId=${account.id}, error=${e.message}", e) + } + + return positions + } + /** * 计算未实现盈亏 + * 使用链上实际持仓数量,而不是 remainingQuantity(考虑手动卖出的情况) */ private fun calculateUnrealizedPnl( buyOrders: List, - currentPrices: Map + currentPrices: Map, + actualPositions: Map ): String { var totalUnrealizedPnl = BigDecimal.ZERO for (order in buyOrders) { - val remainingQty = order.remainingQuantity.toSafeBigDecimal() - if (remainingQty.lte(BigDecimal.ZERO)) continue + // 如果没有 outcomeIndex,跳过(无法确定价格和持仓) + if (order.outcomeIndex == null) { + logger.warn("订单缺少 outcomeIndex,跳过未实现盈亏计算: orderId=${order.buyOrderId}, marketId=${order.marketId}") + continue + } - val currentPrice = currentPrices[order.marketId]?.toSafeBigDecimal() + // 使用 "marketId:outcomeIndex" 作为 key + val key = "${order.marketId}:${order.outcomeIndex}" + + // 获取链上实际持仓数量(如果存在),否则使用 remainingQuantity + val actualQty = actualPositions[key] ?: order.remainingQuantity.toSafeBigDecimal() + + // 如果实际持仓 <= 0,说明已全部卖出(包括手动卖出),跳过未实现盈亏计算 + if (actualQty.lte(BigDecimal.ZERO)) continue + + // 获取当前市场价格 + val currentPrice = currentPrices[key]?.toSafeBigDecimal() ?: continue // 如果没有当前价格,跳过 val buyPrice = order.price.toSafeBigDecimal() - val unrealizedPnl = currentPrice.subtract(buyPrice).multi(remainingQty) + // 使用实际持仓数量计算未实现盈亏 + val unrealizedPnl = currentPrice.subtract(buyPrice).multi(actualQty) totalUnrealizedPnl = totalUnrealizedPnl.add(unrealizedPnl) } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingTemplateService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingTemplateService.kt index 0671af9..f745dff 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingTemplateService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingTemplateService.kt @@ -61,7 +61,9 @@ class CopyTradingTemplateService( supportSell = request.supportSell ?: true, minOrderDepth = request.minOrderDepth?.toSafeBigDecimal(), maxSpread = request.maxSpread?.toSafeBigDecimal(), - minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal() + minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal(), + minPrice = request.minPrice?.toSafeBigDecimal(), + maxPrice = request.maxPrice?.toSafeBigDecimal() ) val saved = templateRepository.save(template) @@ -119,6 +121,8 @@ class CopyTradingTemplateService( minOrderDepth = request.minOrderDepth?.toSafeBigDecimal() ?: template.minOrderDepth, maxSpread = request.maxSpread?.toSafeBigDecimal() ?: template.maxSpread, minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal() ?: template.minOrderbookDepth, + minPrice = request.minPrice?.toSafeBigDecimal() ?: template.minPrice, + maxPrice = request.maxPrice?.toSafeBigDecimal() ?: template.maxPrice, updatedAt = System.currentTimeMillis() ) @@ -183,7 +187,9 @@ class CopyTradingTemplateService( supportSell = request.supportSell ?: sourceTemplate.supportSell, minOrderDepth = request.minOrderDepth?.toSafeBigDecimal() ?: sourceTemplate.minOrderDepth, maxSpread = request.maxSpread?.toSafeBigDecimal() ?: sourceTemplate.maxSpread, - minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal() ?: sourceTemplate.minOrderbookDepth + minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal() ?: sourceTemplate.minOrderbookDepth, + minPrice = request.minPrice?.toSafeBigDecimal() ?: sourceTemplate.minPrice, + maxPrice = request.maxPrice?.toSafeBigDecimal() ?: sourceTemplate.maxPrice ) val saved = templateRepository.save(newTemplate) @@ -256,6 +262,8 @@ class CopyTradingTemplateService( minOrderDepth = template.minOrderDepth?.toPlainString(), maxSpread = template.maxSpread?.toPlainString(), minOrderbookDepth = template.minOrderbookDepth?.toPlainString(), + minPrice = template.minPrice?.toPlainString(), + maxPrice = template.maxPrice?.toPlainString(), createdAt = template.createdAt, updatedAt = template.updatedAt ) diff --git a/backend/src/main/resources/db/migration/V6__add_price_range_filter.sql b/backend/src/main/resources/db/migration/V6__add_price_range_filter.sql new file mode 100644 index 0000000..dc3f426 --- /dev/null +++ b/backend/src/main/resources/db/migration/V6__add_price_range_filter.sql @@ -0,0 +1,15 @@ +-- ============================================ +-- V6: 添加价格区间过滤字段 +-- 用于配置价格区间,仅在指定价格区间内的订单才会下单 +-- ============================================ + +-- 添加价格区间字段到跟单配置表 +ALTER TABLE copy_trading +ADD COLUMN min_price DECIMAL(20, 8) NULL COMMENT '最低价格(可选),NULL表示不限制最低价', +ADD COLUMN max_price DECIMAL(20, 8) NULL COMMENT '最高价格(可选),NULL表示不限制最高价'; + +-- 添加价格区间字段到跟单模板表 +ALTER TABLE copy_trading_templates +ADD COLUMN min_price DECIMAL(20, 8) NULL COMMENT '最低价格(可选),NULL表示不限制最低价', +ADD COLUMN max_price DECIMAL(20, 8) NULL COMMENT '最高价格(可选),NULL表示不限制最高价'; + diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index e6ec9d5..4a0aeb0 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -84,6 +84,11 @@ const Layout: React.FC = ({ children }) => { icon: , label: t('menu.copyTrading'), children: [ + { + key: '/copy-trading', + icon: , + label: t('menu.copyTradingConfig') + }, { key: '/leaders', icon: , @@ -93,11 +98,6 @@ const Layout: React.FC = ({ children }) => { key: '/templates', icon: , label: t('menu.templates') - }, - { - key: '/copy-trading', - icon: , - label: t('menu.copyTradingConfig') } ] }, diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json index b6bcbdf..5bc3def 100644 --- a/frontend/src/locales/en/common.json +++ b/frontend/src/locales/en/common.json @@ -26,7 +26,11 @@ "disabled": "Disabled", "noData": "No Data", "saveConfig": "Save Config", - "refreshConfig": "Refresh Config" + "refreshConfig": "Refresh Config", + "total": "Total", + "items": "items", + "prev": "Previous", + "next": "Next" }, "account": { "title": "Account Management", @@ -478,6 +482,11 @@ "minOrderbookDepth": "Min Orderbook Depth (USDC)", "minOrderbookDepthTooltip": "Minimum orderbook depth (USDC amount), NULL means this filter is not enabled. Check depth of first N levels", "minOrderbookDepthPlaceholder": "For example: 50 (optional, leave empty to disable)", + "priceRangeFilter": "Price Range Filter", + "priceRange": "Price Range", + "priceRangeTooltip": "Only copy orders where Leader's trade price is within the specified range. Leave empty to disable. Examples: Fill 0.11 and 0.89 means only copy orders with price between 0.11 and 0.89; Fill only max price 0.89 means only copy orders with price below 0.89; Fill only min price 0.11 means only copy orders with price above 0.11.", + "minPricePlaceholder": "Min Price (leave empty for no limit)", + "maxPricePlaceholder": "Max Price (leave empty for no limit)", "supportSell": "Support Sell", "supportSellTooltip": "Whether to copy Leader's sell orders. Enabled: copy both Leader's buy and sell orders; Disabled: only copy Leader's buy orders, ignore sell orders.", "create": "Create Template", @@ -530,6 +539,11 @@ "minOrderbookDepth": "Min Orderbook Depth (USDC)", "minOrderbookDepthTooltip": "Minimum orderbook depth (USDC amount), NULL means this filter is not enabled. Check depth of first N levels", "minOrderbookDepthPlaceholder": "For example: 50 (optional, leave empty to disable)", + "priceRangeFilter": "Price Range Filter", + "priceRange": "Price Range", + "priceRangeTooltip": "Only copy orders where Leader's trade price is within the specified range. Leave empty to disable. Examples: Fill 0.11 and 0.89 means only copy orders with price between 0.11 and 0.89; Fill only max price 0.89 means only copy orders with price below 0.89; Fill only min price 0.11 means only copy orders with price above 0.11.", + "minPricePlaceholder": "Min Price (leave empty for no limit)", + "maxPricePlaceholder": "Max Price (leave empty for no limit)", "supportSell": "Support Sell", "supportSellTooltip": "Whether to copy Leader's sell orders. Enabled: copy both Leader's buy and sell orders; Disabled: only copy Leader's buy orders, ignore sell orders.", "invalidNumber": "Please enter a valid number" @@ -590,6 +604,11 @@ "minOrderbookDepth": "Min Orderbook Depth (USDC)", "minOrderbookDepthTooltip": "Minimum orderbook depth (USDC amount), NULL means this filter is not enabled. Check depth of first N levels", "minOrderbookDepthPlaceholder": "For example: 50 (optional, leave empty to disable)", + "priceRangeFilter": "Price Range Filter", + "priceRange": "Price Range", + "priceRangeTooltip": "Only copy orders where Leader's trade price is within the specified range. Leave empty to disable. Examples: Fill 0.11 and 0.89 means only copy orders with price between 0.11 and 0.89; Fill only max price 0.89 means only copy orders with price below 0.89; Fill only min price 0.11 means only copy orders with price above 0.11.", + "minPricePlaceholder": "Min Price (leave empty for no limit)", + "maxPricePlaceholder": "Max Price (leave empty for no limit)", "supportSell": "Support Sell", "supportSellTooltip": "Whether to copy Leader's sell orders", "create": "Create Copy Trading Config", @@ -648,6 +667,11 @@ "minOrderbookDepth": "Min Orderbook Depth (USDC)", "minOrderbookDepthTooltip": "Minimum orderbook depth (USDC amount), NULL means this filter is not enabled", "minOrderbookDepthPlaceholder": "For example: 50 (optional, leave empty to disable)", + "priceRangeFilter": "Price Range Filter", + "priceRange": "Price Range", + "priceRangeTooltip": "Only copy orders where Leader's trade price is within the specified range. Leave empty to disable. Examples: Fill 0.11 and 0.89 means only copy orders with price between 0.11 and 0.89; Fill only max price 0.89 means only copy orders with price below 0.89; Fill only min price 0.11 means only copy orders with price above 0.11.", + "minPricePlaceholder": "Min Price (leave empty for no limit)", + "maxPricePlaceholder": "Max Price (leave empty for no limit)", "supportSell": "Support Sell", "supportSellTooltip": "Whether to copy Leader's sell orders", "save": "Save", @@ -677,8 +701,10 @@ "marketStatus": "Market Not Tradable", "orderbookError": "Orderbook Fetch Failed", "orderbookEmpty": "Orderbook Empty", + "priceRange": "Price Range Mismatch", "unknown": "Unknown Reason" - } + }, + "noData": "No filtered orders" }, "copyTradingList": { "title": "Copy Trading Config Management", diff --git a/frontend/src/locales/zh-CN/common.json b/frontend/src/locales/zh-CN/common.json index 59696a3..a7f3570 100644 --- a/frontend/src/locales/zh-CN/common.json +++ b/frontend/src/locales/zh-CN/common.json @@ -23,7 +23,11 @@ "loading": "加载中", "noData": "暂无数据", "saveConfig": "保存配置", - "refreshConfig": "刷新配置" + "refreshConfig": "刷新配置", + "total": "共", + "items": "条", + "prev": "上一页", + "next": "下一页" }, "login": { "title": "登录", @@ -390,6 +394,11 @@ "minOrderbookDepth": "最小订单簿深度 (USDC)", "minOrderbookDepthTooltip": "最小订单簿深度(USDC金额),NULL表示不启用此过滤。检查前 N 档的深度", "minOrderbookDepthPlaceholder": "例如:50(可选,不填写表示不启用)", + "priceRangeFilter": "价格区间过滤", + "priceRange": "价格区间", + "priceRangeTooltip": "仅跟单 Leader 交易价格在指定区间内的订单。不填写表示不限制。示例:填写 0.11 和 0.89 表示仅跟单价格在 0.11 到 0.89 之间的订单;只填写最高价 0.89 表示仅跟单价格在 0.89 以下的订单;只填写最低价 0.11 表示仅跟单价格在 0.11 以上的订单。", + "minPricePlaceholder": "最低价(留空不限制)", + "maxPricePlaceholder": "最高价(留空不限制)", "supportSell": "跟单卖出", "supportSellTooltip": "是否跟单 Leader 的卖出订单。开启:跟单 Leader 的买入和卖出订单;关闭:只跟单 Leader 的买入订单,忽略卖出订单。", "create": "创建模板", @@ -442,6 +451,11 @@ "minOrderbookDepth": "最小订单簿深度 (USDC)", "minOrderbookDepthTooltip": "最小订单簿深度(USDC金额),NULL表示不启用此过滤。检查前 N 档的深度", "minOrderbookDepthPlaceholder": "例如:50(可选,不填写表示不启用)", + "priceRangeFilter": "价格区间过滤", + "priceRange": "价格区间", + "priceRangeTooltip": "仅跟单 Leader 交易价格在指定区间内的订单。不填写表示不限制。示例:填写 0.11 和 0.89 表示仅跟单价格在 0.11 到 0.89 之间的订单;只填写最高价 0.89 表示仅跟单价格在 0.89 以下的订单;只填写最低价 0.11 表示仅跟单价格在 0.11 以上的订单。", + "minPricePlaceholder": "最低价(留空不限制)", + "maxPricePlaceholder": "最高价(留空不限制)", "supportSell": "跟单卖出", "supportSellTooltip": "是否跟单 Leader 的卖出订单。开启:跟单 Leader 的买入和卖出订单;关闭:只跟单 Leader 的买入订单,忽略卖出订单。", "invalidNumber": "请输入有效的数字" @@ -502,6 +516,11 @@ "minOrderbookDepth": "最小订单簿深度 (USDC)", "minOrderbookDepthTooltip": "最小订单簿深度(USDC金额),NULL表示不启用此过滤。检查前 N 档的深度", "minOrderbookDepthPlaceholder": "例如:50(可选,不填写表示不启用)", + "priceRangeFilter": "价格区间过滤", + "priceRange": "价格区间", + "priceRangeTooltip": "仅跟单 Leader 交易价格在指定区间内的订单。不填写表示不限制。示例:填写 0.11 和 0.89 表示仅跟单价格在 0.11 到 0.89 之间的订单;只填写最高价 0.89 表示仅跟单价格在 0.89 以下的订单;只填写最低价 0.11 表示仅跟单价格在 0.11 以上的订单。", + "minPricePlaceholder": "最低价(留空不限制)", + "maxPricePlaceholder": "最高价(留空不限制)", "supportSell": "跟单卖出", "supportSellTooltip": "是否跟单 Leader 的卖出订单", "create": "创建跟单配置", @@ -560,6 +579,11 @@ "minOrderbookDepth": "最小订单簿深度 (USDC)", "minOrderbookDepthTooltip": "最小订单簿深度(USDC金额),NULL表示不启用此过滤", "minOrderbookDepthPlaceholder": "例如:50(可选,不填写表示不启用)", + "priceRangeFilter": "价格区间过滤", + "priceRange": "价格区间", + "priceRangeTooltip": "仅跟单 Leader 交易价格在指定区间内的订单。不填写表示不限制。示例:填写 0.11 和 0.89 表示仅跟单价格在 0.11 到 0.89 之间的订单;只填写最高价 0.89 表示仅跟单价格在 0.89 以下的订单;只填写最低价 0.11 表示仅跟单价格在 0.11 以上的订单。", + "minPricePlaceholder": "最低价(留空不限制)", + "maxPricePlaceholder": "最高价(留空不限制)", "supportSell": "跟单卖出", "supportSellTooltip": "是否跟单 Leader 的卖出订单", "save": "保存", @@ -569,8 +593,8 @@ "invalidNumber": "请输入有效的数字" }, "filteredOrdersList": { - "title": "被过滤订单列表", - "fetchFailed": "获取被过滤订单列表失败", + "title": "已过滤订单列表", + "fetchFailed": "获取已过滤订单列表失败", "market": "市场", "outcome": "方向", "price": "价格", @@ -589,8 +613,10 @@ "marketStatus": "市场状态不可交易", "orderbookError": "订单簿获取失败", "orderbookEmpty": "订单簿为空", + "priceRange": "价格区间不符", "unknown": "未知原因" - } + }, + "noData": "暂无已过滤订单" }, "copyTradingList": { "title": "跟单配置管理", @@ -611,7 +637,7 @@ "buyOrders": "买入订单", "sellOrders": "卖出订单", "matchedOrders": "匹配关系", - "filteredOrders": "被过滤订单", + "filteredOrders": "已过滤订单", "filterWallet": "筛选钱包", "filterTemplate": "筛选模板", "filterLeader": "筛选 Leader", diff --git a/frontend/src/locales/zh-TW/common.json b/frontend/src/locales/zh-TW/common.json index a800920..a4325a3 100644 --- a/frontend/src/locales/zh-TW/common.json +++ b/frontend/src/locales/zh-TW/common.json @@ -26,7 +26,11 @@ "disabled": "禁用", "noData": "暫無數據", "saveConfig": "保存配置", - "refreshConfig": "刷新配置" + "refreshConfig": "刷新配置", + "total": "共", + "items": "條", + "prev": "上一頁", + "next": "下一頁" }, "account": { "title": "賬戶管理", @@ -478,6 +482,11 @@ "minOrderbookDepth": "最小訂單簿深度 (USDC)", "minOrderbookDepthTooltip": "最小訂單簿深度(USDC金額),NULL表示不啟用此過濾。檢查前 N 檔的深度", "minOrderbookDepthPlaceholder": "例如:50(可選,不填寫表示不啟用)", + "priceRangeFilter": "價格區間過濾", + "priceRange": "價格區間", + "priceRangeTooltip": "僅跟單 Leader 交易價格在指定區間內的訂單。不填寫表示不限制。示例:填寫 0.11 和 0.89 表示僅跟單價格在 0.11 到 0.89 之間的訂單;只填寫最高價 0.89 表示僅跟單價格在 0.89 以下的訂單;只填寫最低價 0.11 表示僅跟單價格在 0.11 以上的訂單。", + "minPricePlaceholder": "最低價(留空不限制)", + "maxPricePlaceholder": "最高價(留空不限制)", "supportSell": "跟單賣出", "supportSellTooltip": "是否跟單 Leader 的賣出訂單。開啟:跟單 Leader 的買入和賣出訂單;關閉:只跟單 Leader 的買入訂單,忽略賣出訂單。", "create": "創建模板", @@ -530,6 +539,11 @@ "minOrderbookDepth": "最小訂單簿深度 (USDC)", "minOrderbookDepthTooltip": "最小訂單簿深度(USDC金額),NULL表示不啟用此過濾。檢查前 N 檔的深度", "minOrderbookDepthPlaceholder": "例如:50(可選,不填寫表示不啟用)", + "priceRangeFilter": "價格區間過濾", + "priceRange": "價格區間", + "priceRangeTooltip": "僅跟單 Leader 交易價格在指定區間內的訂單。不填寫表示不限制。示例:填寫 0.11 和 0.89 表示僅跟單價格在 0.11 到 0.89 之間的訂單;只填寫最高價 0.89 表示僅跟單價格在 0.89 以下的訂單;只填寫最低價 0.11 表示僅跟單價格在 0.11 以上的訂單。", + "minPricePlaceholder": "最低價(留空不限制)", + "maxPricePlaceholder": "最高價(留空不限制)", "supportSell": "跟單賣出", "supportSellTooltip": "是否跟單 Leader 的賣出訂單。開啟:跟單 Leader 的買入和賣出訂單;關閉:只跟單 Leader 的買入訂單,忽略賣出訂單。", "invalidNumber": "請輸入有效的數字" @@ -590,6 +604,11 @@ "minOrderbookDepth": "最小訂單簿深度 (USDC)", "minOrderbookDepthTooltip": "最小訂單簿深度(USDC金額),NULL表示不啟用此過濾。檢查前 N 檔的深度", "minOrderbookDepthPlaceholder": "例如:50(可選,不填寫表示不啟用)", + "priceRangeFilter": "價格區間過濾", + "priceRange": "價格區間", + "priceRangeTooltip": "僅跟單 Leader 交易價格在指定區間內的訂單。不填寫表示不限制。示例:填寫 0.11 和 0.89 表示僅跟單價格在 0.11 到 0.89 之間的訂單;只填寫最高價 0.89 表示僅跟單價格在 0.89 以下的訂單;只填寫最低價 0.11 表示僅跟單價格在 0.11 以上的訂單。", + "minPricePlaceholder": "最低價(留空不限制)", + "maxPricePlaceholder": "最高價(留空不限制)", "supportSell": "跟單賣出", "supportSellTooltip": "是否跟單 Leader 的賣出訂單", "create": "創建跟單配置", @@ -648,6 +667,11 @@ "minOrderbookDepth": "最小訂單簿深度 (USDC)", "minOrderbookDepthTooltip": "最小訂單簿深度(USDC金額),NULL表示不啟用此過濾", "minOrderbookDepthPlaceholder": "例如:50(可選,不填寫表示不啟用)", + "priceRangeFilter": "價格區間過濾", + "priceRange": "價格區間", + "priceRangeTooltip": "僅跟單 Leader 交易價格在指定區間內的訂單。不填寫表示不限制。示例:填寫 0.11 和 0.89 表示僅跟單價格在 0.11 到 0.89 之間的訂單;只填寫最高價 0.89 表示僅跟單價格在 0.89 以下的訂單;只填寫最低價 0.11 表示僅跟單價格在 0.11 以上的訂單。", + "minPricePlaceholder": "最低價(留空不限制)", + "maxPricePlaceholder": "最高價(留空不限制)", "supportSell": "跟單賣出", "supportSellTooltip": "是否跟單 Leader 的賣出訂單", "save": "保存", @@ -677,8 +701,10 @@ "marketStatus": "市場狀態不可交易", "orderbookError": "訂單簿獲取失敗", "orderbookEmpty": "訂單簿為空", + "priceRange": "價格區間不符", "unknown": "未知原因" - } + }, + "noData": "暫無已過濾訂單" }, "copyTradingList": { "title": "跟單配置管理", diff --git a/frontend/src/pages/CopyTradingAdd.tsx b/frontend/src/pages/CopyTradingAdd.tsx index 011fc65..d2b01d1 100644 --- a/frontend/src/pages/CopyTradingAdd.tsx +++ b/frontend/src/pages/CopyTradingAdd.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { Card, Form, Button, Switch, message, Typography, Space, Radio, InputNumber, Modal, Table, Select } from 'antd' +import { Card, Form, Button, Switch, message, Typography, Space, Radio, InputNumber, Modal, Table, Select, Divider, Input } from 'antd' import { ArrowLeftOutlined, SaveOutlined, FileTextOutlined } from '@ant-design/icons' import { apiService } from '../services/api' import { useAccountStore } from '../store/accountStore' @@ -63,7 +63,9 @@ const CopyTradingAdd: React.FC = () => { supportSell: template.supportSell, minOrderDepth: template.minOrderDepth ? parseFloat(template.minOrderDepth) : undefined, maxSpread: template.maxSpread ? parseFloat(template.maxSpread) : undefined, - minOrderbookDepth: template.minOrderbookDepth ? parseFloat(template.minOrderbookDepth) : undefined + minOrderbookDepth: template.minOrderbookDepth ? parseFloat(template.minOrderbookDepth) : undefined, + minPrice: template.minPrice ? parseFloat(template.minPrice) : undefined, + maxPrice: template.maxPrice ? parseFloat(template.maxPrice) : undefined }) setCopyMode(template.copyMode) setTemplateModalVisible(false) @@ -110,7 +112,9 @@ const CopyTradingAdd: React.FC = () => { supportSell: values.supportSell !== false, minOrderDepth: values.minOrderDepth?.toString(), maxSpread: values.maxSpread?.toString(), - minOrderbookDepth: values.minOrderbookDepth?.toString() + minOrderbookDepth: values.minOrderbookDepth?.toString(), + minPrice: values.minPrice?.toString(), + maxPrice: values.maxPrice?.toString() } const response = await apiService.copyTrading.create(request) @@ -408,6 +412,38 @@ const CopyTradingAdd: React.FC = () => { /> + {t('copyTradingAdd.priceRangeFilter') || '价格区间过滤'} + + + + + + + - + + + + + + {/* 跟单卖出 - 表单最底部 */} { + const { t } = useTranslation() const { copyTradingId } = useParams<{ copyTradingId: string }>() const navigate = useNavigate() const isMobile = useMediaQuery({ maxWidth: 768 }) @@ -199,8 +201,8 @@ const CopyTradingBuyOrdersPage: React.FC = () => {
-

买入订单列表

diff --git a/frontend/src/pages/CopyTradingEdit.tsx b/frontend/src/pages/CopyTradingEdit.tsx index 8c7f8f1..57a1c0f 100644 --- a/frontend/src/pages/CopyTradingEdit.tsx +++ b/frontend/src/pages/CopyTradingEdit.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' -import { Card, Form, Button, Switch, message, Typography, Space, Radio, InputNumber, Divider, Spin, Select } from 'antd' +import { Card, Form, Button, Switch, message, Typography, Space, Radio, InputNumber, Divider, Spin, Select, Input } from 'antd' import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons' import { apiService } from '../services/api' import type { CopyTrading, CopyTradingUpdateRequest } from '../types' @@ -56,7 +56,9 @@ const CopyTradingEdit: React.FC = () => { supportSell: found.supportSell, minOrderDepth: found.minOrderDepth ? parseFloat(found.minOrderDepth) : undefined, maxSpread: found.maxSpread ? parseFloat(found.maxSpread) : undefined, - minOrderbookDepth: found.minOrderbookDepth ? parseFloat(found.minOrderbookDepth) : undefined + minOrderbookDepth: found.minOrderbookDepth ? parseFloat(found.minOrderbookDepth) : undefined, + minPrice: found.minPrice ? parseFloat(found.minPrice) : undefined, + maxPrice: found.maxPrice ? parseFloat(found.maxPrice) : undefined }) } else { message.error(t('copyTradingEdit.fetchFailed') || '跟单配置不存在') @@ -118,7 +120,9 @@ const CopyTradingEdit: React.FC = () => { supportSell: values.supportSell, minOrderDepth: values.minOrderDepth?.toString(), maxSpread: values.maxSpread?.toString(), - minOrderbookDepth: values.minOrderbookDepth?.toString() + minOrderbookDepth: values.minOrderbookDepth?.toString(), + minPrice: values.minPrice?.toString(), + maxPrice: values.maxPrice?.toString() } const response = await apiService.copyTrading.update(request) @@ -397,6 +401,38 @@ const CopyTradingEdit: React.FC = () => { /> + {t('copyTradingEdit.priceRangeFilter') || '价格区间过滤'} + + + + + + + - + + + + + + {/* 跟单卖出 - 表单最底部 */} { { type: 'divider' }, - { - key: 'statistics', - label: t('copyTradingList.viewStatistics') || '查看统计', - icon: , - onClick: () => navigate(`/copy-trading/statistics/${record.id}`) - }, { key: 'buyOrders', label: t('copyTradingList.buyOrders') || '买入订单', @@ -290,7 +284,7 @@ const CopyTradingList: React.FC = () => { }, { key: 'filteredOrders', - label: t('copyTradingList.filteredOrders') || '被过滤订单', + label: t('copyTradingList.filteredOrders') || '已过滤订单', icon: , onClick: () => navigate(`/copy-trading/filtered-orders/${record.id}`) }, @@ -550,39 +544,47 @@ const CopyTradingList: React.FC = () => {
+ , - onClick: () => navigate(`/copy-trading/statistics/${record.id}`) - }, { key: 'buyOrders', - label: '买入订单', + label: t('copyTradingList.buyOrders') || '买入订单', icon: , onClick: () => navigate(`/copy-trading/orders/buy/${record.id}`) }, { key: 'sellOrders', - label: '卖出订单', + label: t('copyTradingList.sellOrders') || '卖出订单', icon: , onClick: () => navigate(`/copy-trading/orders/sell/${record.id}`) }, { key: 'matchedOrders', - label: '匹配关系', + label: t('copyTradingList.matchedOrders') || '匹配关系', icon: , onClick: () => navigate(`/copy-trading/orders/matched/${record.id}`) + }, + { + key: 'filteredOrders', + label: t('copyTradingList.filteredOrders') || '已过滤订单', + icon: , + onClick: () => navigate(`/copy-trading/filtered-orders/${record.id}`) } ] }} @@ -593,14 +595,14 @@ const CopyTradingList: React.FC = () => { icon={} style={{ flex: 1, minWidth: '80px' }} > - 订单 + {t('copyTradingList.orders') || '订单'} handleDelete(record.id)} - okText="确定" - cancelText="取消" + okText={t('common.confirm') || '确定'} + cancelText={t('common.cancel') || '取消'} >
diff --git a/frontend/src/pages/CopyTradingMatchedOrders.tsx b/frontend/src/pages/CopyTradingMatchedOrders.tsx index 0d47042..77c27ce 100644 --- a/frontend/src/pages/CopyTradingMatchedOrders.tsx +++ b/frontend/src/pages/CopyTradingMatchedOrders.tsx @@ -5,9 +5,11 @@ import { LeftOutlined } from '@ant-design/icons' import { apiService } from '../services/api' import { formatUSDC } from '../utils' import { useMediaQuery } from 'react-responsive' +import { useTranslation } from 'react-i18next' import type { MatchedOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../types' const CopyTradingMatchedOrdersPage: React.FC = () => { + const { t } = useTranslation() const { copyTradingId } = useParams<{ copyTradingId: string }>() const navigate = useNavigate() const isMobile = useMediaQuery({ maxWidth: 768 }) @@ -153,8 +155,8 @@ const CopyTradingMatchedOrdersPage: React.FC = () => {
-

匹配关系列表

diff --git a/frontend/src/pages/CopyTradingSellOrders.tsx b/frontend/src/pages/CopyTradingSellOrders.tsx index d8fb179..fbe8f76 100644 --- a/frontend/src/pages/CopyTradingSellOrders.tsx +++ b/frontend/src/pages/CopyTradingSellOrders.tsx @@ -5,11 +5,13 @@ import { LeftOutlined } from '@ant-design/icons' import { apiService } from '../services/api' import { formatUSDC } from '../utils' import { useMediaQuery } from 'react-responsive' +import { useTranslation } from 'react-i18next' import type { SellOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../types' const { Option } = Select const CopyTradingSellOrdersPage: React.FC = () => { + const { t } = useTranslation() const { copyTradingId } = useParams<{ copyTradingId: string }>() const navigate = useNavigate() const isMobile = useMediaQuery({ maxWidth: 768 }) @@ -184,8 +186,8 @@ const CopyTradingSellOrdersPage: React.FC = () => {
-

卖出订单列表

diff --git a/frontend/src/pages/CopyTradingStatistics.tsx b/frontend/src/pages/CopyTradingStatistics.tsx index e70267f..d49d61a 100644 --- a/frontend/src/pages/CopyTradingStatistics.tsx +++ b/frontend/src/pages/CopyTradingStatistics.tsx @@ -110,7 +110,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
- +
Leader 名称
@@ -118,15 +118,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
- -
-
模板名称
-
- {statistics.templateName || `模板 ${statistics.templateId}`} -
-
- - +
跟单状态
@@ -203,21 +195,14 @@ const CopyTradingStatisticsPage: React.FC = () => { {/* 持仓统计卡片 */} - + - - - - + { 'MARKET_STATUS': { color: 'blue', label: t('filteredOrdersList.filterTypes.marketStatus') || '市场状态不可交易' }, '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') || '价格区间不符' }, 'UNKNOWN': { color: 'default', label: t('filteredOrdersList.filterTypes.unknown') || '未知原因' } } const config = typeMap[type] || typeMap['UNKNOWN'] @@ -210,26 +211,186 @@ const FilteredOrdersList: React.FC = () => { +
- t('common.total') + `: ${total}`, - onChange: (page) => setPage(page) - }} - scroll={{ x: isMobile ? 800 : 'auto' }} - size={isMobile ? 'small' : 'middle'} - /> + {isMobile ? ( + // 移动端卡片布局 +
+ {loading ? ( +
+ +
+ ) : filteredOrders.length === 0 ? ( +
+ {t('filteredOrdersList.noData') || '暂无已过滤订单'} +
+ ) : ( +
+ {filteredOrders.map((order) => { + const date = new Date(order.createdAt) + const formattedDate = date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) + const marketLink = getMarketLink(order) + const marketTitle = order.marketTitle || order.marketId.slice(0, 10) + '...' + + return ( + + {/* 市场信息 */} +
+
+ {marketLink ? ( + + {marketTitle} + + ) : ( + marketTitle + )} +
+
+ + {order.side === 'BUY' ? (t('order.buy') || '买入') : (t('order.sell') || '卖出')} + + {getFilterTypeTag(order.filterType)} +
+
+ + + + {/* 订单详情 */} +
+
+ {t('filteredOrdersList.outcome') || '市场方向'} +
+
+ {order.outcome || (order.outcomeIndex !== undefined ? `Index ${order.outcomeIndex}` : '-')} +
+
+ +
+
+ {t('filteredOrdersList.price') || '价格'} +
+
+ {order.price} +
+
+ +
+
+ {t('filteredOrdersList.size') || 'Leader数量'} +
+
+ {formatUSDC(order.size)} +
+
+ + {order.calculatedQuantity && ( +
+
+ {t('filteredOrdersList.calculatedQuantity') || '计算数量'} +
+
+ {formatUSDC(order.calculatedQuantity)} +
+
+ )} + +
+
+ {t('filteredOrdersList.filterReason') || '过滤原因'} +
+
+ {order.filterReason} +
+
+ + {/* 时间 */} +
+
+ {t('filteredOrdersList.createdAt') || '时间'}: {formattedDate} +
+
+
+ ) + })} +
+ )} + + {/* 移动端分页 */} + {filteredOrders.length > 0 && ( +
+
+ {t('common.total') || '共'} {total} {t('common.items') || '条'} +
+
+ + + {page} / {Math.ceil(total / limit)} + + +
+
+ )} +
+ ) : ( + // 桌面端表格布局 +
t('common.total') + `: ${total}`, + onChange: (page) => setPage(page) + }} + scroll={{ x: 'auto' }} + size="middle" + /> + )} ) diff --git a/frontend/src/pages/TemplateAdd.tsx b/frontend/src/pages/TemplateAdd.tsx index 39846c1..44db2ad 100644 --- a/frontend/src/pages/TemplateAdd.tsx +++ b/frontend/src/pages/TemplateAdd.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { useNavigate } from 'react-router-dom' -import { Card, Form, Input, Button, Radio, InputNumber, Switch, message, Typography, Space } from 'antd' +import { Card, Form, Input, Button, Radio, InputNumber, Switch, message, Typography, Space, Divider } from 'antd' import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons' import { apiService } from '../services/api' import { useTranslation } from 'react-i18next' @@ -54,7 +54,9 @@ const TemplateAdd: React.FC = () => { supportSell: values.supportSell !== false, minOrderDepth: values.minOrderDepth?.toString(), maxSpread: values.maxSpread?.toString(), - minOrderbookDepth: values.minOrderbookDepth?.toString() + minOrderbookDepth: values.minOrderbookDepth?.toString(), + minPrice: values.minPrice?.toString(), + maxPrice: values.maxPrice?.toString() }) if (response.data.code === 0) { @@ -295,6 +297,38 @@ const TemplateAdd: React.FC = () => { /> + {t('templateAdd.priceRangeFilter') || '价格区间过滤'} + + + + + + + - + + + + + + {/* 跟单卖出 - 表单最底部 */} { priceTolerance: parseFloat(template.priceTolerance), minOrderDepth: template.minOrderDepth ? parseFloat(template.minOrderDepth) : undefined, maxSpread: template.maxSpread ? parseFloat(template.maxSpread) : undefined, - minOrderbookDepth: template.minOrderbookDepth ? parseFloat(template.minOrderbookDepth) : undefined + minOrderbookDepth: template.minOrderbookDepth ? parseFloat(template.minOrderbookDepth) : undefined, + minPrice: template.minPrice ? parseFloat(template.minPrice) : undefined, + maxPrice: template.maxPrice ? parseFloat(template.maxPrice) : undefined }) } else { message.error(response.data.msg || t('templateEdit.fetchFailed') || '获取模板详情失败') @@ -97,7 +99,9 @@ const TemplateEdit: React.FC = () => { supportSell: values.supportSell, minOrderDepth: values.minOrderDepth?.toString(), maxSpread: values.maxSpread?.toString(), - minOrderbookDepth: values.minOrderbookDepth?.toString() + minOrderbookDepth: values.minOrderbookDepth?.toString(), + minPrice: values.minPrice?.toString(), + maxPrice: values.maxPrice?.toString() }) if (response.data.code === 0) { @@ -330,6 +334,38 @@ const TemplateEdit: React.FC = () => { /> + {t('templateEdit.priceRangeFilter') || '价格区间过滤'} + + + + + + + - + + + + + + {/* 跟单卖出 - 表单最底部 */} { minOrderSize: template.minOrderSize ? parseFloat(template.minOrderSize) : undefined, maxDailyOrders: template.maxDailyOrders, priceTolerance: parseFloat(template.priceTolerance), - supportSell: template.supportSell + supportSell: template.supportSell, + minOrderDepth: template.minOrderDepth ? parseFloat(template.minOrderDepth) : undefined, + maxSpread: template.maxSpread ? parseFloat(template.maxSpread) : undefined, + minOrderbookDepth: template.minOrderbookDepth ? parseFloat(template.minOrderbookDepth) : undefined, + minPrice: template.minPrice ? parseFloat(template.minPrice) : undefined, + maxPrice: template.maxPrice ? parseFloat(template.maxPrice) : undefined }) setCopyModalVisible(true) @@ -114,7 +119,12 @@ const TemplateList: React.FC = () => { minOrderSize: values.copyMode === 'RATIO' ? values.minOrderSize?.toString() : undefined, maxDailyOrders: values.maxDailyOrders, priceTolerance: values.priceTolerance?.toString(), - supportSell: values.supportSell !== false + supportSell: values.supportSell !== false, + minOrderDepth: values.minOrderDepth?.toString(), + maxSpread: values.maxSpread?.toString(), + minOrderbookDepth: values.minOrderbookDepth?.toString(), + minPrice: values.minPrice?.toString(), + maxPrice: values.maxPrice?.toString() }) if (response.data.code === 0) { @@ -598,6 +608,82 @@ const TemplateList: React.FC = () => { + 过滤条件(可选) + + + + + + + + + + + + + + 价格区间过滤 + + + + + + + - + + + + + + {({ getFieldsError }) => { const errors = getFieldsError() diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index d97e854..c9d5075 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -111,6 +111,8 @@ export interface CopyTradingTemplate { minOrderDepth?: string maxSpread?: string minOrderbookDepth?: string + minPrice?: string // 最低价格(可选),NULL表示不限制最低价 + maxPrice?: string // 最高价格(可选),NULL表示不限制最高价 createdAt: number updatedAt: number } @@ -201,6 +203,8 @@ export interface CopyTrading { minOrderDepth?: string maxSpread?: string minOrderbookDepth?: string + minPrice?: string // 最低价格(可选),NULL表示不限制最低价 + maxPrice?: string // 最高价格(可选),NULL表示不限制最高价 createdAt: number updatedAt: number } @@ -240,6 +244,8 @@ export interface CopyTradingCreateRequest { minOrderDepth?: string maxSpread?: string minOrderbookDepth?: string + minPrice?: string // 最低价格(可选),NULL表示不限制最低价 + maxPrice?: string // 最高价格(可选),NULL表示不限制最高价 } /** @@ -267,6 +273,8 @@ export interface CopyTradingUpdateRequest { minOrderDepth?: string maxSpread?: string minOrderbookDepth?: string + minPrice?: string // 最低价格(可选),NULL表示不限制最低价 + maxPrice?: string // 最高价格(可选),NULL表示不限制最高价 } /** @@ -570,8 +578,6 @@ export interface CopyTradingStatistics { accountName: string | null leaderId: number leaderName: string | null - templateId: number - templateName: string | null enabled: boolean // 买入统计 @@ -587,7 +593,7 @@ export interface CopyTradingStatistics { // 持仓统计 currentPositionQuantity: string - currentPositionValue: string + currentPositionValue: string // 当前实现总是返回 "0",保留用于未来扩展 // 盈亏统计 totalRealizedPnl: string