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 d0c8cb5..49c1baf 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
@@ -1331,6 +1331,14 @@ class AccountService(
// 使用当前时间作为订单创建时间
val orderTime = System.currentTimeMillis()
+
+ // 查询可用余额
+ val availableBalance = try {
+ blockchainService.getUsdcBalance(account.walletAddress, account.proxyAddress).getOrNull()
+ } catch (e: Exception) {
+ logger.warn("查询可用余额失败: accountId=${account.id}, ${e.message}")
+ null
+ }
telegramNotificationService?.sendOrderSuccessNotification(
orderId = orderId,
@@ -1348,7 +1356,8 @@ class AccountService(
apiPassphrase = try { cryptoUtils.decrypt(account.apiPassphrase!!) } catch (e: Exception) { null },
walletAddressForApi = account.walletAddress,
locale = locale,
- orderTime = orderTime // 使用订单创建时间
+ orderTime = orderTime, // 使用订单创建时间
+ availableBalance = availableBalance
)
} catch (e: Exception) {
logger.warn("发送订单成功通知失败: ${e.message}", e)
@@ -1800,16 +1809,42 @@ class AccountService(
for (transaction in accountTransactions) {
val account = accounts[transaction.accountId]
if (account != null) {
- telegramNotificationService?.sendRedeemNotification(
- accountName = account.accountName,
- walletAddress = account.walletAddress,
- transactionHash = transaction.transactionHash,
- totalRedeemedValue = transaction.positions.fold(BigDecimal.ZERO) { sum, info ->
- sum.add(info.value.toSafeBigDecimal())
- }.toPlainString(),
- positions = transaction.positions,
- locale = locale
- )
+ // 查询可用余额
+ val availableBalance = try {
+ blockchainService.getUsdcBalance(account.walletAddress, account.proxyAddress).getOrNull()
+ } catch (e: Exception) {
+ logger.warn("查询可用余额失败: accountId=${account.id}, ${e.message}")
+ null
+ }
+
+ // 计算该账户的赎回总价值
+ val accountTotalValue = transaction.positions.fold(BigDecimal.ZERO) { sum, info ->
+ sum.add(info.value.toSafeBigDecimal())
+ }
+
+ // 根据赎回价值选择不同的通知类型
+ if (accountTotalValue.gt(BigDecimal.ZERO)) {
+ // 有收益:发送赎回成功通知
+ telegramNotificationService?.sendRedeemNotification(
+ accountName = account.accountName,
+ walletAddress = account.walletAddress,
+ transactionHash = transaction.transactionHash,
+ totalRedeemedValue = accountTotalValue.toPlainString(),
+ positions = transaction.positions,
+ locale = locale,
+ availableBalance = availableBalance
+ )
+ } else {
+ // 无收益(输的仓位):发送已结算无收益通知
+ telegramNotificationService?.sendRedeemNoReturnNotification(
+ accountName = account.accountName,
+ walletAddress = account.walletAddress,
+ transactionHash = transaction.transactionHash,
+ positions = transaction.positions,
+ locale = locale,
+ availableBalance = availableBalance
+ )
+ }
}
}
} catch (e: Exception) {
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 2f31d63..97f58db 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
@@ -38,7 +38,8 @@ class OrderStatusUpdateService(
private val cryptoUtils: CryptoUtils,
private val trackingService: CopyOrderTrackingService,
private val marketService: MarketService, // 市场信息服务
- private val telegramNotificationService: TelegramNotificationService?
+ private val telegramNotificationService: TelegramNotificationService?,
+ private val blockchainService: com.wrbug.polymarketbot.service.common.BlockchainService
) : ApplicationContextAware {
private val logger = LoggerFactory.getLogger(OrderStatusUpdateService::class.java)
@@ -930,6 +931,14 @@ class OrderStatusUpdateService(
null
}
+ // 查询可用余额
+ val availableBalance = try {
+ blockchainService.getUsdcBalance(finalAccount.walletAddress, finalAccount.proxyAddress).getOrNull()
+ } catch (e: Exception) {
+ logger.warn("查询可用余额失败: accountId=${finalAccount.id}, ${e.message}")
+ null
+ }
+
// 发送通知
telegramNotificationService.sendOrderSuccessNotification(
orderId = order.buyOrderId,
@@ -950,7 +959,8 @@ class OrderStatusUpdateService(
locale = locale,
leaderName = leaderName,
configName = configName,
- orderTime = orderCreatedAt // 使用订单创建时间
+ orderTime = orderCreatedAt, // 使用订单创建时间
+ availableBalance = availableBalance
)
logger.info("买入订单通知已发送: orderId=${order.buyOrderId}, copyTradingId=${order.copyTradingId}")
@@ -1023,6 +1033,14 @@ class OrderStatusUpdateService(
null
}
+ // 查询可用余额
+ val availableBalance = try {
+ blockchainService.getUsdcBalance(finalAccount.walletAddress, finalAccount.proxyAddress).getOrNull()
+ } catch (e: Exception) {
+ logger.warn("查询可用余额失败: accountId=${finalAccount.id}, ${e.message}")
+ null
+ }
+
// 发送通知
telegramNotificationService.sendOrderSuccessNotification(
orderId = record.sellOrderId,
@@ -1043,7 +1061,8 @@ class OrderStatusUpdateService(
locale = locale,
leaderName = leaderName,
configName = configName,
- orderTime = orderCreatedAt // 使用订单创建时间
+ orderTime = orderCreatedAt, // 使用订单创建时间
+ availableBalance = availableBalance
)
logger.info("卖出订单通知已发送: orderId=${record.sellOrderId}, copyTradingId=${record.copyTradingId}")
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 49dba41..c53256d 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
@@ -98,7 +98,8 @@ class TelegramNotificationService(
locale: java.util.Locale? = null,
leaderName: String? = null, // Leader 名称(备注)
configName: String? = null, // 跟单配置名
- orderTime: Long? = null // 订单创建时间(毫秒时间戳),用于通知中的时间显示
+ orderTime: Long? = null, // 订单创建时间(毫秒时间戳),用于通知中的时间显示
+ availableBalance: String? = null // 可用余额(可选)
) {
// 1. 如果提供了 orderId,检查是否已发送过通知(去重)
if (orderId != null) {
@@ -192,7 +193,8 @@ class TelegramNotificationService(
locale = currentLocale,
leaderName = leaderName,
configName = configName,
- orderTime = orderTime
+ orderTime = orderTime,
+ availableBalance = availableBalance
)
sendMessage(message)
}
@@ -766,7 +768,8 @@ class TelegramNotificationService(
locale: java.util.Locale,
leaderName: String? = null, // Leader 名称(备注)
configName: String? = null, // 跟单配置名
- orderTime: Long? = null // 订单创建时间(毫秒时间戳)
+ orderTime: Long? = null, // 订单创建时间(毫秒时间戳)
+ availableBalance: String? = null // 可用余额
): String {
// 获取多语言文本
@@ -781,6 +784,7 @@ class TelegramNotificationService(
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 availableBalanceLabel = messageSource.getMessage("notification.order.available_balance", null, "可用余额", locale)
val unknown = messageSource.getMessage("common.unknown", null, "未知", locale)
val unknownAccount: String = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", locale) ?: "未知账户"
val calculateFailed = messageSource.getMessage("notification.order.calculate_failed", null, "计算失败", locale)
@@ -879,6 +883,23 @@ class TelegramNotificationService(
val priceDisplay = formatPrice(price)
val sizeDisplay = formatQuantity(size)
+ // 格式化可用余额
+ val availableBalanceDisplay = if (!availableBalance.isNullOrBlank()) {
+ try {
+ val balanceDecimal = availableBalance.toSafeBigDecimal()
+ val formatted = if (balanceDecimal.scale() > 4) {
+ balanceDecimal.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros()
+ } else {
+ balanceDecimal.stripTrailingZeros()
+ }
+ "\n• $availableBalanceLabel: ${formatted.toPlainString()} USDC"
+ } catch (e: Exception) {
+ "\n• $availableBalanceLabel: $availableBalance USDC"
+ }
+ } else {
+ ""
+ }
+
return """$icon $orderCreatedSuccess
📊 $orderInfo:
@@ -888,7 +909,7 @@ class TelegramNotificationService(
• $priceLabel: $priceDisplay
• $quantityLabel: $sizeDisplay shares
• $amountLabel: $amountDisplay USDC
-• $accountLabel: $escapedAccountInfo$escapedCopyTradingInfo
+• $accountLabel: $escapedAccountInfo$escapedCopyTradingInfo$availableBalanceDisplay
⏰ $timeLabel: $time"""
}
@@ -1095,6 +1116,7 @@ class TelegramNotificationService(
/**
* 发送仓位赎回通知
* @param locale 语言设置(可选,如果提供则使用,否则使用 LocaleContextHolder 获取)
+ * @param availableBalance 可用余额(可选)
*/
suspend fun sendRedeemNotification(
accountName: String?,
@@ -1102,7 +1124,8 @@ class TelegramNotificationService(
transactionHash: String,
totalRedeemedValue: String,
positions: List,
- locale: java.util.Locale? = null
+ locale: java.util.Locale? = null,
+ availableBalance: String? = null
) {
// 获取语言设置(优先使用传入的 locale,否则从 LocaleContextHolder 获取)
val currentLocale = locale ?: try {
@@ -1118,7 +1141,8 @@ class TelegramNotificationService(
transactionHash = transactionHash,
totalRedeemedValue = totalRedeemedValue,
positions = positions,
- locale = currentLocale
+ locale = currentLocale,
+ availableBalance = availableBalance
)
sendMessage(message)
}
@@ -1132,7 +1156,8 @@ class TelegramNotificationService(
transactionHash: String,
totalRedeemedValue: String,
positions: List,
- locale: java.util.Locale
+ locale: java.util.Locale,
+ availableBalance: String? = null
): String {
// 获取多语言文本
val redeemSuccess = messageSource.getMessage("notification.redeem.success", null, "仓位赎回成功", locale)
@@ -1145,6 +1170,7 @@ class TelegramNotificationService(
val quantityLabel = messageSource.getMessage("notification.order.quantity", null, "数量", locale)
val valueLabel = messageSource.getMessage("notification.order.amount", null, "金额", locale)
val timeLabel = messageSource.getMessage("notification.order.time", null, "时间", locale)
+ val availableBalanceLabel = messageSource.getMessage("notification.redeem.available_balance", null, "可用余额", locale)
val unknownAccount: String = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", locale) ?: "未知账户"
// 构建账户信息(格式:账户名(钱包地址))
@@ -1186,19 +1212,129 @@ class TelegramNotificationService(
" • ${position.marketId.substring(0, 8)}... (${position.side}): $quantityDisplay shares = $valueDisplay USDC"
}
+ // 格式化可用余额
+ val availableBalanceDisplay = if (!availableBalance.isNullOrBlank()) {
+ try {
+ val balanceDecimal = availableBalance.toSafeBigDecimal()
+ val formatted = if (balanceDecimal.scale() > 4) {
+ balanceDecimal.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros()
+ } else {
+ balanceDecimal.stripTrailingZeros()
+ }
+ "\n• $availableBalanceLabel: ${formatted.toPlainString()} USDC"
+ } catch (e: Exception) {
+ "\n• $availableBalanceLabel: $availableBalance USDC"
+ }
+ } else {
+ ""
+ }
+
return """💸 $redeemSuccess
📊 $redeemInfo:
• $accountLabel: $escapedAccountInfo
• $transactionHashLabel: $escapedTxHash
-• $totalValueLabel: $totalValueDisplay USDC
+• $totalValueLabel: $totalValueDisplay USDC$availableBalanceDisplay
📦 $positionsLabel:
$positionsText
⏰ $timeLabel: $time"""
}
-
+
+ /**
+ * 发送仓位已结算(无收益)通知
+ * 用于输的仓位,赎回价值为 0 的情况
+ */
+ suspend fun sendRedeemNoReturnNotification(
+ accountName: String?,
+ walletAddress: String?,
+ transactionHash: String,
+ positions: List,
+ locale: java.util.Locale? = null,
+ availableBalance: String? = null
+ ) {
+ val currentLocale = locale ?: try {
+ LocaleContextHolder.getLocale()
+ } catch (e: Exception) {
+ logger.warn("获取语言设置失败,使用默认语言: ${e.message}", e)
+ java.util.Locale("zh", "CN")
+ }
+
+ val message = buildRedeemNoReturnMessage(
+ accountName = accountName,
+ walletAddress = walletAddress,
+ transactionHash = transactionHash,
+ positions = positions,
+ locale = currentLocale,
+ availableBalance = availableBalance
+ )
+ sendMessage(message)
+ }
+
+ /**
+ * 构建仓位已结算(无收益)消息
+ */
+ private fun buildRedeemNoReturnMessage(
+ accountName: String?,
+ walletAddress: String?,
+ transactionHash: String,
+ positions: List,
+ locale: java.util.Locale,
+ availableBalance: String? = null
+ ): String {
+ val noReturnTitle = messageSource.getMessage("notification.redeem.no_return.title", null, "仓位已结算(无收益)", locale)
+ val noReturnInfo = messageSource.getMessage("notification.redeem.no_return.info", null, "结算信息", locale)
+ val noReturnMessage = messageSource.getMessage("notification.redeem.no_return.message", null, "市场已结算,您的预测未命中,赎回价值为 0。", locale)
+ val accountLabel = messageSource.getMessage("notification.order.account", null, "账户", locale)
+ val transactionHashLabel = messageSource.getMessage("notification.redeem.transaction_hash", null, "交易哈希", locale)
+ val positionsLabel = messageSource.getMessage("notification.redeem.no_return.positions", null, "结算仓位", locale)
+ val timeLabel = messageSource.getMessage("notification.order.time", null, "时间", locale)
+ val availableBalanceLabel = messageSource.getMessage("notification.redeem.available_balance", null, "可用余额", locale)
+ val unknownAccount: String = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", locale) ?: "未知账户"
+
+ val accountInfo = buildAccountInfo(accountName, walletAddress, unknownAccount)
+ val time = DateUtils.formatDateTime()
+
+ val escapedAccountInfo = accountInfo.replace("<", "<").replace(">", ">")
+ val escapedTxHash = transactionHash.replace("<", "<").replace(">", ">")
+
+ val positionsText = positions.joinToString("\n") { position ->
+ val quantityDisplay = formatQuantity(position.quantity)
+ " • ${position.marketId.substring(0, 8)}... (${position.side}): $quantityDisplay shares"
+ }
+
+ // 格式化可用余额
+ val availableBalanceDisplay = if (!availableBalance.isNullOrBlank()) {
+ try {
+ val balanceDecimal = availableBalance.toSafeBigDecimal()
+ val formatted = if (balanceDecimal.scale() > 4) {
+ balanceDecimal.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros()
+ } else {
+ balanceDecimal.stripTrailingZeros()
+ }
+ "\n• $availableBalanceLabel: ${formatted.toPlainString()} USDC"
+ } catch (e: Exception) {
+ "\n• $availableBalanceLabel: $availableBalance USDC"
+ }
+ } else {
+ ""
+ }
+
+ return """📋 $noReturnTitle
+
+📊 $noReturnInfo:
+$noReturnMessage
+
+• $accountLabel: $escapedAccountInfo
+• $transactionHashLabel: $escapedTxHash$availableBalanceDisplay
+
+📦 $positionsLabel:
+$positionsText
+
+⏰ $timeLabel: $time"""
+ }
+
/**
* 脱敏显示地址(只显示前6位和后4位)
*/
diff --git a/backend/src/main/resources/i18n/messages_en.properties b/backend/src/main/resources/i18n/messages_en.properties
index 3cebc68..960115e 100644
--- a/backend/src/main/resources/i18n/messages_en.properties
+++ b/backend/src/main/resources/i18n/messages_en.properties
@@ -13,6 +13,7 @@ notification.order.quantity=Quantity
notification.order.amount=Amount
notification.order.account=Account
notification.order.time=Time
+notification.order.available_balance=Available Balance
notification.order.error_info=Error Information
notification.order.unknown_account=Unknown Account
notification.order.calculate_failed=Calculation Failed
@@ -26,6 +27,13 @@ notification.redeem.position_count=Position Count
notification.redeem.positions=Redeemed Positions
notification.redeem.account=Account
notification.redeem.time=Time
+notification.redeem.available_balance=Available Balance
+
+# Position Settled (No Return)
+notification.redeem.no_return.title=Position Settled (No Return)
+notification.redeem.no_return.info=Settlement Information
+notification.redeem.no_return.message=Market settled. Your prediction was incorrect. Redemption value is 0.
+notification.redeem.no_return.positions=Settled Positions
# Auto Redeem related notifications
notification.auto_redeem.disabled.title=Auto Redeem Disabled
diff --git a/backend/src/main/resources/i18n/messages_zh_CN.properties b/backend/src/main/resources/i18n/messages_zh_CN.properties
index 8d9d961..7fc04f6 100644
--- a/backend/src/main/resources/i18n/messages_zh_CN.properties
+++ b/backend/src/main/resources/i18n/messages_zh_CN.properties
@@ -13,6 +13,7 @@ notification.order.quantity=数量
notification.order.amount=金额
notification.order.account=账户
notification.order.time=时间
+notification.order.available_balance=可用余额
notification.order.error_info=错误信息
notification.order.unknown_account=未知账户
notification.order.calculate_failed=计算失败
@@ -26,6 +27,13 @@ notification.redeem.position_count=仓位数量
notification.redeem.positions=赎回仓位
notification.redeem.account=账户
notification.redeem.time=时间
+notification.redeem.available_balance=可用余额
+
+# 仓位已结算(无收益)
+notification.redeem.no_return.title=仓位已结算(无收益)
+notification.redeem.no_return.info=结算信息
+notification.redeem.no_return.message=市场已结算,您的预测未命中,赎回价值为 0。
+notification.redeem.no_return.positions=结算仓位
# 自动赎回相关通知
notification.auto_redeem.disabled.title=自动赎回未开启
diff --git a/backend/src/main/resources/i18n/messages_zh_TW.properties b/backend/src/main/resources/i18n/messages_zh_TW.properties
index 5d06e6c..a71d80c 100644
--- a/backend/src/main/resources/i18n/messages_zh_TW.properties
+++ b/backend/src/main/resources/i18n/messages_zh_TW.properties
@@ -13,6 +13,7 @@ notification.order.quantity=數量
notification.order.amount=金額
notification.order.account=賬戶
notification.order.time=時間
+notification.order.available_balance=可用餘額
notification.order.error_info=錯誤信息
notification.order.unknown_account=未知賬戶
notification.order.calculate_failed=計算失敗
@@ -26,6 +27,13 @@ notification.redeem.position_count=倉位數量
notification.redeem.positions=贖回倉位
notification.redeem.account=賬戶
notification.redeem.time=時間
+notification.redeem.available_balance=可用餘額
+
+# 倉位已結算(無收益)
+notification.redeem.no_return.title=倉位已結算(無收益)
+notification.redeem.no_return.info=結算信息
+notification.redeem.no_return.message=市場已結算,您的預測未命中,贖回價值為 0。
+notification.redeem.no_return.positions=結算倉位
# 自動贖回相關通知
notification.auto_redeem.disabled.title=自動贖回未開啟