From 561ebf0ce3a32ba35a2b2f5a79ca1834e1dd6c0a Mon Sep 17 00:00:00 2001 From: WrBug Date: Mon, 2 Mar 2026 23:05:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=AE=A2=E5=8D=95=E6=8E=A8=E9=80=81/?= =?UTF-8?q?=E9=80=9A=E7=9F=A5):=20=E6=8C=89=E5=AE=9E=E9=99=85=E6=88=90?= =?UTF-8?q?=E4=BA=A4=E4=BB=B7=E4=B8=8E=20size=5Fmatched=20=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E4=BB=B7=E6=A0=BC=E4=B8=8E=E6=95=B0=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实际成交价公式: original_size * price / size_matched,数量展示用 size_matched - OrderPushService/OrderStatusUpdateService/CryptoTail: 用公式计算 avgFilledPrice,移除 getTrades 依赖 - TelegramNotificationService: 有 avgFilledPrice 时展示数量用 filled,ORDER_SUCCESS/CRYPTO_TAIL 一致 - 前端订单推送: 有成交时展示 size_matched;无 orderDetail 时用 WebSocket 数据计算价格与数量 - OrderDetailDto 增加 avgFilledPrice;PolymarketClobService 移除 getAvgFilledPriceFromTradeIds Made-with: Cursor --- .../polymarketbot/api/PolymarketClobApi.kt | 13 ++++- .../polymarketbot/dto/OrderMessageDto.kt | 7 ++- .../service/accounts/AccountService.kt | 21 ++++++-- .../service/common/PolymarketClobService.kt | 2 +- .../copytrading/orders/OrderPushService.kt | 22 ++++++-- .../statistics/OrderStatusUpdateService.kt | 42 +++++++++++++--- ...yptoTailOrderNotificationPollingService.kt | 16 ++++++ .../system/NotificationTemplateService.kt | 34 +++++++++++-- .../system/TelegramNotificationService.kt | 50 ++++++++++++++----- frontend/src/App.tsx | 31 ++++++++---- frontend/src/types/index.ts | 4 +- 11 files changed, 194 insertions(+), 48 deletions(-) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketClobApi.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketClobApi.kt index a3d14c1..ab63872 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketClobApi.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketClobApi.kt @@ -212,6 +212,7 @@ data class NewOrderResponse( val success: Boolean, // boolean indicating if server-side error @SerializedName("errorMsg") val errorMsg: String? = null, // error message in case of unsuccessful placement + val error: String? = null, // error message (alternative field, e.g. "Trading restricted in your region...") @SerializedName("orderID") val orderId: String? = null, // id of order(API 返回字段名为 orderID) @SerializedName("transactionsHashes") @@ -222,7 +223,17 @@ data class NewOrderResponse( val takingAmount: String? = null, // taking amount @SerializedName("makingAmount") val makingAmount: String? = null // making amount -) +) { + /** + * 获取错误信息的便捷方法 + * 优先返回 errorMsg,其次返回 error,最后返回默认消息 + */ + fun getErrorMessage(): String { + return errorMsg?.takeIf { it.isNotBlank() } + ?: error?.takeIf { it.isNotBlank() } + ?: "创建订单失败" + } +} /** * 旧的订单请求格式(已废弃,保留用于兼容) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/OrderMessageDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/OrderMessageDto.kt index 96a33fd..10bfa79 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/OrderMessageDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/OrderMessageDto.kt @@ -51,18 +51,21 @@ data class OrderPushMessage( /** * 订单详情(通过 API 获取) + * @param price 订单限价(用户提交的买入/卖出价) + * @param avgFilledPrice 实际成交价 = original_size * price / size_matched(有成交时优先用于推送展示) */ data class OrderDetailDto( val id: String, // 订单 ID val market: String, // 市场 ID (condition ID) val side: String, // BUY/SELL - val price: String, // 价格 + val price: String, // 订单限价 val size: String, // 订单大小 val filled: String, // 已成交数量 val status: String, // 订单状态 val createdAt: String, // 创建时间(ISO 8601 格式) val marketName: String? = null, // 市场名称(通过 Data API 获取) val marketSlug: String? = null, // 市场 slug - val marketIcon: String? = null // 市场图标 + val marketIcon: String? = null, // 市场图标 + val avgFilledPrice: String? = null // 实际成交价 = original_size*price/size_matched(有成交时使用) ) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt index 49c1baf..ef7d8f2 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt @@ -1345,7 +1345,8 @@ class AccountService( marketTitle = marketTitle, marketId = request.marketId, marketSlug = marketSlug, - side = request.side, + side = "SELL", // 手动卖出订单,方向固定为 SELL + outcome = request.side, // request.side 是市场方向(YES/NO) price = sellPrice, // 直接传递卖出价格 size = sellQuantity.toPlainString(), // 直接传递卖出数量 accountName = account.accountName, @@ -1377,7 +1378,7 @@ class AccountService( ) ) } else { - val errorMsg = response.errorMsg ?: "未知错误" + val errorMsg = response.getErrorMessage() val fullErrorMsg = "创建订单失败: accountId=${account.id}, marketId=${request.marketId}, side=${request.side}, orderType=${request.orderType}, price=${if (request.orderType == "LIMIT") sellPrice else "MARKET"}, quantity=${sellQuantity.toPlainString()}, errorMsg=$errorMsg" logger.error(fullErrorMsg) @@ -1422,6 +1423,14 @@ class AccountService( } catch (e: Exception) { null } + + // 尝试从 errorBody 解析 error 字段 + val apiError = try { + errorBody?.let { objectMapper.readTree(it).get("error")?.asText() } + } catch (e: Exception) { + null + } + val fullErrorMsg = "创建订单失败: accountId=${account.id}, marketId=${request.marketId}, side=${request.side}, orderType=${request.orderType}, price=${if (request.orderType == "LIMIT") sellPrice else "MARKET"}, quantity=${sellQuantity.toPlainString()}, code=${orderResponse.code()}, message=${orderResponse.message()}${if (errorBody != null) ", errorBody=$errorBody" else ""}" logger.error(fullErrorMsg) @@ -1440,8 +1449,10 @@ class AccountService( java.util.Locale("zh", "CN") // 默认简体中文 } - // 只传递后端返回的 msg,不传递完整堆栈 - val errorMsg = orderResponse.body()?.errorMsg ?: "创建订单失败" + // 优先使用解析的 API error,其次使用响应体的 errorMsg,最后使用默认消息 + val errorMsg = apiError + ?: orderResponse.body()?.getErrorMessage() + ?: "创建订单失败 (HTTP ${orderResponse.code()})" telegramNotificationService?.sendOrderFailureNotification( marketTitle = marketTitle, @@ -1451,7 +1462,7 @@ class AccountService( outcome = null, // 失败时可能没有 outcome price = if (request.orderType == "LIMIT") sellPrice.toString() else "MARKET", size = sellQuantity.toString(), - errorMessage = errorMsg, // 只传递后端返回的 msg + errorMessage = errorMsg, // 只传递后端返回的错误信息 accountName = account.accountName, walletAddress = account.walletAddress, locale = locale diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/PolymarketClobService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/PolymarketClobService.kt index 685bd93..a630791 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/PolymarketClobService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/PolymarketClobService.kt @@ -401,7 +401,7 @@ class PolymarketClobService( Result.failure(e) } } - + /** * 获取费率 * 文档: https://docs.polymarket.com/developers/market-makers/maker-rebates-program#1-fetch-the-fee-rate diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/orders/OrderPushService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/orders/OrderPushService.kt index 6ca9e1c..77b6cca 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/orders/OrderPushService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/orders/OrderPushService.kt @@ -20,7 +20,12 @@ import com.wrbug.polymarketbot.repository.CopyTradingRepository import com.wrbug.polymarketbot.repository.LeaderRepository import com.wrbug.polymarketbot.constants.PolymarketConstants import com.wrbug.polymarketbot.service.common.MarketService +import com.wrbug.polymarketbot.util.div +import com.wrbug.polymarketbot.util.gt +import com.wrbug.polymarketbot.util.multi +import com.wrbug.polymarketbot.util.toSafeBigDecimal import org.springframework.stereotype.Service +import java.math.BigDecimal import java.util.concurrent.ConcurrentHashMap /** @@ -426,20 +431,29 @@ class OrderPushService( // 获取市场信息(使用 MarketService,优先从数据库/缓存获取) val market = marketService.getMarket(conditionId ?: openOrder.market) - // 转换为 DTO + // 有成交时按公式计算实际成交价:original_size * price / size_matched,数量用 size_matched + val sizeMatched = openOrder.sizeMatched.toSafeBigDecimal() + val avgFilledPrice = if (sizeMatched.gt(BigDecimal.ZERO)) { + openOrder.originalSize.toSafeBigDecimal() + .multi(openOrder.price) + .div(sizeMatched, 18) + } else null + + // 转换为 DTO(展示数量用 size_matched) // 注意:createdAt 是 unix timestamp (Long),需要转换为字符串 OrderDetailDto( id = openOrder.id, market = openOrder.market, side = openOrder.side, price = openOrder.price, - size = openOrder.originalSize, // 使用 original_size - filled = openOrder.sizeMatched, // 使用 size_matched + size = openOrder.originalSize, + filled = openOrder.sizeMatched, // 已成交数量用 size_matched status = openOrder.status, createdAt = openOrder.createdAt.toString(), // unix timestamp 转换为字符串 marketName = market?.title, marketSlug = market?.slug, // 显示用的 slug - marketIcon = market?.icon + marketIcon = market?.icon, + avgFilledPrice = avgFilledPrice?.toPlainString() // 实际成交价 = original_size*price/size_matched ) }, onFailure = { e -> diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/OrderStatusUpdateService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/OrderStatusUpdateService.kt index 97f58db..46fe3fb 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/OrderStatusUpdateService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/OrderStatusUpdateService.kt @@ -7,8 +7,10 @@ import com.wrbug.polymarketbot.service.common.MarketService import com.wrbug.polymarketbot.service.system.TelegramNotificationService import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.CryptoUtils -import com.wrbug.polymarketbot.util.toSafeBigDecimal +import com.wrbug.polymarketbot.util.div +import com.wrbug.polymarketbot.util.gt import com.wrbug.polymarketbot.util.multi +import com.wrbug.polymarketbot.util.toSafeBigDecimal import kotlinx.coroutines.* import org.slf4j.LoggerFactory import org.springframework.boot.context.event.ApplicationReadyEvent @@ -556,11 +558,13 @@ class OrderStatusUpdateService( logger.info("更新卖出订单价格成功: orderId=${record.sellOrderId}, 原价格=${record.sellPrice}, 新价格=$actualSellPrice") - // 发送通知(使用实际价格) + // 发送通知(使用实际成交价) sendSellOrderNotification( record = updatedRecord, actualPrice = actualSellPrice.toString(), actualSize = record.totalMatchedQuantity.toString(), + avgFilledPrice = actualSellPrice.toString(), + filled = record.totalMatchedQuantity.toString(), account = account, copyTrading = copyTrading, clobApi = clobApi, @@ -589,11 +593,13 @@ class OrderStatusUpdateService( logger.debug("卖出订单价格无需更新: orderId=${record.sellOrderId}, price=$actualSellPrice") - // 发送通知 + // 发送通知(使用实际成交价) sendSellOrderNotification( record = updatedRecord, actualPrice = actualSellPrice.toString(), actualSize = record.totalMatchedQuantity.toString(), + avgFilledPrice = actualSellPrice.toString(), + filled = record.totalMatchedQuantity.toString(), account = account, copyTrading = copyTrading, clobApi = clobApi, @@ -845,12 +851,24 @@ class OrderStatusUpdateService( logger.debug("买入订单数据无需更新: orderId=${order.buyOrderId}") } - // 发送通知(使用实际数据) + // 有成交时按公式计算实际成交价:original_size * price / size_matched,数量用 size_matched + val sizeMatchedDec = orderDetail.sizeMatched.toSafeBigDecimal() + val avgFilledPriceStr = if (sizeMatchedDec.gt(BigDecimal.ZERO)) { + orderDetail.originalSize.toSafeBigDecimal() + .multi(orderDetail.price) + .div(sizeMatchedDec, 18) + .toPlainString() + } else null + val filledSize = orderDetail.sizeMatched + + // 发送通知(使用实际数据,优先展示平均成交价) sendBuyOrderNotification( order = updatedOrder, actualPrice = actualPrice.toString(), actualSize = actualSize.toString(), actualOutcome = actualOutcome, + avgFilledPrice = avgFilledPriceStr, + filled = filledSize, account = account, copyTrading = copyTrading, clobApi = clobApi, @@ -877,6 +895,8 @@ class OrderStatusUpdateService( actualPrice: String? = null, actualSize: String? = null, actualOutcome: String? = null, + avgFilledPrice: String? = null, // 平均成交价(有成交时用于 TG 展示) + filled: String? = null, // 已成交数量(与 avgFilledPrice 一起用于金额计算) account: Account? = null, copyTrading: CopyTrading? = null, clobApi: PolymarketClobApi? = null, @@ -939,14 +959,16 @@ class OrderStatusUpdateService( null } - // 发送通知 + // 发送通知(优先使用平均成交价展示) telegramNotificationService.sendOrderSuccessNotification( orderId = order.buyOrderId, marketTitle = marketTitle, marketId = order.marketId, marketSlug = market?.eventSlug, // 跳转用的 slug side = "BUY", - price = actualPrice ?: order.price.toString(), // 使用实际价格或临时价格 + price = actualPrice ?: order.price.toString(), // 限价,无 avgFilledPrice 时展示 + avgFilledPrice = avgFilledPrice, + filled = filled, size = actualSize ?: order.quantity.toString(), // 使用实际数量或临时数量 outcome = actualOutcome, // 使用实际 outcome accountName = finalAccount.accountName, @@ -979,6 +1001,8 @@ class OrderStatusUpdateService( actualPrice: String? = null, actualSize: String? = null, actualOutcome: String? = null, + avgFilledPrice: String? = null, // 平均成交价(有成交时用于 TG 展示) + filled: String? = null, // 已成交数量(与 avgFilledPrice 一起用于金额计算) account: Account? = null, copyTrading: CopyTrading? = null, clobApi: PolymarketClobApi? = null, @@ -1041,14 +1065,16 @@ class OrderStatusUpdateService( null } - // 发送通知 + // 发送通知(优先使用平均成交价展示) telegramNotificationService.sendOrderSuccessNotification( orderId = record.sellOrderId, marketTitle = marketTitle, marketId = record.marketId, marketSlug = market?.eventSlug, // 跳转用的 slug side = "SELL", - price = actualPrice ?: record.sellPrice.toString(), // 使用实际价格或临时价格 + price = actualPrice ?: record.sellPrice.toString(), // 限价,无 avgFilledPrice 时展示 + avgFilledPrice = avgFilledPrice, + filled = filled, size = actualSize ?: record.totalMatchedQuantity.toString(), // 使用实际数量或临时数量 outcome = actualOutcome, // 使用实际 outcome accountName = finalAccount.accountName, diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/cryptotail/CryptoTailOrderNotificationPollingService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/cryptotail/CryptoTailOrderNotificationPollingService.kt index faba9f6..52ebede 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/cryptotail/CryptoTailOrderNotificationPollingService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/cryptotail/CryptoTailOrderNotificationPollingService.kt @@ -6,6 +6,10 @@ import com.wrbug.polymarketbot.repository.CryptoTailStrategyRepository import com.wrbug.polymarketbot.repository.CryptoTailStrategyTriggerRepository import com.wrbug.polymarketbot.service.common.MarketService import com.wrbug.polymarketbot.service.system.TelegramNotificationService +import com.wrbug.polymarketbot.util.div +import com.wrbug.polymarketbot.util.gt +import com.wrbug.polymarketbot.util.multi +import com.wrbug.polymarketbot.util.toSafeBigDecimal import com.wrbug.polymarketbot.util.CryptoUtils import com.wrbug.polymarketbot.util.RetrofitFactory import kotlinx.coroutines.CoroutineScope @@ -20,6 +24,7 @@ import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import jakarta.annotation.PreDestroy +import java.math.BigDecimal /** * 加密价差策略订单 TG 通知轮询服务(与跟单一致) @@ -128,6 +133,15 @@ class CryptoTailOrderNotificationPollingService( val market = marketService.getMarket(order.market) val marketTitle = trigger.marketTitle?.takeIf { it.isNotBlank() } ?: market?.title ?: order.market val orderTimeMs = if (order.createdAt < 1_000_000_000_000L) order.createdAt * 1000 else order.createdAt + // 实际成交价 = original_size * price / size_matched,数量用 size_matched + val sizeMatchedDec = order.sizeMatched.toSafeBigDecimal() + val avgFilledPriceStr = if (sizeMatchedDec.gt(BigDecimal.ZERO)) { + order.originalSize.toSafeBigDecimal() + .multi(order.price) + .div(sizeMatchedDec, 18) + .toPlainString() + } else null + val filledSize = order.sizeMatched telegramNotificationService.sendCryptoTailOrderSuccessNotification( orderId = orderId, marketTitle = marketTitle, @@ -137,6 +151,8 @@ class CryptoTailOrderNotificationPollingService( outcome = order.outcome, price = order.price, size = order.originalSize, + avgFilledPrice = avgFilledPriceStr, + filled = filledSize, strategyName = strategy.name, accountName = account.accountName, walletAddress = account.walletAddress, diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/NotificationTemplateService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/NotificationTemplateService.kt index dbc8870..28f95f1 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/NotificationTemplateService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/NotificationTemplateService.kt @@ -352,6 +352,7 @@ class NotificationTemplateService( /** * 渲染模板(按类型取模板内容后替换变量) + * 优化:先解析模版中需要的变量,只替换这些变量,未提供的变量使用 "-" 占位 */ fun renderTemplate(templateType: String, variables: Map): String { val template = getTemplate(templateType) @@ -361,16 +362,43 @@ class NotificationTemplateService( /** * 对给定模板内容做变量替换(不查库) + * 优化:先解析模版中的变量占位符,只替换这些变量,未提供的变量使用 "-" 占位 */ fun renderTemplateContent(content: String, variables: Map): String { + // 先解析模版中需要的变量 + val requiredVariables = extractTemplateVariables(content) + var result = content - variables.forEach { (key, value) -> - result = result.replace("{{$key}}", value) + // 只替换模版中实际使用的变量 + requiredVariables.forEach { varName -> + val value = variables[varName] + result = result.replace("{{$varName}}", value ?: "-") } - result = result.replace(Regex("\\{\\{[^}]+}}"), "-") return result } + /** + * 解析模版中使用的变量名 + * @return 变量名列表(去重) + */ + private fun extractTemplateVariables(content: String): Set { + val regex = Regex("\\{\\{([^}]+)}}") + return regex.findAll(content) + .map { it.groupValues[1].trim() } + .toSet() + } + + /** + * 根据模版需要的变量过滤输入变量 + * 只保留模版中实际使用的变量,避免不必要的数据获取 + */ + fun filterVariablesForTemplate(templateType: String, variables: Map): Map { + val template = getTemplate(templateType) + val content = template?.templateContent ?: DEFAULT_TEMPLATES[templateType] ?: return emptyMap() + val requiredVariables = extractTemplateVariables(content) + return variables.filterKeys { it in requiredVariables } + } + /** * 发送测试消息 */ diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/TelegramNotificationService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/TelegramNotificationService.kt index 72c1dd3..c473a88 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/TelegramNotificationService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/TelegramNotificationService.kt @@ -86,7 +86,9 @@ class TelegramNotificationService( marketId: String? = null, marketSlug: String? = null, side: String, - price: String? = null, // 订单价格(可选,如果提供则直接使用) + price: String? = null, // 订单限价(可选) + avgFilledPrice: String? = null, // 平均成交价(可选,有成交时优先展示) + filled: String? = null, // 已成交数量(可选,与 avgFilledPrice 一起时用于金额计算) size: String? = null, // 订单数量(可选,如果提供则直接使用) outcome: String? = null, // 市场方向(可选,如果提供则直接使用) accountName: String? = null, @@ -130,14 +132,21 @@ class TelegramNotificationService( java.util.Locale("zh", "CN") // 默认简体中文 } - // 优先使用传入的价格和数量,如果没有提供则尝试从订单详情获取 - var actualPrice: String? = price + // 优先使用平均成交价(实际成交价),其次传入的限价,若未提供则从订单详情获取 + var actualPrice: String? = avgFilledPrice?.takeIf { it.isNotBlank() } ?: price var actualSize: String? = size var actualSide: String = side var actualOutcome: String? = outcome + + // 有平均成交价时,已成交数量优先用 filled,用于金额计算 + val sizeForAmount: String? = if (avgFilledPrice != null && avgFilledPrice.isNotBlank() && filled != null && filled.isNotBlank()) { + filled + } else { + null + } - // 如果价格或数量未提供,尝试从订单详情获取 - if ((actualPrice == null || actualSize == null) && orderId != null && clobApi != null && apiKey != null && apiSecret != null && apiPassphrase != null && walletAddressForApi != null) { + // 如果价格、数量或市场方向未提供,尝试从订单详情获取 + if ((actualPrice == null || actualSize == null || actualOutcome == null) && orderId != null && clobApi != null && apiKey != null && apiSecret != null && apiPassphrase != null && walletAddressForApi != null) { try { val orderResponse = clobApi.getOrder(orderId) if (orderResponse.isSuccessful) { @@ -149,7 +158,8 @@ class TelegramNotificationService( if (actualSize == null) { actualSize = order.originalSize // 使用 originalSize 作为订单数量 } - actualSide = order.side // 使用订单详情中的 side + // 注意:不覆盖 side,因为传入的 side(BUY/SELL)是正确的 + // actualSide = order.side // 不要使用订单详情中的 side,因为它可能不准确 if (actualOutcome == null) { actualOutcome = order.outcome // 使用订单详情中的 outcome(市场方向) } @@ -167,12 +177,19 @@ class TelegramNotificationService( // 如果仍然没有获取到实际值,使用默认值(这种情况不应该发生,但为了兼容性保留) val finalPrice = actualPrice ?: "0" - val finalSize = actualSize ?: "0" + // 有实际成交价时展示数量用 size_matched(filled),否则用订单数量(original_size) + val finalSize = if (avgFilledPrice != null && avgFilledPrice.isNotBlank() && filled != null && filled.isNotBlank()) { + filled + } else { + actualSize ?: "0" + } + // 金额计算:有实际成交价和已成交数量时用二者乘积,否则用展示价格×订单数量 + val sizeForCalc = sizeForAmount?.takeIf { it.isNotBlank() } ?: finalSize // 计算订单金额 = price × size(USDC) val amount = try { val priceDecimal = finalPrice.toSafeBigDecimal() - val sizeDecimal = finalSize.toSafeBigDecimal() + val sizeDecimal = sizeForCalc.toSafeBigDecimal() priceDecimal.multiply(sizeDecimal).toString() } catch (e: Exception) { logger.warn("计算订单金额失败: ${e.message}", e) @@ -430,6 +447,9 @@ class TelegramNotificationService( /** * 发送加密价差策略下单成功通知(与跟单一致:在收到 WS 订单推送时匹配价差策略订单后调用) + * @param price 订单限价 + * @param avgFilledPrice 平均成交价(可选,有成交时优先展示) + * @param filled 已成交数量(可选,与 avgFilledPrice 一起时用于金额计算) */ suspend fun sendCryptoTailOrderSuccessNotification( orderId: String?, @@ -440,6 +460,8 @@ class TelegramNotificationService( outcome: String? = null, price: String, size: String, + avgFilledPrice: String? = null, + filled: String? = null, strategyName: String? = null, accountName: String? = null, walletAddress: String? = null, @@ -464,9 +486,13 @@ class TelegramNotificationService( logger.warn("获取语言设置失败,使用默认语言: ${e.message}", e) java.util.Locale("zh", "CN") } + val displayPrice = avgFilledPrice?.takeIf { it.isNotBlank() } ?: price + val hasAvgFilled = avgFilledPrice != null && avgFilledPrice.isNotBlank() && filled != null && filled.isNotBlank() + val sizeForAmount = if (hasAvgFilled) filled else size + val quantityDisplay = if (hasAvgFilled) filled else size // 有实际成交价时展示数量用 size_matched val amount = try { - val priceDecimal = price.toSafeBigDecimal() - val sizeDecimal = size.toSafeBigDecimal() + val priceDecimal = displayPrice.toSafeBigDecimal() + val sizeDecimal = sizeForAmount.toSafeBigDecimal() priceDecimal.multiply(sizeDecimal).toString() } catch (e: Exception) { logger.warn("计算订单金额失败: ${e.message}", e) @@ -482,8 +508,8 @@ class TelegramNotificationService( marketSlug = marketSlug, side = side, outcome = outcome, - price = price, - size = size, + price = displayPrice, + size = quantityDisplay.orEmpty(), amount = amount, strategyName = strategyName, accountName = accountName, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fd602a3..caeaf1e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -117,28 +117,37 @@ function App() { } } - // 优先使用订单详情中的数据,如果没有则使用 WebSocket 消息中的数据 - const price = orderDetail ? parseFloat(orderDetail.price).toFixed(4) : parseFloat(order.price).toFixed(4) - const size = orderDetail ? parseFloat(orderDetail.size).toFixed(2) : parseFloat(order.original_size).toFixed(2) - const filled = orderDetail ? parseFloat(orderDetail.filled).toFixed(2) : parseFloat(order.size_matched).toFixed(2) + // 实际成交价 = original_size*price/size_matched;有成交时数量用 size_matched + const size = orderDetail ? orderDetail.size : order.original_size + const filled = orderDetail ? orderDetail.filled : order.size_matched + const sizeNum = parseFloat(size).toFixed(2) + const filledNum = parseFloat(filled).toFixed(2) + const hasFilled = parseFloat(filled) > 0 + const price = orderDetail + ? (orderDetail.avgFilledPrice ?? orderDetail.price) + : (hasFilled + ? (parseFloat(order.original_size) * parseFloat(order.price) / parseFloat(order.size_matched)).toString() + : order.price) + const priceStr = parseFloat(price).toFixed(4) const status = orderDetail?.status || 'UNKNOWN' - + // 有成交时展示数量用 size_matched(filled),否则用 original_size + const displaySize = (orderDetail?.avgFilledPrice || (orderDetail == null && hasFilled)) ? filledNum : sizeNum + // 构建描述信息 - let description = `${t('order.market')}: ${marketName}\n${sideText} ${size} @ ${price}` + let description = `${t('order.market')}: ${marketName}\n${sideText} ${displaySize} @ ${priceStr}` // 如果有订单详情,显示更详细的信息 if (orderDetail) { description += `\n${t('order.status')}: ${status}` - if (parseFloat(filled) > 0) { - description += ` | ${t('order.filled')}: ${filled}` + if (parseFloat(filledNum) > 0) { + description += ` | ${t('order.filled')}: ${filledNum}` } - const remaining = (parseFloat(size) - parseFloat(filled)).toFixed(2) + const remaining = (parseFloat(sizeNum) - parseFloat(filledNum)).toFixed(2) if (parseFloat(remaining) > 0) { description += ` | ${t('order.remaining')}: ${remaining}` } } else if (order.type === 'UPDATE' && parseFloat(order.size_matched) > 0) { - // 如果没有订单详情,使用 WebSocket 消息中的已成交数量 - description += `\n${t('order.filled')}: ${filled}` + description += `\n${t('order.filled')}: ${filledNum}` } // 根据订单类型选择通知类型 diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index fd00090..7034f94 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -580,12 +580,13 @@ export interface OrderMessage { /** * 订单详情(通过 API 获取) + * price 为订单限价,avgFilledPrice 为平均成交价(有成交时优先用于展示) */ export interface OrderDetail { id: string // 订单 ID market: string // 市场 ID (condition ID) side: string // BUY/SELL - price: string // 价格 + price: string // 订单限价 size: string // 订单大小 filled: string // 已成交数量 status: string // 订单状态 @@ -593,6 +594,7 @@ export interface OrderDetail { marketName?: string // 市场名称 marketSlug?: string // 市场 slug marketIcon?: string // 市场图标 + avgFilledPrice?: string // 平均成交价(有成交时优先展示) } /**