feat: 实现卖出订单价格轮询更新和订单状态管理

- 添加 price_updated 字段到 sell_match_record 表,用于标记价格是否已更新
- 创建 OrderStatusUpdateService 定时任务服务,每5秒轮询一次:
  - 更新卖出订单的实际成交价(通过 orderId 查询订单详情)
  - 清理已删除账户的订单记录
- 修改 processSellTrade:下单完成后不再立即查询价格,直接保存,等待定时任务更新
- 添加 orderId 格式验证:非0x开头的直接标记为已更新,0x开头的等待定时任务更新
- 启用 Spring 定时任务功能(@EnableScheduling)
- 将 getActualExecutionPrice 方法改为 public,供定时任务调用
This commit is contained in:
WrBug
2025-12-26 02:20:48 +08:00
parent a6ca6cc3ff
commit 62669719d9
6 changed files with 447 additions and 7 deletions
@@ -2,8 +2,10 @@ package com.wrbug.polymarketbot
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.scheduling.annotation.EnableScheduling
@SpringBootApplication
@EnableScheduling
class PolymarketBotApplication
fun main(args: Array<String>) {
@@ -41,6 +41,9 @@ data class SellMatchRecord(
@Column(name = "total_realized_pnl", nullable = false, precision = 20, scale = 8)
val totalRealizedPnl: BigDecimal, // 总已实现盈亏
@Column(name = "price_updated", nullable = false)
var priceUpdated: Boolean = false, // 价格是否已更新(从订单详情获取实际成交价)
@Column(name = "created_at", nullable = false)
val createdAt: Long = System.currentTimeMillis()
)
@@ -24,5 +24,10 @@ interface SellMatchRecordRepository : JpaRepository<SellMatchRecord, Long> {
* 根据Leader卖出交易ID查询记录
*/
fun findByLeaderSellTradeId(leaderSellTradeId: String): SellMatchRecord?
/**
* 查询所有价格未更新的卖出记录
*/
fun findByPriceUpdatedFalse(): List<SellMatchRecord>
}
@@ -567,6 +567,12 @@ open class CopyOrderTrackingService(
}
val realOrderId = createOrderResult.getOrNull() ?: continue
// 验证 orderId 格式(必须以 0x 开头的 16 进制)
if (!isValidOrderId(realOrderId)) {
logger.warn("买入订单ID格式无效,跳过保存: orderId=$realOrderId")
continue
}
// 创建买入订单跟踪记录(使用真实订单ID,使用outcomeIndex
val tracking = CopyOrderTracking(
@@ -942,8 +948,20 @@ open class CopyOrderTrackingService(
}
val realSellOrderId = createOrderResult.getOrNull() ?: return
// 12. 下单时直接使用下单价格保存,等待定时任务更新实际成交价
// priceUpdated 统一由定时任务更新,下单时统一设置为 false(非0x开头的除外)
val priceUpdated = !realSellOrderId.startsWith("0x", ignoreCase = true)
if (priceUpdated) {
logger.debug("卖出订单ID非0x开头,标记为已更新: orderId=$realSellOrderId")
} else {
logger.debug("卖出订单ID为0x开头,等待定时任务更新价格: orderId=$realSellOrderId")
}
// 使用下单价格,等待定时任务更新实际成交价
val actualSellPrice = sellPrice
// 12. 更新买入订单跟踪状态
// 13. 更新买入订单跟踪状态
for (order in unmatchedOrders) {
val detail = matchDetails.find { it.trackingId == order.id }
if (detail != null) {
@@ -956,8 +974,17 @@ open class CopyOrderTrackingService(
}
}
// 13. 创建卖出匹配记录(使用真实订单ID,使用outcomeIndex
val totalRealizedPnl = matchDetails.sumOf { it.realizedPnl.toSafeBigDecimal() }
// 14. 重新计算盈亏(使用实际成交价
val updatedMatchDetails = matchDetails.map { detail ->
val updatedRealizedPnl = actualSellPrice.subtract(detail.buyPrice).multi(detail.matchedQuantity)
detail.copy(
sellPrice = actualSellPrice,
realizedPnl = updatedRealizedPnl
)
}
// 15. 创建卖出匹配记录(使用真实订单ID和实际成交价)
val totalRealizedPnl = updatedMatchDetails.sumOf { it.realizedPnl.toSafeBigDecimal() }
val matchRecord = SellMatchRecord(
copyTradingId = copyTrading.id,
@@ -967,14 +994,15 @@ open class CopyOrderTrackingService(
side = leaderSellTrade.outcomeIndex.toString(), // 使用outcomeIndex作为side(兼容旧数据)
outcomeIndex = leaderSellTrade.outcomeIndex, // 新增字段
totalMatchedQuantity = totalMatched,
sellPrice = sellPrice,
totalRealizedPnl = totalRealizedPnl
sellPrice = actualSellPrice, // 使用实际成交价(如果查询失败则为下单价格)
totalRealizedPnl = totalRealizedPnl,
priceUpdated = priceUpdated // 标记价格是否已更新
)
val savedRecord = sellMatchRecordRepository.save(matchRecord)
// 14. 保存匹配明细
for (detail in matchDetails) {
// 16. 保存匹配明细(使用实际成交价)
for (detail in updatedMatchDetails) {
val savedDetail = detail.copy(matchRecordId = savedRecord.id!!)
sellMatchDetailRepository.save(savedDetail)
}
@@ -1382,6 +1410,111 @@ open class CopyOrderTrackingService(
}
}
/**
* 验证订单ID格式
* 订单ID必须以 0x 开头,且是有效的 16 进制字符串
*
* @param orderId 订单ID
* @return 如果格式有效返回 true,否则返回 false
*/
private fun isValidOrderId(orderId: String): Boolean {
if (!orderId.startsWith("0x", ignoreCase = true)) {
return false
}
// 验证是否为有效的 16 进制字符串(去除 0x 前缀后)
val hexPart = orderId.substring(2)
if (hexPart.isEmpty()) {
return false
}
// 检查是否只包含 0-9, a-f, A-F
return hexPart.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }
}
/**
* 获取订单的实际成交价
* 通过查询订单详情和关联的交易记录,计算加权平均成交价
*
* @param orderId 订单ID
* @param clobApi CLOB API 客户端(已认证)
* @param fallbackPrice 如果查询失败,使用此价格作为默认值
* @return 实际成交价(加权平均),如果查询失败则返回 fallbackPrice
*/
suspend fun getActualExecutionPrice(
orderId: String,
clobApi: PolymarketClobApi,
fallbackPrice: BigDecimal
): BigDecimal {
return try {
// 1. 查询订单详情
val orderResponse = clobApi.getOrder(orderId)
if (!orderResponse.isSuccessful || orderResponse.body() == null) {
logger.warn("查询订单详情失败: orderId=$orderId, code=${orderResponse.code()}")
return fallbackPrice
}
val order = orderResponse.body()!!
// 2. 如果订单未成交,使用下单价格
if (order.status != "FILLED" && order.sizeMatched.toSafeBigDecimal() <= BigDecimal.ZERO) {
logger.debug("订单未成交,使用下单价格: orderId=$orderId, status=${order.status}")
return fallbackPrice
}
// 3. 如果订单已成交,通过 associateTrades 获取交易记录
val associateTrades = order.associateTrades
if (associateTrades.isNullOrEmpty()) {
logger.debug("订单无关联交易记录,使用下单价格: orderId=$orderId")
return fallbackPrice
}
// 4. 查询所有关联的交易记录
val trades = mutableListOf<TradeResponse>()
for (tradeId in associateTrades) {
try {
val tradesResponse = clobApi.getTrades(id = tradeId)
if (tradesResponse.isSuccessful && tradesResponse.body() != null) {
val tradesData = tradesResponse.body()!!.data
trades.addAll(tradesData)
}
} catch (e: Exception) {
logger.warn("查询交易记录失败: tradeId=$tradeId, error=${e.message}")
}
}
if (trades.isEmpty()) {
logger.debug("未找到交易记录,使用下单价格: orderId=$orderId")
return fallbackPrice
}
// 5. 计算加权平均成交价
// 加权平均 = Σ(price * size) / Σ(size)
var totalAmount = BigDecimal.ZERO
var totalSize = BigDecimal.ZERO
for (trade in trades) {
val tradePrice = trade.price.toSafeBigDecimal()
val tradeSize = trade.size.toSafeBigDecimal()
if (tradeSize > BigDecimal.ZERO) {
totalAmount = totalAmount.add(tradePrice.multiply(tradeSize))
totalSize = totalSize.add(tradeSize)
}
}
if (totalSize > BigDecimal.ZERO) {
val weightedAveragePrice = totalAmount.divide(totalSize, 8, java.math.RoundingMode.HALF_UP)
logger.info("计算实际成交价成功: orderId=$orderId, 加权平均价=$weightedAveragePrice, 下单价格=$fallbackPrice, 交易笔数=${trades.size}")
return weightedAveragePrice
} else {
logger.warn("交易记录数量为0,使用下单价格: orderId=$orderId")
return fallbackPrice
}
} catch (e: Exception) {
logger.error("获取实际成交价异常: orderId=$orderId, error=${e.message}", e)
return fallbackPrice
}
}
/**
* 从trade中提取side(结果名称)
*
@@ -0,0 +1,289 @@
package com.wrbug.polymarketbot.service.copytrading.statistics
import com.wrbug.polymarketbot.api.PolymarketClobApi
import com.wrbug.polymarketbot.entity.*
import com.wrbug.polymarketbot.repository.*
import com.wrbug.polymarketbot.util.RetrofitFactory
import com.wrbug.polymarketbot.util.CryptoUtils
import com.wrbug.polymarketbot.util.toSafeBigDecimal
import com.wrbug.polymarketbot.util.multi
import kotlinx.coroutines.*
import org.slf4j.LoggerFactory
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.context.event.EventListener
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
/**
* 订单状态更新服务
* 定时轮询更新卖出订单的实际成交价,并清理已删除账户的订单
*/
@Service
class OrderStatusUpdateService(
private val sellMatchRecordRepository: SellMatchRecordRepository,
private val sellMatchDetailRepository: SellMatchDetailRepository,
private val copyTradingRepository: CopyTradingRepository,
private val accountRepository: AccountRepository,
private val copyOrderTrackingRepository: CopyOrderTrackingRepository,
private val retrofitFactory: RetrofitFactory,
private val cryptoUtils: CryptoUtils,
private val trackingService: CopyOrderTrackingService
) {
private val logger = LoggerFactory.getLogger(OrderStatusUpdateService::class.java)
private val updateScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@EventListener(ApplicationReadyEvent::class)
fun onApplicationReady() {
logger.info("订单状态更新服务已启动,将每5秒轮询一次")
}
/**
* 定时更新卖出订单价格
* 每5秒执行一次
*/
@Scheduled(fixedDelay = 5000)
fun updateSellOrderPrices() {
updateScope.launch {
try {
// 1. 清理已删除账户的订单
cleanupDeletedAccountOrders()
// 2. 更新卖出订单的实际成交价
updatePendingSellOrderPrices()
} catch (e: Exception) {
logger.error("订单状态更新异常: ${e.message}", e)
}
}
}
/**
* 验证订单ID格式
* 订单ID必须以 0x 开头,且是有效的 16 进制字符串
*
* @param orderId 订单ID
* @return 如果格式有效返回 true,否则返回 false
*/
private fun isValidOrderId(orderId: String): Boolean {
if (!orderId.startsWith("0x", ignoreCase = true)) {
return false
}
// 验证是否为有效的 16 进制字符串(去除 0x 前缀后)
val hexPart = orderId.substring(2)
if (hexPart.isEmpty()) {
return false
}
// 检查是否只包含 0-9, a-f, A-F
return hexPart.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }
}
/**
* 清理已删除账户的订单
*/
@Transactional
private suspend fun cleanupDeletedAccountOrders() {
try {
// 查询所有卖出记录
val allRecords = sellMatchRecordRepository.findAll()
// 查询所有有效的账户ID
val validAccountIds = accountRepository.findAll().mapNotNull { it.id }.toSet()
// 查询所有有效的跟单关系
val validCopyTradingIds = copyTradingRepository.findAll()
.filter { it.accountId in validAccountIds }
.mapNotNull { it.id }
.toSet()
// 找出需要删除的记录(关联的跟单关系已不存在或账户已删除)
val recordsToDelete = allRecords.filter { record ->
val copyTrading = copyTradingRepository.findById(record.copyTradingId).orElse(null)
copyTrading == null || copyTrading.accountId !in validAccountIds
}
if (recordsToDelete.isNotEmpty()) {
logger.info("清理已删除账户的订单: ${recordsToDelete.size} 条记录")
// 删除匹配明细
for (record in recordsToDelete) {
val details = sellMatchDetailRepository.findByMatchRecordId(record.id!!)
sellMatchDetailRepository.deleteAll(details)
}
// 删除卖出记录
sellMatchRecordRepository.deleteAll(recordsToDelete)
logger.info("已清理 ${recordsToDelete.size} 条已删除账户的订单记录")
}
} catch (e: Exception) {
logger.error("清理已删除账户订单异常: ${e.message}", e)
}
}
/**
* 更新待更新的卖出订单价格
*/
@Transactional
private suspend fun updatePendingSellOrderPrices() {
try {
// 查询所有价格未更新的卖出记录
val pendingRecords = sellMatchRecordRepository.findByPriceUpdatedFalse()
if (pendingRecords.isEmpty()) {
return
}
logger.debug("找到 ${pendingRecords.size} 条待更新价格的卖出订单")
for (record in pendingRecords) {
try {
// 获取跟单关系
val copyTrading = copyTradingRepository.findById(record.copyTradingId).orElse(null)
if (copyTrading == null) {
logger.warn("跟单关系不存在,跳过更新: copyTradingId=${record.copyTradingId}")
continue
}
// 获取账户
val account = accountRepository.findById(copyTrading.accountId).orElse(null)
if (account == null) {
logger.warn("账户不存在,跳过更新: accountId=${copyTrading.accountId}")
continue
}
// 检查账户是否配置了 API 凭证
if (account.apiKey == null || account.apiSecret == null || account.apiPassphrase == null) {
logger.debug("账户未配置 API 凭证,跳过更新: accountId=${account.id}")
continue
}
// 解密 API 凭证
val apiSecret = try {
cryptoUtils.decrypt(account.apiSecret!!)
} catch (e: Exception) {
logger.warn("解密 API Secret 失败: accountId=${account.id}, error=${e.message}")
continue
}
val apiPassphrase = try {
cryptoUtils.decrypt(account.apiPassphrase!!)
} catch (e: Exception) {
logger.warn("解密 API Passphrase 失败: accountId=${account.id}, error=${e.message}")
continue
}
// 创建带认证的 CLOB API 客户端
val clobApi = retrofitFactory.createClobApi(
account.apiKey!!,
apiSecret,
apiPassphrase,
account.walletAddress
)
// 如果 orderId 不是 0x 开头,直接标记为已更新(不需要通过API查询)
if (!record.sellOrderId.startsWith("0x", ignoreCase = true)) {
logger.debug("卖出订单ID非0x开头,直接标记为已更新: orderId=${record.sellOrderId}")
val updatedRecord = SellMatchRecord(
id = record.id,
copyTradingId = record.copyTradingId,
sellOrderId = record.sellOrderId,
leaderSellTradeId = record.leaderSellTradeId,
marketId = record.marketId,
side = record.side,
outcomeIndex = record.outcomeIndex,
totalMatchedQuantity = record.totalMatchedQuantity,
sellPrice = record.sellPrice,
totalRealizedPnl = record.totalRealizedPnl,
priceUpdated = true, // 标记为已更新
createdAt = record.createdAt
)
sellMatchRecordRepository.save(updatedRecord)
continue
}
// 查询订单详情,获取实际成交价
val actualSellPrice = trackingService.getActualExecutionPrice(
orderId = record.sellOrderId,
clobApi = clobApi,
fallbackPrice = record.sellPrice
)
// 如果价格已更新(与当前价格不同),更新数据库
if (actualSellPrice != record.sellPrice) {
// 重新计算盈亏
val details = sellMatchDetailRepository.findByMatchRecordId(record.id!!)
var totalRealizedPnl = BigDecimal.ZERO
for (detail in details) {
val updatedRealizedPnl = actualSellPrice.subtract(detail.buyPrice).multi(detail.matchedQuantity)
// 更新明细的卖出价格和盈亏
// 注意:SellMatchDetail 的字段都是 val,需要创建新对象
val updatedDetail = SellMatchDetail(
id = detail.id,
matchRecordId = detail.matchRecordId,
trackingId = detail.trackingId,
buyOrderId = detail.buyOrderId,
matchedQuantity = detail.matchedQuantity,
buyPrice = detail.buyPrice,
sellPrice = actualSellPrice, // 更新卖出价格
realizedPnl = updatedRealizedPnl, // 更新盈亏
createdAt = detail.createdAt
)
sellMatchDetailRepository.save(updatedDetail)
totalRealizedPnl = totalRealizedPnl.add(updatedRealizedPnl)
}
// 更新卖出记录
// 注意:SellMatchRecord 的字段都是 val,需要创建新对象
val updatedRecord = SellMatchRecord(
id = record.id,
copyTradingId = record.copyTradingId,
sellOrderId = record.sellOrderId,
leaderSellTradeId = record.leaderSellTradeId,
marketId = record.marketId,
side = record.side,
outcomeIndex = record.outcomeIndex,
totalMatchedQuantity = record.totalMatchedQuantity,
sellPrice = actualSellPrice, // 更新卖出价格
totalRealizedPnl = totalRealizedPnl, // 更新总盈亏
priceUpdated = true, // 标记为已更新
createdAt = record.createdAt
)
sellMatchRecordRepository.save(updatedRecord)
logger.info("更新卖出订单价格成功: orderId=${record.sellOrderId}, 原价格=${record.sellPrice}, 新价格=$actualSellPrice")
} else {
// 价格相同,但可能已经查询过,标记为已更新
val updatedRecord = SellMatchRecord(
id = record.id,
copyTradingId = record.copyTradingId,
sellOrderId = record.sellOrderId,
leaderSellTradeId = record.leaderSellTradeId,
marketId = record.marketId,
side = record.side,
outcomeIndex = record.outcomeIndex,
totalMatchedQuantity = record.totalMatchedQuantity,
sellPrice = record.sellPrice,
totalRealizedPnl = record.totalRealizedPnl,
priceUpdated = true, // 标记为已更新
createdAt = record.createdAt
)
sellMatchRecordRepository.save(updatedRecord)
logger.debug("卖出订单价格无需更新: orderId=${record.sellOrderId}, price=$actualSellPrice")
}
} catch (e: Exception) {
logger.warn("更新卖出订单价格失败: orderId=${record.sellOrderId}, error=${e.message}", e)
// 继续处理下一条记录
}
}
} catch (e: Exception) {
logger.error("更新待更新卖出订单价格异常: ${e.message}", e)
}
}
}
@@ -0,0 +1,8 @@
-- 添加 price_updated 字段到 sell_match_record 表
-- 用于标记卖出价格是否已从订单详情中更新
ALTER TABLE sell_match_record
ADD COLUMN price_updated BOOLEAN DEFAULT FALSE COMMENT '价格是否已更新(从订单详情获取实际成交价)';
-- 为已存在的记录设置默认值
UPDATE sell_match_record SET price_updated = TRUE WHERE price_updated IS NULL;