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
|
val success: Boolean, // boolean indicating if server-side error
|
||||||
@SerializedName("errorMsg")
|
@SerializedName("errorMsg")
|
||||||
val errorMsg: String? = null, // error message in case of unsuccessful placement
|
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")
|
@SerializedName("orderID")
|
||||||
val orderId: String? = null, // id of order(API 返回字段名为 orderID)
|
val orderId: String? = null, // id of order(API 返回字段名为 orderID)
|
||||||
@SerializedName("transactionsHashes")
|
@SerializedName("transactionsHashes")
|
||||||
@@ -222,7 +223,17 @@ data class NewOrderResponse(
|
|||||||
val takingAmount: String? = null, // taking amount
|
val takingAmount: String? = null, // taking amount
|
||||||
@SerializedName("makingAmount")
|
@SerializedName("makingAmount")
|
||||||
val makingAmount: String? = null // making amount
|
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 获取)
|
* 订单详情(通过 API 获取)
|
||||||
|
* @param price 订单限价(用户提交的买入/卖出价)
|
||||||
|
* @param avgFilledPrice 实际成交价 = original_size * price / size_matched(有成交时优先用于推送展示)
|
||||||
*/
|
*/
|
||||||
data class OrderDetailDto(
|
data class OrderDetailDto(
|
||||||
val id: String, // 订单 ID
|
val id: String, // 订单 ID
|
||||||
val market: String, // 市场 ID (condition ID)
|
val market: String, // 市场 ID (condition ID)
|
||||||
val side: String, // BUY/SELL
|
val side: String, // BUY/SELL
|
||||||
val price: String, // 价格
|
val price: String, // 订单限价
|
||||||
val size: String, // 订单大小
|
val size: String, // 订单大小
|
||||||
val filled: String, // 已成交数量
|
val filled: String, // 已成交数量
|
||||||
val status: String, // 订单状态
|
val status: String, // 订单状态
|
||||||
val createdAt: String, // 创建时间(ISO 8601 格式)
|
val createdAt: String, // 创建时间(ISO 8601 格式)
|
||||||
val marketName: String? = null, // 市场名称(通过 Data API 获取)
|
val marketName: String? = null, // 市场名称(通过 Data API 获取)
|
||||||
val marketSlug: String? = null, // 市场 slug
|
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,
|
marketTitle = marketTitle,
|
||||||
marketId = request.marketId,
|
marketId = request.marketId,
|
||||||
marketSlug = marketSlug,
|
marketSlug = marketSlug,
|
||||||
side = request.side,
|
side = "SELL", // 手动卖出订单,方向固定为 SELL
|
||||||
|
outcome = request.side, // request.side 是市场方向(YES/NO)
|
||||||
price = sellPrice, // 直接传递卖出价格
|
price = sellPrice, // 直接传递卖出价格
|
||||||
size = sellQuantity.toPlainString(), // 直接传递卖出数量
|
size = sellQuantity.toPlainString(), // 直接传递卖出数量
|
||||||
accountName = account.accountName,
|
accountName = account.accountName,
|
||||||
@@ -1377,7 +1378,7 @@ class AccountService(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} 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"
|
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)
|
logger.error(fullErrorMsg)
|
||||||
|
|
||||||
@@ -1422,6 +1423,14 @@ class AccountService(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
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 ""}"
|
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)
|
logger.error(fullErrorMsg)
|
||||||
|
|
||||||
@@ -1440,8 +1449,10 @@ class AccountService(
|
|||||||
java.util.Locale("zh", "CN") // 默认简体中文
|
java.util.Locale("zh", "CN") // 默认简体中文
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只传递后端返回的 msg,不传递完整堆栈
|
// 优先使用解析的 API error,其次使用响应体的 errorMsg,最后使用默认消息
|
||||||
val errorMsg = orderResponse.body()?.errorMsg ?: "创建订单失败"
|
val errorMsg = apiError
|
||||||
|
?: orderResponse.body()?.getErrorMessage()
|
||||||
|
?: "创建订单失败 (HTTP ${orderResponse.code()})"
|
||||||
|
|
||||||
telegramNotificationService?.sendOrderFailureNotification(
|
telegramNotificationService?.sendOrderFailureNotification(
|
||||||
marketTitle = marketTitle,
|
marketTitle = marketTitle,
|
||||||
@@ -1451,7 +1462,7 @@ class AccountService(
|
|||||||
outcome = null, // 失败时可能没有 outcome
|
outcome = null, // 失败时可能没有 outcome
|
||||||
price = if (request.orderType == "LIMIT") sellPrice.toString() else "MARKET",
|
price = if (request.orderType == "LIMIT") sellPrice.toString() else "MARKET",
|
||||||
size = sellQuantity.toString(),
|
size = sellQuantity.toString(),
|
||||||
errorMessage = errorMsg, // 只传递后端返回的 msg
|
errorMessage = errorMsg, // 只传递后端返回的错误信息
|
||||||
accountName = account.accountName,
|
accountName = account.accountName,
|
||||||
walletAddress = account.walletAddress,
|
walletAddress = account.walletAddress,
|
||||||
locale = locale
|
locale = locale
|
||||||
|
|||||||
+18
-4
@@ -20,7 +20,12 @@ import com.wrbug.polymarketbot.repository.CopyTradingRepository
|
|||||||
import com.wrbug.polymarketbot.repository.LeaderRepository
|
import com.wrbug.polymarketbot.repository.LeaderRepository
|
||||||
import com.wrbug.polymarketbot.constants.PolymarketConstants
|
import com.wrbug.polymarketbot.constants.PolymarketConstants
|
||||||
import com.wrbug.polymarketbot.service.common.MarketService
|
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 org.springframework.stereotype.Service
|
||||||
|
import java.math.BigDecimal
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -426,20 +431,29 @@ class OrderPushService(
|
|||||||
// 获取市场信息(使用 MarketService,优先从数据库/缓存获取)
|
// 获取市场信息(使用 MarketService,优先从数据库/缓存获取)
|
||||||
val market = marketService.getMarket(conditionId ?: openOrder.market)
|
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),需要转换为字符串
|
// 注意:createdAt 是 unix timestamp (Long),需要转换为字符串
|
||||||
OrderDetailDto(
|
OrderDetailDto(
|
||||||
id = openOrder.id,
|
id = openOrder.id,
|
||||||
market = openOrder.market,
|
market = openOrder.market,
|
||||||
side = openOrder.side,
|
side = openOrder.side,
|
||||||
price = openOrder.price,
|
price = openOrder.price,
|
||||||
size = openOrder.originalSize, // 使用 original_size
|
size = openOrder.originalSize,
|
||||||
filled = openOrder.sizeMatched, // 使用 size_matched
|
filled = openOrder.sizeMatched, // 已成交数量用 size_matched
|
||||||
status = openOrder.status,
|
status = openOrder.status,
|
||||||
createdAt = openOrder.createdAt.toString(), // unix timestamp 转换为字符串
|
createdAt = openOrder.createdAt.toString(), // unix timestamp 转换为字符串
|
||||||
marketName = market?.title,
|
marketName = market?.title,
|
||||||
marketSlug = market?.slug, // 显示用的 slug
|
marketSlug = market?.slug, // 显示用的 slug
|
||||||
marketIcon = market?.icon
|
marketIcon = market?.icon,
|
||||||
|
avgFilledPrice = avgFilledPrice?.toPlainString() // 实际成交价 = original_size*price/size_matched
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onFailure = { e ->
|
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.service.system.TelegramNotificationService
|
||||||
import com.wrbug.polymarketbot.util.RetrofitFactory
|
import com.wrbug.polymarketbot.util.RetrofitFactory
|
||||||
import com.wrbug.polymarketbot.util.CryptoUtils
|
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.multi
|
||||||
|
import com.wrbug.polymarketbot.util.toSafeBigDecimal
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||||
@@ -556,11 +558,13 @@ class OrderStatusUpdateService(
|
|||||||
|
|
||||||
logger.info("更新卖出订单价格成功: orderId=${record.sellOrderId}, 原价格=${record.sellPrice}, 新价格=$actualSellPrice")
|
logger.info("更新卖出订单价格成功: orderId=${record.sellOrderId}, 原价格=${record.sellPrice}, 新价格=$actualSellPrice")
|
||||||
|
|
||||||
// 发送通知(使用实际价格)
|
// 发送通知(使用实际成交价)
|
||||||
sendSellOrderNotification(
|
sendSellOrderNotification(
|
||||||
record = updatedRecord,
|
record = updatedRecord,
|
||||||
actualPrice = actualSellPrice.toString(),
|
actualPrice = actualSellPrice.toString(),
|
||||||
actualSize = record.totalMatchedQuantity.toString(),
|
actualSize = record.totalMatchedQuantity.toString(),
|
||||||
|
avgFilledPrice = actualSellPrice.toString(),
|
||||||
|
filled = record.totalMatchedQuantity.toString(),
|
||||||
account = account,
|
account = account,
|
||||||
copyTrading = copyTrading,
|
copyTrading = copyTrading,
|
||||||
clobApi = clobApi,
|
clobApi = clobApi,
|
||||||
@@ -589,11 +593,13 @@ class OrderStatusUpdateService(
|
|||||||
|
|
||||||
logger.debug("卖出订单价格无需更新: orderId=${record.sellOrderId}, price=$actualSellPrice")
|
logger.debug("卖出订单价格无需更新: orderId=${record.sellOrderId}, price=$actualSellPrice")
|
||||||
|
|
||||||
// 发送通知
|
// 发送通知(使用实际成交价)
|
||||||
sendSellOrderNotification(
|
sendSellOrderNotification(
|
||||||
record = updatedRecord,
|
record = updatedRecord,
|
||||||
actualPrice = actualSellPrice.toString(),
|
actualPrice = actualSellPrice.toString(),
|
||||||
actualSize = record.totalMatchedQuantity.toString(),
|
actualSize = record.totalMatchedQuantity.toString(),
|
||||||
|
avgFilledPrice = actualSellPrice.toString(),
|
||||||
|
filled = record.totalMatchedQuantity.toString(),
|
||||||
account = account,
|
account = account,
|
||||||
copyTrading = copyTrading,
|
copyTrading = copyTrading,
|
||||||
clobApi = clobApi,
|
clobApi = clobApi,
|
||||||
@@ -845,12 +851,24 @@ class OrderStatusUpdateService(
|
|||||||
logger.debug("买入订单数据无需更新: orderId=${order.buyOrderId}")
|
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(
|
sendBuyOrderNotification(
|
||||||
order = updatedOrder,
|
order = updatedOrder,
|
||||||
actualPrice = actualPrice.toString(),
|
actualPrice = actualPrice.toString(),
|
||||||
actualSize = actualSize.toString(),
|
actualSize = actualSize.toString(),
|
||||||
actualOutcome = actualOutcome,
|
actualOutcome = actualOutcome,
|
||||||
|
avgFilledPrice = avgFilledPriceStr,
|
||||||
|
filled = filledSize,
|
||||||
account = account,
|
account = account,
|
||||||
copyTrading = copyTrading,
|
copyTrading = copyTrading,
|
||||||
clobApi = clobApi,
|
clobApi = clobApi,
|
||||||
@@ -877,6 +895,8 @@ class OrderStatusUpdateService(
|
|||||||
actualPrice: String? = null,
|
actualPrice: String? = null,
|
||||||
actualSize: String? = null,
|
actualSize: String? = null,
|
||||||
actualOutcome: String? = null,
|
actualOutcome: String? = null,
|
||||||
|
avgFilledPrice: String? = null, // 平均成交价(有成交时用于 TG 展示)
|
||||||
|
filled: String? = null, // 已成交数量(与 avgFilledPrice 一起用于金额计算)
|
||||||
account: Account? = null,
|
account: Account? = null,
|
||||||
copyTrading: CopyTrading? = null,
|
copyTrading: CopyTrading? = null,
|
||||||
clobApi: PolymarketClobApi? = null,
|
clobApi: PolymarketClobApi? = null,
|
||||||
@@ -939,14 +959,16 @@ class OrderStatusUpdateService(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送通知
|
// 发送通知(优先使用平均成交价展示)
|
||||||
telegramNotificationService.sendOrderSuccessNotification(
|
telegramNotificationService.sendOrderSuccessNotification(
|
||||||
orderId = order.buyOrderId,
|
orderId = order.buyOrderId,
|
||||||
marketTitle = marketTitle,
|
marketTitle = marketTitle,
|
||||||
marketId = order.marketId,
|
marketId = order.marketId,
|
||||||
marketSlug = market?.eventSlug, // 跳转用的 slug
|
marketSlug = market?.eventSlug, // 跳转用的 slug
|
||||||
side = "BUY",
|
side = "BUY",
|
||||||
price = actualPrice ?: order.price.toString(), // 使用实际价格或临时价格
|
price = actualPrice ?: order.price.toString(), // 限价,无 avgFilledPrice 时展示
|
||||||
|
avgFilledPrice = avgFilledPrice,
|
||||||
|
filled = filled,
|
||||||
size = actualSize ?: order.quantity.toString(), // 使用实际数量或临时数量
|
size = actualSize ?: order.quantity.toString(), // 使用实际数量或临时数量
|
||||||
outcome = actualOutcome, // 使用实际 outcome
|
outcome = actualOutcome, // 使用实际 outcome
|
||||||
accountName = finalAccount.accountName,
|
accountName = finalAccount.accountName,
|
||||||
@@ -979,6 +1001,8 @@ class OrderStatusUpdateService(
|
|||||||
actualPrice: String? = null,
|
actualPrice: String? = null,
|
||||||
actualSize: String? = null,
|
actualSize: String? = null,
|
||||||
actualOutcome: String? = null,
|
actualOutcome: String? = null,
|
||||||
|
avgFilledPrice: String? = null, // 平均成交价(有成交时用于 TG 展示)
|
||||||
|
filled: String? = null, // 已成交数量(与 avgFilledPrice 一起用于金额计算)
|
||||||
account: Account? = null,
|
account: Account? = null,
|
||||||
copyTrading: CopyTrading? = null,
|
copyTrading: CopyTrading? = null,
|
||||||
clobApi: PolymarketClobApi? = null,
|
clobApi: PolymarketClobApi? = null,
|
||||||
@@ -1041,14 +1065,16 @@ class OrderStatusUpdateService(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送通知
|
// 发送通知(优先使用平均成交价展示)
|
||||||
telegramNotificationService.sendOrderSuccessNotification(
|
telegramNotificationService.sendOrderSuccessNotification(
|
||||||
orderId = record.sellOrderId,
|
orderId = record.sellOrderId,
|
||||||
marketTitle = marketTitle,
|
marketTitle = marketTitle,
|
||||||
marketId = record.marketId,
|
marketId = record.marketId,
|
||||||
marketSlug = market?.eventSlug, // 跳转用的 slug
|
marketSlug = market?.eventSlug, // 跳转用的 slug
|
||||||
side = "SELL",
|
side = "SELL",
|
||||||
price = actualPrice ?: record.sellPrice.toString(), // 使用实际价格或临时价格
|
price = actualPrice ?: record.sellPrice.toString(), // 限价,无 avgFilledPrice 时展示
|
||||||
|
avgFilledPrice = avgFilledPrice,
|
||||||
|
filled = filled,
|
||||||
size = actualSize ?: record.totalMatchedQuantity.toString(), // 使用实际数量或临时数量
|
size = actualSize ?: record.totalMatchedQuantity.toString(), // 使用实际数量或临时数量
|
||||||
outcome = actualOutcome, // 使用实际 outcome
|
outcome = actualOutcome, // 使用实际 outcome
|
||||||
accountName = finalAccount.accountName,
|
accountName = finalAccount.accountName,
|
||||||
|
|||||||
+16
@@ -6,6 +6,10 @@ import com.wrbug.polymarketbot.repository.CryptoTailStrategyRepository
|
|||||||
import com.wrbug.polymarketbot.repository.CryptoTailStrategyTriggerRepository
|
import com.wrbug.polymarketbot.repository.CryptoTailStrategyTriggerRepository
|
||||||
import com.wrbug.polymarketbot.service.common.MarketService
|
import com.wrbug.polymarketbot.service.common.MarketService
|
||||||
import com.wrbug.polymarketbot.service.system.TelegramNotificationService
|
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.CryptoUtils
|
||||||
import com.wrbug.polymarketbot.util.RetrofitFactory
|
import com.wrbug.polymarketbot.util.RetrofitFactory
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -20,6 +24,7 @@ import org.springframework.scheduling.annotation.Scheduled
|
|||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import jakarta.annotation.PreDestroy
|
import jakarta.annotation.PreDestroy
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加密价差策略订单 TG 通知轮询服务(与跟单一致)
|
* 加密价差策略订单 TG 通知轮询服务(与跟单一致)
|
||||||
@@ -128,6 +133,15 @@ class CryptoTailOrderNotificationPollingService(
|
|||||||
val market = marketService.getMarket(order.market)
|
val market = marketService.getMarket(order.market)
|
||||||
val marketTitle = trigger.marketTitle?.takeIf { it.isNotBlank() } ?: market?.title ?: 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
|
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(
|
telegramNotificationService.sendCryptoTailOrderSuccessNotification(
|
||||||
orderId = orderId,
|
orderId = orderId,
|
||||||
marketTitle = marketTitle,
|
marketTitle = marketTitle,
|
||||||
@@ -137,6 +151,8 @@ class CryptoTailOrderNotificationPollingService(
|
|||||||
outcome = order.outcome,
|
outcome = order.outcome,
|
||||||
price = order.price,
|
price = order.price,
|
||||||
size = order.originalSize,
|
size = order.originalSize,
|
||||||
|
avgFilledPrice = avgFilledPriceStr,
|
||||||
|
filled = filledSize,
|
||||||
strategyName = strategy.name,
|
strategyName = strategy.name,
|
||||||
accountName = account.accountName,
|
accountName = account.accountName,
|
||||||
walletAddress = account.walletAddress,
|
walletAddress = account.walletAddress,
|
||||||
|
|||||||
+31
-3
@@ -352,6 +352,7 @@ class NotificationTemplateService(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染模板(按类型取模板内容后替换变量)
|
* 渲染模板(按类型取模板内容后替换变量)
|
||||||
|
* 优化:先解析模版中需要的变量,只替换这些变量,未提供的变量使用 "-" 占位
|
||||||
*/
|
*/
|
||||||
fun renderTemplate(templateType: String, variables: Map<String, String>): String {
|
fun renderTemplate(templateType: String, variables: Map<String, String>): String {
|
||||||
val template = getTemplate(templateType)
|
val template = getTemplate(templateType)
|
||||||
@@ -361,16 +362,43 @@ class NotificationTemplateService(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 对给定模板内容做变量替换(不查库)
|
* 对给定模板内容做变量替换(不查库)
|
||||||
|
* 优化:先解析模版中的变量占位符,只替换这些变量,未提供的变量使用 "-" 占位
|
||||||
*/
|
*/
|
||||||
fun renderTemplateContent(content: String, variables: Map<String, String>): String {
|
fun renderTemplateContent(content: String, variables: Map<String, String>): String {
|
||||||
|
// 先解析模版中需要的变量
|
||||||
|
val requiredVariables = extractTemplateVariables(content)
|
||||||
|
|
||||||
var result = 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 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,
|
marketId: String? = null,
|
||||||
marketSlug: String? = null,
|
marketSlug: String? = null,
|
||||||
side: String,
|
side: String,
|
||||||
price: String? = null, // 订单价格(可选,如果提供则直接使用)
|
price: String? = null, // 订单限价(可选)
|
||||||
|
avgFilledPrice: String? = null, // 平均成交价(可选,有成交时优先展示)
|
||||||
|
filled: String? = null, // 已成交数量(可选,与 avgFilledPrice 一起时用于金额计算)
|
||||||
size: String? = null, // 订单数量(可选,如果提供则直接使用)
|
size: String? = null, // 订单数量(可选,如果提供则直接使用)
|
||||||
outcome: String? = null, // 市场方向(可选,如果提供则直接使用)
|
outcome: String? = null, // 市场方向(可选,如果提供则直接使用)
|
||||||
accountName: String? = null,
|
accountName: String? = null,
|
||||||
@@ -130,14 +132,21 @@ class TelegramNotificationService(
|
|||||||
java.util.Locale("zh", "CN") // 默认简体中文
|
java.util.Locale("zh", "CN") // 默认简体中文
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先使用传入的价格和数量,如果没有提供则尝试从订单详情获取
|
// 优先使用平均成交价(实际成交价),其次传入的限价,若未提供则从订单详情获取
|
||||||
var actualPrice: String? = price
|
var actualPrice: String? = avgFilledPrice?.takeIf { it.isNotBlank() } ?: price
|
||||||
var actualSize: String? = size
|
var actualSize: String? = size
|
||||||
var actualSide: String = side
|
var actualSide: String = side
|
||||||
var actualOutcome: String? = outcome
|
var actualOutcome: String? = outcome
|
||||||
|
|
||||||
// 如果价格或数量未提供,尝试从订单详情获取
|
// 有平均成交价时,已成交数量优先用 filled,用于金额计算
|
||||||
if ((actualPrice == null || actualSize == null) && orderId != null && clobApi != null && apiKey != null && apiSecret != null && apiPassphrase != null && walletAddressForApi != null) {
|
val sizeForAmount: String? = if (avgFilledPrice != null && avgFilledPrice.isNotBlank() && filled != null && filled.isNotBlank()) {
|
||||||
|
filled
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果价格、数量或市场方向未提供,尝试从订单详情获取
|
||||||
|
if ((actualPrice == null || actualSize == null || actualOutcome == null) && orderId != null && clobApi != null && apiKey != null && apiSecret != null && apiPassphrase != null && walletAddressForApi != null) {
|
||||||
try {
|
try {
|
||||||
val orderResponse = clobApi.getOrder(orderId)
|
val orderResponse = clobApi.getOrder(orderId)
|
||||||
if (orderResponse.isSuccessful) {
|
if (orderResponse.isSuccessful) {
|
||||||
@@ -149,7 +158,8 @@ class TelegramNotificationService(
|
|||||||
if (actualSize == null) {
|
if (actualSize == null) {
|
||||||
actualSize = order.originalSize // 使用 originalSize 作为订单数量
|
actualSize = order.originalSize // 使用 originalSize 作为订单数量
|
||||||
}
|
}
|
||||||
actualSide = order.side // 使用订单详情中的 side
|
// 注意:不覆盖 side,因为传入的 side(BUY/SELL)是正确的
|
||||||
|
// actualSide = order.side // 不要使用订单详情中的 side,因为它可能不准确
|
||||||
if (actualOutcome == null) {
|
if (actualOutcome == null) {
|
||||||
actualOutcome = order.outcome // 使用订单详情中的 outcome(市场方向)
|
actualOutcome = order.outcome // 使用订单详情中的 outcome(市场方向)
|
||||||
}
|
}
|
||||||
@@ -167,12 +177,19 @@ class TelegramNotificationService(
|
|||||||
|
|
||||||
// 如果仍然没有获取到实际值,使用默认值(这种情况不应该发生,但为了兼容性保留)
|
// 如果仍然没有获取到实际值,使用默认值(这种情况不应该发生,但为了兼容性保留)
|
||||||
val finalPrice = actualPrice ?: "0"
|
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)
|
// 计算订单金额 = price × size(USDC)
|
||||||
val amount = try {
|
val amount = try {
|
||||||
val priceDecimal = finalPrice.toSafeBigDecimal()
|
val priceDecimal = finalPrice.toSafeBigDecimal()
|
||||||
val sizeDecimal = finalSize.toSafeBigDecimal()
|
val sizeDecimal = sizeForCalc.toSafeBigDecimal()
|
||||||
priceDecimal.multiply(sizeDecimal).toString()
|
priceDecimal.multiply(sizeDecimal).toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn("计算订单金额失败: ${e.message}", e)
|
logger.warn("计算订单金额失败: ${e.message}", e)
|
||||||
@@ -430,6 +447,9 @@ class TelegramNotificationService(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送加密价差策略下单成功通知(与跟单一致:在收到 WS 订单推送时匹配价差策略订单后调用)
|
* 发送加密价差策略下单成功通知(与跟单一致:在收到 WS 订单推送时匹配价差策略订单后调用)
|
||||||
|
* @param price 订单限价
|
||||||
|
* @param avgFilledPrice 平均成交价(可选,有成交时优先展示)
|
||||||
|
* @param filled 已成交数量(可选,与 avgFilledPrice 一起时用于金额计算)
|
||||||
*/
|
*/
|
||||||
suspend fun sendCryptoTailOrderSuccessNotification(
|
suspend fun sendCryptoTailOrderSuccessNotification(
|
||||||
orderId: String?,
|
orderId: String?,
|
||||||
@@ -440,6 +460,8 @@ class TelegramNotificationService(
|
|||||||
outcome: String? = null,
|
outcome: String? = null,
|
||||||
price: String,
|
price: String,
|
||||||
size: String,
|
size: String,
|
||||||
|
avgFilledPrice: String? = null,
|
||||||
|
filled: String? = null,
|
||||||
strategyName: String? = null,
|
strategyName: String? = null,
|
||||||
accountName: String? = null,
|
accountName: String? = null,
|
||||||
walletAddress: String? = null,
|
walletAddress: String? = null,
|
||||||
@@ -464,9 +486,13 @@ class TelegramNotificationService(
|
|||||||
logger.warn("获取语言设置失败,使用默认语言: ${e.message}", e)
|
logger.warn("获取语言设置失败,使用默认语言: ${e.message}", e)
|
||||||
java.util.Locale("zh", "CN")
|
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 amount = try {
|
||||||
val priceDecimal = price.toSafeBigDecimal()
|
val priceDecimal = displayPrice.toSafeBigDecimal()
|
||||||
val sizeDecimal = size.toSafeBigDecimal()
|
val sizeDecimal = sizeForAmount.toSafeBigDecimal()
|
||||||
priceDecimal.multiply(sizeDecimal).toString()
|
priceDecimal.multiply(sizeDecimal).toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.warn("计算订单金额失败: ${e.message}", e)
|
logger.warn("计算订单金额失败: ${e.message}", e)
|
||||||
@@ -482,8 +508,8 @@ class TelegramNotificationService(
|
|||||||
marketSlug = marketSlug,
|
marketSlug = marketSlug,
|
||||||
side = side,
|
side = side,
|
||||||
outcome = outcome,
|
outcome = outcome,
|
||||||
price = price,
|
price = displayPrice,
|
||||||
size = size,
|
size = quantityDisplay.orEmpty(),
|
||||||
amount = amount,
|
amount = amount,
|
||||||
strategyName = strategyName,
|
strategyName = strategyName,
|
||||||
accountName = accountName,
|
accountName = accountName,
|
||||||
|
|||||||
+19
-10
@@ -117,28 +117,37 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先使用订单详情中的数据,如果没有则使用 WebSocket 消息中的数据
|
// 实际成交价 = original_size*price/size_matched;有成交时数量用 size_matched
|
||||||
const price = orderDetail ? parseFloat(orderDetail.price).toFixed(4) : parseFloat(order.price).toFixed(4)
|
const size = orderDetail ? orderDetail.size : order.original_size
|
||||||
const size = orderDetail ? parseFloat(orderDetail.size).toFixed(2) : parseFloat(order.original_size).toFixed(2)
|
const filled = orderDetail ? orderDetail.filled : order.size_matched
|
||||||
const filled = orderDetail ? parseFloat(orderDetail.filled).toFixed(2) : parseFloat(order.size_matched).toFixed(2)
|
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'
|
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) {
|
if (orderDetail) {
|
||||||
description += `\n${t('order.status')}: ${status}`
|
description += `\n${t('order.status')}: ${status}`
|
||||||
if (parseFloat(filled) > 0) {
|
if (parseFloat(filledNum) > 0) {
|
||||||
description += ` | ${t('order.filled')}: ${filled}`
|
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) {
|
if (parseFloat(remaining) > 0) {
|
||||||
description += ` | ${t('order.remaining')}: ${remaining}`
|
description += ` | ${t('order.remaining')}: ${remaining}`
|
||||||
}
|
}
|
||||||
} else if (order.type === 'UPDATE' && parseFloat(order.size_matched) > 0) {
|
} else if (order.type === 'UPDATE' && parseFloat(order.size_matched) > 0) {
|
||||||
// 如果没有订单详情,使用 WebSocket 消息中的已成交数量
|
description += `\n${t('order.filled')}: ${filledNum}`
|
||||||
description += `\n${t('order.filled')}: ${filled}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据订单类型选择通知类型
|
// 根据订单类型选择通知类型
|
||||||
|
|||||||
@@ -580,12 +580,13 @@ export interface OrderMessage {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单详情(通过 API 获取)
|
* 订单详情(通过 API 获取)
|
||||||
|
* price 为订单限价,avgFilledPrice 为平均成交价(有成交时优先用于展示)
|
||||||
*/
|
*/
|
||||||
export interface OrderDetail {
|
export interface OrderDetail {
|
||||||
id: string // 订单 ID
|
id: string // 订单 ID
|
||||||
market: string // 市场 ID (condition ID)
|
market: string // 市场 ID (condition ID)
|
||||||
side: string // BUY/SELL
|
side: string // BUY/SELL
|
||||||
price: string // 价格
|
price: string // 订单限价
|
||||||
size: string // 订单大小
|
size: string // 订单大小
|
||||||
filled: string // 已成交数量
|
filled: string // 已成交数量
|
||||||
status: string // 订单状态
|
status: string // 订单状态
|
||||||
@@ -593,6 +594,7 @@ export interface OrderDetail {
|
|||||||
marketName?: string // 市场名称
|
marketName?: string // 市场名称
|
||||||
marketSlug?: string // 市场 slug
|
marketSlug?: string // 市场 slug
|
||||||
marketIcon?: string // 市场图标
|
marketIcon?: string // 市场图标
|
||||||
|
avgFilledPrice?: string // 平均成交价(有成交时优先展示)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user