feat(订单推送/通知): 按实际成交价与 size_matched 展示价格与数量
- 实际成交价公式: 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
This commit is contained in:
@@ -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() }
|
||||
?: "创建订单失败"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧的订单请求格式(已废弃,保留用于兼容)
|
||||
|
||||
@@ -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(有成交时使用)
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -401,7 +401,7 @@ class PolymarketClobService(
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取费率
|
||||
* 文档: https://docs.polymarket.com/developers/market-makers/maker-rebates-program#1-fetch-the-fee-rate
|
||||
|
||||
+18
-4
@@ -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 ->
|
||||
|
||||
+34
-8
@@ -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,
|
||||
|
||||
+16
@@ -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,
|
||||
|
||||
+31
-3
@@ -352,6 +352,7 @@ class NotificationTemplateService(
|
||||
|
||||
/**
|
||||
* 渲染模板(按类型取模板内容后替换变量)
|
||||
* 优化:先解析模版中需要的变量,只替换这些变量,未提供的变量使用 "-" 占位
|
||||
*/
|
||||
fun renderTemplate(templateType: String, variables: Map<String, String>): String {
|
||||
val template = getTemplate(templateType)
|
||||
@@ -361,16 +362,43 @@ class NotificationTemplateService(
|
||||
|
||||
/**
|
||||
* 对给定模板内容做变量替换(不查库)
|
||||
* 优化:先解析模版中的变量占位符,只替换这些变量,未提供的变量使用 "-" 占位
|
||||
*/
|
||||
fun renderTemplateContent(content: String, variables: Map<String, String>): 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<String> {
|
||||
val regex = Regex("\\{\\{([^}]+)}}")
|
||||
return regex.findAll(content)
|
||||
.map { it.groupValues[1].trim() }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模版需要的变量过滤输入变量
|
||||
* 只保留模版中实际使用的变量,避免不必要的数据获取
|
||||
*/
|
||||
fun filterVariablesForTemplate(templateType: String, variables: Map<String, String>): Map<String, String> {
|
||||
val template = getTemplate(templateType)
|
||||
val content = template?.templateContent ?: DEFAULT_TEMPLATES[templateType] ?: return emptyMap()
|
||||
val requiredVariables = extractTemplateVariables(content)
|
||||
return variables.filterKeys { it in requiredVariables }
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测试消息
|
||||
*/
|
||||
|
||||
+38
-12
@@ -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,
|
||||
|
||||
+20
-11
@@ -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}`
|
||||
}
|
||||
|
||||
// 根据订单类型选择通知类型
|
||||
|
||||
@@ -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 // 平均成交价(有成交时优先展示)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user