feat(cryptotail): 尾盘下单成功 TG 通知,通过 subscribe 订阅订单广播
- CryptoTailStrategyTriggerRepository 新增 findByOrderId 用于 WS 匹配 - TelegramNotificationService 新增 sendCryptoTailOrderSuccessNotification 及多语言 - 新增 CryptoTailOrderNotificationSubscriber,与跟单一致通过 OrderPushService.subscribeAllEnabled 订阅订单推送,匹配尾盘订单后发 TG - i18n: notification.tail.order.success, notification.tail.strategy (zh-CN/zh-TW/en) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+3
@@ -18,6 +18,9 @@ interface CryptoTailStrategyTriggerRepository : JpaRepository<CryptoTailStrategy
|
||||
/** 轮询结算:仅处理下单成功的订单(status=success 且 orderId 非空)、且未结算的触发记录 */
|
||||
fun findByStatusAndResolvedAndOrderIdIsNotNullOrderByCreatedAtAsc(status: String, resolved: Boolean): List<CryptoTailStrategyTrigger>
|
||||
|
||||
/** 根据订单 ID 查询尾盘触发记录(用于 WS 推送时匹配并发送 TG 通知) */
|
||||
fun findByOrderId(orderId: String): CryptoTailStrategyTrigger?
|
||||
|
||||
/** 策略已结算订单的总已实现盈亏(用于收益统计) */
|
||||
@Query("SELECT COALESCE(SUM(t.realizedPnl), 0) FROM CryptoTailStrategyTrigger t WHERE t.strategyId = :strategyId AND t.resolved = true")
|
||||
fun sumRealizedPnlByStrategyId(@Param("strategyId") strategyId: Long): BigDecimal?
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.wrbug.polymarketbot.service.cryptotail
|
||||
|
||||
import com.wrbug.polymarketbot.dto.OrderPushMessage
|
||||
import com.wrbug.polymarketbot.repository.AccountRepository
|
||||
import com.wrbug.polymarketbot.repository.CryptoTailStrategyRepository
|
||||
import com.wrbug.polymarketbot.repository.CryptoTailStrategyTriggerRepository
|
||||
import com.wrbug.polymarketbot.service.copytrading.orders.OrderPushService
|
||||
import com.wrbug.polymarketbot.service.system.TelegramNotificationService
|
||||
import jakarta.annotation.PostConstruct
|
||||
import jakarta.annotation.PreDestroy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
/**
|
||||
* 尾盘策略订单 TG 通知订阅者
|
||||
* 与跟单订单广播方式一致:通过 OrderPushService.subscribeAllEnabled 订阅订单推送,
|
||||
* 收到广播后匹配是否为尾盘订单,若是则发送 TG 通知。
|
||||
*/
|
||||
@Service
|
||||
class CryptoTailOrderNotificationSubscriber(
|
||||
private val orderPushService: OrderPushService,
|
||||
private val triggerRepository: CryptoTailStrategyTriggerRepository,
|
||||
private val strategyRepository: CryptoTailStrategyRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val telegramNotificationService: TelegramNotificationService
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(CryptoTailOrderNotificationSubscriber::class.java)
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
private var orderPushCallback: ((OrderPushMessage) -> Unit)? = null
|
||||
|
||||
@PostConstruct
|
||||
fun subscribe() {
|
||||
val callback: (OrderPushMessage) -> Unit = { message -> onOrderPush(message) }
|
||||
orderPushCallback = callback
|
||||
orderPushService.subscribeAllEnabled(callback)
|
||||
logger.info("尾盘订单 TG 通知已订阅 OrderPushService 广播")
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
fun unsubscribe() {
|
||||
orderPushCallback?.let { orderPushService.unsubscribeAll(it) }
|
||||
orderPushCallback = null
|
||||
logger.info("尾盘订单 TG 通知已取消订阅")
|
||||
}
|
||||
|
||||
private fun onOrderPush(message: OrderPushMessage) {
|
||||
val trigger = triggerRepository.findByOrderId(message.order.id) ?: return
|
||||
val strategy = strategyRepository.findById(trigger.strategyId).orElse(null) ?: return
|
||||
val account = accountRepository.findById(strategy.accountId).orElse(null) ?: return
|
||||
val orderTimeMs = message.order.timestamp.toLongOrNull()?.let { ts ->
|
||||
if (ts < 1_000_000_000_000L) ts * 1000 else ts
|
||||
}
|
||||
scope.launch {
|
||||
try {
|
||||
telegramNotificationService.sendCryptoTailOrderSuccessNotification(
|
||||
orderId = message.order.id,
|
||||
marketTitle = trigger.marketTitle ?: message.orderDetail?.marketName ?: "",
|
||||
marketId = message.order.market,
|
||||
marketSlug = message.orderDetail?.marketSlug,
|
||||
side = message.order.side,
|
||||
outcome = message.order.outcome,
|
||||
price = message.order.price,
|
||||
size = message.order.originalSize,
|
||||
strategyName = strategy.name,
|
||||
accountName = account.accountName,
|
||||
walletAddress = account.walletAddress,
|
||||
orderTime = orderTimeMs
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("尾盘订单 TG 通知失败: orderId=${message.order.id}, ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+142
@@ -303,6 +303,69 @@ class TelegramNotificationService(
|
||||
sendMessage(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送尾盘策略下单成功通知(与跟单一致:在收到 WS 订单推送时匹配尾盘订单后调用)
|
||||
*/
|
||||
suspend fun sendCryptoTailOrderSuccessNotification(
|
||||
orderId: String?,
|
||||
marketTitle: String,
|
||||
marketId: String? = null,
|
||||
marketSlug: String? = null,
|
||||
side: String,
|
||||
outcome: String? = null,
|
||||
price: String,
|
||||
size: String,
|
||||
strategyName: String? = null,
|
||||
accountName: String? = null,
|
||||
walletAddress: String? = null,
|
||||
locale: java.util.Locale? = null,
|
||||
orderTime: Long? = null
|
||||
) {
|
||||
if (orderId != null) {
|
||||
val lastSentTime = sentOrderIds[orderId]
|
||||
if (lastSentTime != null && System.currentTimeMillis() - lastSentTime < 5 * 60 * 1000) {
|
||||
logger.info("尾盘订单通知已发送过(5分钟内),跳过: orderId=$orderId")
|
||||
return
|
||||
}
|
||||
sentOrderIds[orderId] = System.currentTimeMillis()
|
||||
if (sentOrderIds.size > 1000) {
|
||||
val expiryTime = System.currentTimeMillis() - 5 * 60 * 1000
|
||||
sentOrderIds.entries.removeIf { it.value < expiryTime }
|
||||
}
|
||||
}
|
||||
val currentLocale = locale ?: try {
|
||||
LocaleContextHolder.getLocale()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("获取语言设置失败,使用默认语言: ${e.message}", e)
|
||||
java.util.Locale("zh", "CN")
|
||||
}
|
||||
val amount = try {
|
||||
val priceDecimal = price.toSafeBigDecimal()
|
||||
val sizeDecimal = size.toSafeBigDecimal()
|
||||
priceDecimal.multiply(sizeDecimal).toString()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("计算订单金额失败: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
val message = buildCryptoTailOrderSuccessMessage(
|
||||
orderId = orderId,
|
||||
marketTitle = marketTitle,
|
||||
marketId = marketId,
|
||||
marketSlug = marketSlug,
|
||||
side = side,
|
||||
outcome = outcome,
|
||||
price = price,
|
||||
size = size,
|
||||
amount = amount,
|
||||
strategyName = strategyName,
|
||||
accountName = accountName,
|
||||
walletAddress = walletAddress,
|
||||
locale = currentLocale,
|
||||
orderTime = orderTime
|
||||
)
|
||||
sendMessage(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建订单被过滤消息
|
||||
*/
|
||||
@@ -827,6 +890,85 @@ class TelegramNotificationService(
|
||||
• $amountLabel: <code>$amountDisplay</code> USDC
|
||||
• $accountLabel: $escapedAccountInfo$escapedCopyTradingInfo
|
||||
|
||||
⏰ $timeLabel: <code>$time</code>"""
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建尾盘策略下单成功消息(与订单成功格式一致,增加「尾盘策略」标题与策略名)
|
||||
*/
|
||||
private fun buildCryptoTailOrderSuccessMessage(
|
||||
orderId: String?,
|
||||
marketTitle: String,
|
||||
marketId: String?,
|
||||
marketSlug: String?,
|
||||
side: String,
|
||||
outcome: String?,
|
||||
price: String,
|
||||
size: String,
|
||||
amount: String?,
|
||||
strategyName: String?,
|
||||
accountName: String?,
|
||||
walletAddress: String?,
|
||||
locale: java.util.Locale,
|
||||
orderTime: Long?
|
||||
): String {
|
||||
val tailOrderSuccess = messageSource.getMessage("notification.tail.order.success", null, "尾盘策略下单成功", locale)
|
||||
val strategyLabel = messageSource.getMessage("notification.tail.strategy", null, "策略", locale)
|
||||
val orderInfo = messageSource.getMessage("notification.order.info", null, "订单信息", locale)
|
||||
val orderIdLabel = messageSource.getMessage("notification.order.id", null, "订单ID", locale)
|
||||
val marketLabel = messageSource.getMessage("notification.order.market", null, "市场", locale)
|
||||
val sideLabel = messageSource.getMessage("notification.order.side", null, "方向", locale)
|
||||
val outcomeLabel = messageSource.getMessage("notification.order.outcome", null, "市场方向", locale)
|
||||
val priceLabel = messageSource.getMessage("notification.order.price", null, "价格", locale)
|
||||
val quantityLabel = messageSource.getMessage("notification.order.quantity", null, "数量", locale)
|
||||
val amountLabel = messageSource.getMessage("notification.order.amount", null, "金额", locale)
|
||||
val accountLabel = messageSource.getMessage("notification.order.account", null, "账户", locale)
|
||||
val timeLabel = messageSource.getMessage("notification.order.time", null, "时间", locale)
|
||||
val unknown: String = messageSource.getMessage("common.unknown", null, "未知", locale) ?: "未知"
|
||||
val unknownAccount = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", locale) ?: "未知账户"
|
||||
val calculateFailed = messageSource.getMessage("notification.order.calculate_failed", null, "计算失败", locale)
|
||||
val sideDisplay = when (side.uppercase()) {
|
||||
"BUY" -> messageSource.getMessage("notification.order.side.buy", null, "买入", locale)
|
||||
"SELL" -> messageSource.getMessage("notification.order.side.sell", null, "卖出", locale)
|
||||
else -> side
|
||||
}
|
||||
val accountInfo = buildAccountInfo(accountName, walletAddress, unknownAccount)
|
||||
val time = if (orderTime != null) DateUtils.formatDateTime(orderTime) else DateUtils.formatDateTime()
|
||||
val escapedMarketTitle = marketTitle.replace("<", "<").replace(">", ">")
|
||||
val escapedAccountInfo = accountInfo.replace("<", "<").replace(">", ">")
|
||||
val strategyDisplay = strategyName?.takeIf { it.isNotBlank() } ?: unknown
|
||||
val escapedStrategyName = strategyDisplay.replace("<", "<").replace(">", ">")
|
||||
val amountDisplay = if (amount != null) {
|
||||
try {
|
||||
val amountDecimal = amount.toSafeBigDecimal()
|
||||
val formatted = if (amountDecimal.scale() > 4) amountDecimal.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros() else amountDecimal.stripTrailingZeros()
|
||||
formatted.toPlainString()
|
||||
} catch (e: Exception) { amount }
|
||||
} else calculateFailed
|
||||
val marketLink = when {
|
||||
!marketSlug.isNullOrBlank() -> "https://polymarket.com/event/$marketSlug"
|
||||
!marketId.isNullOrBlank() && marketId.startsWith("0x") -> "https://polymarket.com/condition/$marketId"
|
||||
else -> null
|
||||
}
|
||||
val marketDisplay = if (marketLink != null) "<a href=\"$marketLink\">$escapedMarketTitle</a>" else escapedMarketTitle
|
||||
val outcomeDisplay = if (!outcome.isNullOrBlank()) {
|
||||
val escapedOutcome = outcome.replace("<", "<").replace(">", ">")
|
||||
"\n• $outcomeLabel: <b>$escapedOutcome</b>"
|
||||
} else ""
|
||||
val priceDisplay = formatPrice(price)
|
||||
val sizeDisplay = formatQuantity(size)
|
||||
return """🚀 <b>$tailOrderSuccess</b>
|
||||
|
||||
📊 <b>$orderInfo:</b>
|
||||
• $orderIdLabel: <code>${orderId ?: unknown}</code>
|
||||
• $strategyLabel: $escapedStrategyName
|
||||
• $marketLabel: $marketDisplay$outcomeDisplay
|
||||
• $sideLabel: <b>$sideDisplay</b>
|
||||
• $priceLabel: <code>$priceDisplay</code>
|
||||
• $quantityLabel: <code>$sizeDisplay</code> shares
|
||||
• $amountLabel: <code>$amountDisplay</code> USDC
|
||||
• $accountLabel: $escapedAccountInfo
|
||||
|
||||
⏰ $timeLabel: <code>$time</code>"""
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ notification.order.time=Time
|
||||
notification.order.error_info=Error Information
|
||||
notification.order.unknown_account=Unknown Account
|
||||
notification.order.calculate_failed=Calculation Failed
|
||||
notification.tail.order.success=Tail Session Order Success
|
||||
notification.tail.strategy=Strategy
|
||||
notification.redeem.success=Position Redeemed Successfully
|
||||
notification.redeem.info=Redeem Information
|
||||
notification.redeem.transaction_hash=Transaction Hash
|
||||
|
||||
@@ -16,6 +16,8 @@ notification.order.time=时间
|
||||
notification.order.error_info=错误信息
|
||||
notification.order.unknown_account=未知账户
|
||||
notification.order.calculate_failed=计算失败
|
||||
notification.tail.order.success=尾盘策略下单成功
|
||||
notification.tail.strategy=策略
|
||||
notification.redeem.success=仓位赎回成功
|
||||
notification.redeem.info=赎回信息
|
||||
notification.redeem.transaction_hash=交易哈希
|
||||
|
||||
@@ -16,6 +16,8 @@ notification.order.time=時間
|
||||
notification.order.error_info=錯誤信息
|
||||
notification.order.unknown_account=未知賬戶
|
||||
notification.order.calculate_failed=計算失敗
|
||||
notification.tail.order.success=尾盤策略下單成功
|
||||
notification.tail.strategy=策略
|
||||
notification.redeem.success=倉位贖回成功
|
||||
notification.redeem.info=贖回信息
|
||||
notification.redeem.transaction_hash=交易哈希
|
||||
|
||||
Reference in New Issue
Block a user