优化市场价格服务:移除降级查询逻辑,仅保留链上和订单簿查询

- 移除 CLOB Trades、Gamma Market Status、Gamma Market Price 查询逻辑
- 只保留链上 RPC 查询(市场结算结果)和 CLOB 订单簿查询
- 如果所有数据源都失败,抛出 IllegalStateException 异常
- 价格截位到 4 位小数(向下截断,不四舍五入)
- 移除未使用的导入和方法(MarketResponse, JsonUtils, OutcomeResult 等)
- 所有调用方都已正确处理异常,无需额外修改
This commit is contained in:
WrBug
2026-01-02 00:14:42 +08:00
parent d96eb3e00a
commit f7f2411b9d
8 changed files with 250 additions and 245 deletions
@@ -4,8 +4,10 @@ import com.wrbug.polymarketbot.api.LatestPriceResponse
import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.accounts.AccountService
import com.wrbug.polymarketbot.service.common.MarketPriceService
import com.wrbug.polymarketbot.service.common.PolymarketClobService
import kotlinx.coroutines.runBlocking
import java.math.BigDecimal
import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource
import org.springframework.http.ResponseEntity
@@ -20,14 +22,16 @@ import org.springframework.web.bind.annotation.*
class MarketController(
private val accountService: AccountService,
private val clobService: PolymarketClobService,
private val marketPriceService: MarketPriceService,
private val messageSource: MessageSource
) {
private val logger = LoggerFactory.getLogger(MarketController::class.java)
/**
* 获取市场价格(通过 Gamma API
* 使用 Gamma API 获取价格信息,因为 Gamma API 支持 condition_ids 参数
* 获取市场价格
* 使用 MarketPriceService 获取当前市场价格(支持多数据源降级)
* 返回当前价格,前端接收后自行填充到 bestBid 字段
*/
@PostMapping("/price")
fun getMarketPrice(@RequestBody request: MarketPriceRequest): ResponseEntity<ApiResponse<MarketPriceResponse>> {
@@ -36,16 +40,16 @@ class MarketController(
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY, messageSource = messageSource))
}
val result = runBlocking { accountService.getMarketPrice(request.marketId, request.outcomeIndex) }
result.fold(
onSuccess = { response ->
ResponseEntity.ok(ApiResponse.success(response))
},
onFailure = { e ->
logger.error("获取市场价格失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_MARKET_PRICE_FETCH_FAILED, e.message, messageSource))
}
val outcomeIndex = request.outcomeIndex ?: 0
val price = runBlocking {
marketPriceService.getCurrentMarketPrice(request.marketId, outcomeIndex)
}
val response = MarketPriceResponse(
marketId = request.marketId,
currentPrice = price.toString()
)
ResponseEntity.ok(ApiResponse.success(response))
} catch (e: Exception) {
logger.error("获取市场价格异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_MARKET_PRICE_FETCH_FAILED, e.message, messageSource))
@@ -195,14 +195,11 @@ data class LatestPriceRequest(
)
/**
* 市场价格响应
* 市场当前价格响应
*/
data class MarketPriceResponse(
val marketId: String,
val lastPrice: String?, // 最新成交价
val bestBid: String?, // 最优买价(用于卖出参考)
val bestAsk: String?, // 最优卖价(用于买入参考)
val midpoint: String? // 中间价
val currentPrice: String // 当前价格(通过 MarketPriceService 获取,支持多数据源降级)
)
/**
@@ -1154,13 +1154,13 @@ class AccountService(
null
}
// 优先使用 lastPrice(最近成交价),如果没有则使用 bestBid,最后使用 midpoint
val currentPrice = lastPrice ?: bestBid ?: midpoint ?: "0"
Result.success(
MarketPriceResponse(
marketId = marketId,
lastPrice = lastPrice,
bestBid = bestBid,
bestAsk = bestAsk,
midpoint = midpoint
currentPrice = currentPrice
)
)
} else {
@@ -21,12 +21,9 @@ import org.springframework.context.i18n.LocaleContextHolder
import com.wrbug.polymarketbot.service.system.SystemConfigService
import com.wrbug.polymarketbot.service.system.RelayClientService
import com.wrbug.polymarketbot.service.system.TelegramNotificationService
import com.wrbug.polymarketbot.util.RetrofitFactory
import com.wrbug.polymarketbot.util.JsonUtils
import com.wrbug.polymarketbot.service.common.BlockchainService
import com.wrbug.polymarketbot.service.common.MarketPriceService
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.math.BigInteger
import java.util.concurrent.ConcurrentHashMap
/**
@@ -47,9 +44,7 @@ class PositionCheckService(
private val telegramNotificationService: TelegramNotificationService?,
private val accountRepository: AccountRepository,
private val messageSource: MessageSource,
private val retrofitFactory: RetrofitFactory,
private val blockchainService: BlockchainService,
private val jsonUtils: JsonUtils
private val marketPriceService: MarketPriceService
) {
private val logger = LoggerFactory.getLogger(PositionCheckService::class.java)
@@ -431,200 +426,10 @@ class PositionCheckService(
/**
* 获取当前市场最新价(用于更新订单卖出价)
* 优先使用链上查询获取市场结算结果,如果未结算则使用 API 查询
* 如果市场已关闭:
* - 该 outcome 赢了,返回 1
* - 该 outcome 输了,返回 0
* 委托给 MarketPriceService 处理
*/
private suspend fun getCurrentMarketPrice(marketId: String, outcomeIndex: Int): BigDecimal {
return try {
// 优先从链上查询市场结算结果(实时性高)
val chainResult = blockchainService.getCondition(marketId)
chainResult.fold(
onSuccess = { (payoutDenominator, payouts) ->
// 如果 payouts 不为空,说明市场已结算
if (payouts.isNotEmpty() && outcomeIndex < payouts.size) {
val payout = payouts[outcomeIndex]
when {
payout > BigInteger.ZERO -> {
// payout > 0 表示赢了
logger.info("从链上查询到市场已结算,该 outcome 赢了: marketId=$marketId, outcomeIndex=$outcomeIndex, payout=$payout")
return BigDecimal.ONE
}
payout == BigInteger.ZERO -> {
// payout == 0 表示输了
logger.info("从链上查询到市场已结算,该 outcome 输了: marketId=$marketId, outcomeIndex=$outcomeIndex, payout=$payout")
return BigDecimal.ZERO
}
else -> {
logger.warn("从链上查询到异常的 payout 值: marketId=$marketId, outcomeIndex=$outcomeIndex, payout=$payout")
}
}
} else {
logger.debug("从链上查询到市场尚未结算: marketId=$marketId, payouts=${payouts.size}")
}
},
onFailure = { e ->
logger.debug("链上查询市场条件失败,降级到 API 查询: marketId=$marketId, error=${e.message}")
}
)
// 链上查询失败或市场未结算,降级到 API 查询
val gammaApi = retrofitFactory.createGammaApi()
val marketResponse = gammaApi.listMarkets(conditionIds = listOf(marketId))
if (marketResponse.isSuccessful && marketResponse.body() != null) {
val markets = marketResponse.body()!!
val market = markets.firstOrNull()
if (market != null) {
// 检查市场是否已结束:1) closed == true 或 2) endDate 已过
val isMarketEnded = checkIfMarketEnded(market)
if (isMarketEnded) {
logger.debug("市场已结束: marketId=$marketId, closed=${market.closed}, endDate=${market.endDate}")
// 市场已结束,检查该 outcome 是赢了还是输了
val outcomeResult = checkOutcomeResult(market, outcomeIndex)
when (outcomeResult) {
OutcomeResult.WON -> {
logger.info("市场已结束且该 outcome 赢了,返回价格为 1: marketId=$marketId, outcomeIndex=$outcomeIndex")
return BigDecimal.ONE
}
OutcomeResult.LOST -> {
logger.info("市场已结束且该 outcome 输了,返回价格为 0: marketId=$marketId, outcomeIndex=$outcomeIndex")
return BigDecimal.ZERO
}
OutcomeResult.UNKNOWN -> {
// 无法判断,记录警告并继续使用正常价格逻辑
logger.warn("市场已结束但无法判断 outcome 结果,使用正常价格: marketId=$marketId, outcomeIndex=$outcomeIndex, closed=${market.closed}, endDate=${market.endDate}, outcomePrices=${market.outcomePrices}, bestBid=${market.bestBid}, bestAsk=${market.bestAsk}")
}
}
}
}
}
// 如果市场未关闭或无法判断输赢,获取正常价格
val priceResult = accountService.getMarketPrice(marketId, outcomeIndex)
val marketPrice = priceResult.getOrNull()
if (marketPrice != null) {
// 优先使用 bestBid(最优买价,用于卖出参考),如果没有则使用 midpoint
val priceStr = marketPrice.bestBid ?: marketPrice.midpoint ?: marketPrice.lastPrice
priceStr?.toSafeBigDecimal() ?: BigDecimal.ZERO
} else {
BigDecimal.ZERO
}
} catch (e: Exception) {
logger.error("获取市场最新价失败: marketId=$marketId, outcomeIndex=$outcomeIndex, error=${e.message}", e)
BigDecimal.ZERO
}
}
/**
* 检查市场是否已结束
* 判断条件:
* 1. closed == true
* 2. 或 endDate 已过(如果 endDate 不为空)
*/
private fun checkIfMarketEnded(market: com.wrbug.polymarketbot.api.MarketResponse): Boolean {
// 1. 检查 closed 字段
if (market.closed == true) {
return true
}
// 2. 检查 endDate 是否已过
val endDateStr = market.endDate
if (endDateStr != null && endDateStr.isNotBlank()) {
try {
// endDate 可能是 ISO 8601 格式字符串或时间戳
val endDate = if (endDateStr.matches(Regex("^\\d+$"))) {
// 时间戳(秒或毫秒)
val timestamp = endDateStr.toLong()
// 判断是秒还是毫秒(如果小于 10^10,认为是秒)
if (timestamp < 10000000000L) {
timestamp * 1000 // 转换为毫秒
} else {
timestamp
}
} else {
// ISO 8601 格式,尝试解析
java.time.Instant.parse(endDateStr).toEpochMilli()
}
val now = System.currentTimeMillis()
if (now >= endDate) {
logger.debug("市场 endDate 已过: marketId=${market.conditionId}, endDate=$endDateStr, now=$now")
return true
}
} catch (e: Exception) {
logger.warn("解析 endDate 失败: marketId=${market.conditionId}, endDate=$endDateStr, error=${e.message}")
}
}
return false
}
/**
* Outcome 结果枚举
*/
private enum class OutcomeResult {
WON, // 赢了
LOST, // 输了
UNKNOWN // 无法判断
}
/**
* 检查该 outcome 的结果(赢了、输了或无法判断)
* @param market 市场信息
* @param outcomeIndex outcome 索引
* @return OutcomeResult
*/
private fun checkOutcomeResult(market: com.wrbug.polymarketbot.api.MarketResponse, outcomeIndex: Int): OutcomeResult {
return try {
// 优先使用 outcomePrices(结算价格数组)
val outcomePrices = market.outcomePrices
if (outcomePrices != null && outcomePrices.isNotBlank()) {
val prices = jsonUtils.parseStringArray(outcomePrices)
if (outcomeIndex < prices.size) {
val price = prices[outcomeIndex].toSafeBigDecimal()
// 如果价格 >= 0.99,认为赢了
if (price >= BigDecimal("0.99")) {
return OutcomeResult.WON
}
// 如果价格 <= 0.01,认为输了
if (price <= BigDecimal("0.01")) {
return OutcomeResult.LOST
}
// 其他情况,无法判断
return OutcomeResult.UNKNOWN
}
}
// 如果没有 outcomePrices,使用 bestBid 和 bestAsk 判断
val bestBid = market.bestBid ?: 0.0
val bestAsk = market.bestAsk ?: 0.0
// 如果目标 outcome 不是第一个(index != 0),需要转换价格
val targetBid = if (outcomeIndex > 0) {
// 第二个 outcome 的 bestBid = 1 - 第一个 outcome 的 bestAsk
BigDecimal.ONE.subtract(BigDecimal.valueOf(bestAsk))
} else {
BigDecimal.valueOf(bestBid)
}
// 如果 bestBid >= 0.99,认为赢了
if (targetBid >= BigDecimal("0.99")) {
return OutcomeResult.WON
}
// 如果 bestBid <= 0.01,认为输了
if (targetBid <= BigDecimal("0.01")) {
return OutcomeResult.LOST
}
// 其他情况,无法判断
OutcomeResult.UNKNOWN
} catch (e: Exception) {
logger.warn("检查 outcome 结果失败: marketId=${market.conditionId}, outcomeIndex=$outcomeIndex, error=${e.message}", e)
OutcomeResult.UNKNOWN
}
return marketPriceService.getCurrentMarketPrice(marketId, outcomeIndex)
}
@@ -0,0 +1,211 @@
package com.wrbug.polymarketbot.service.common
import com.wrbug.polymarketbot.api.PolymarketClobApi
import com.wrbug.polymarketbot.repository.AccountRepository
import com.wrbug.polymarketbot.util.CryptoUtils
import com.wrbug.polymarketbot.util.RetrofitFactory
import com.wrbug.polymarketbot.util.toSafeBigDecimal
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.math.BigInteger
/**
* 市场价格服务
* 统一封装从不同数据源获取市场价格的逻辑
* 数据源包括:
* 1. 链上 RPC 查询(市场结算结果)
* 2. CLOB API(订单簿价格)
*/
@Service
class MarketPriceService(
private val blockchainService: BlockchainService,
private val retrofitFactory: RetrofitFactory,
private val accountRepository: AccountRepository,
private val cryptoUtils: CryptoUtils
) {
private val logger = LoggerFactory.getLogger(MarketPriceService::class.java)
/**
* 获取当前市场最新价
* 优先级:
* 1. 链上查询市场结算结果(如果已结算,返回 1.0 或 0.0)
* 2. CLOB API 查询订单簿价格(最准确,使用 bestBid)
*
* 价格会被截位到 4 位小数(向下截断,不四舍五入),用于显示和后续计算
*
* @param marketId 市场ID
* @param outcomeIndex 结果索引
* @return 市场价格(已截位到 4 位小数)
* @throws IllegalStateException 如果所有数据源都失败
*/
suspend fun getCurrentMarketPrice(marketId: String, outcomeIndex: Int): BigDecimal {
// 1. 优先从链上查询市场结算结果
val chainPrice = getPriceFromChainCondition(marketId, outcomeIndex)
if (chainPrice != null) {
// 截位到 4 位小数(向下截断,不四舍五入)
return chainPrice.setScale(4, java.math.RoundingMode.DOWN)
}
// 2. 从 CLOB API 查询订单簿价格(最准确)
val orderbookPrice = getPriceFromClobOrderbook(marketId, outcomeIndex)
if (orderbookPrice != null) {
// 截位到 4 位小数(向下截断,不四舍五入)
return orderbookPrice.setScale(4, java.math.RoundingMode.DOWN)
}
// 如果所有数据源都失败,抛出异常
val errorMsg = "无法获取市场价格: marketId=$marketId, outcomeIndex=$outcomeIndex (链上查询和订单簿查询均失败)"
logger.error(errorMsg)
throw IllegalStateException(errorMsg)
}
/**
* 从链上查询市场结算结果获取价格
* 如果市场已结算:
* - payout > 0(赢了)→ 返回 1.0
* - payout == 0(输了)→ 返回 0.0
* 如果市场未结算或查询失败,返回 null
*/
private suspend fun getPriceFromChainCondition(marketId: String, outcomeIndex: Int): BigDecimal? {
return try {
val chainResult = blockchainService.getCondition(marketId)
chainResult.fold(
onSuccess = { (_, payouts) ->
// 如果 payouts 不为空,说明市场已结算
if (payouts.isNotEmpty() && outcomeIndex < payouts.size) {
val payout = payouts[outcomeIndex]
when {
payout > BigInteger.ZERO -> {
logger.info("从链上查询到市场已结算,该 outcome 赢了: marketId=$marketId, outcomeIndex=$outcomeIndex, payout=$payout")
return BigDecimal.ONE
}
payout == BigInteger.ZERO -> {
logger.info("从链上查询到市场已结算,该 outcome 输了: marketId=$marketId, outcomeIndex=$outcomeIndex, payout=$payout")
return BigDecimal.ZERO
}
else -> {
logger.warn("从链上查询到异常的 payout 值: marketId=$marketId, outcomeIndex=$outcomeIndex, payout=$payout")
null
}
}
} else {
logger.debug("从链上查询到市场尚未结算: marketId=$marketId, payouts=${payouts.size}")
null
}
},
onFailure = { e ->
logger.debug("链上查询市场条件失败,降级到 API 查询: marketId=$marketId, error=${e.message}")
null
}
)
} catch (e: Exception) {
logger.debug("链上查询市场条件异常: marketId=$marketId, outcomeIndex=$outcomeIndex, error=${e.message}")
null
}
}
/**
* 从 CLOB API 查询订单簿价格
* 获取订单簿的 bestBid 和 bestAsk,计算 midpoint = (bestBid + bestAsk) / 2
* 订单簿数据最准确,反映当前市场真实价格
* 如果查询失败,返回 null
*/
private suspend fun getPriceFromClobOrderbook(marketId: String, outcomeIndex: Int): BigDecimal? {
return try {
// 获取 tokenId(用于查询特定 outcome 的订单簿)
val tokenIdResult = blockchainService.getTokenId(marketId, outcomeIndex)
if (!tokenIdResult.isSuccess) {
return null
}
val tokenId = tokenIdResult.getOrNull() ?: return null
// 尝试使用带鉴权的 CLOB API,如果没有则使用不带鉴权的 API
val clobApi = try {
getAuthenticatedClobApi() ?: retrofitFactory.createClobApiWithoutAuth()
} catch (e: Exception) {
logger.debug("获取带鉴权的 CLOB API 失败,使用不带鉴权的 API: ${e.message}")
retrofitFactory.createClobApiWithoutAuth()
}
val orderbookResponse = clobApi.getOrderbook(tokenId = tokenId, market = null)
if (!orderbookResponse.isSuccessful || orderbookResponse.body() == null) {
return null
}
val orderbook = orderbookResponse.body()!!
// 获取 bestBid(最高买入价):从 bids 中找到价格最大的
// bids 表示买入订单列表,价格越高表示愿意出的价格越高
val bestBid = orderbook.bids
.mapNotNull { it.price.toSafeBigDecimal() }
.maxOrNull()
// 获取 bestAsk(最低卖出价):从 asks 中找到价格最小的
// asks 表示卖出订单列表,价格越低表示愿意卖的价格越低
val bestAsk = orderbook.asks
.mapNotNull { it.price.toSafeBigDecimal() }
.minOrNull()
// 由于主要用于卖出场景,优先使用 bestBid(最高买入价,卖给愿意买入的人)
// 如果没有 bestBid,则使用 midpoint 或 bestAsk
if (bestBid != null) {
logger.debug("从订单簿获取价格(bestBid: marketId=$marketId, outcomeIndex=$outcomeIndex, bestBid=$bestBid, bestAsk=$bestAsk")
return bestBid
} else if (bestAsk != null && bestAsk > BigDecimal.ZERO) {
// 如果没有 bestBid,使用 bestAsk 作为备选
logger.debug("从订单簿获取价格(bestAsk: marketId=$marketId, outcomeIndex=$outcomeIndex, bestAsk=$bestAsk")
return bestAsk
}
null
} catch (e: Exception) {
logger.debug("CLOB API 查询订单簿失败: marketId=$marketId, outcomeIndex=$outcomeIndex, error=${e.message}")
null
}
}
/**
* 获取带鉴权的 CLOB API 客户端
* 使用第一个有 API 凭证的账户
* 如果都没有,返回 null
*/
private fun getAuthenticatedClobApi(): PolymarketClobApi? {
return try {
// 使用第一个有 API 凭证的账户
val account = accountRepository.findAllByOrderByCreatedAtAsc()
.firstOrNull { it.apiKey != null && it.apiSecret != null && it.apiPassphrase != null }
if (account == null || account.apiKey == null || account.apiSecret == null || account.apiPassphrase == null) {
return null
}
// 解密 API 凭证
val apiKey = account.apiKey
val apiSecret = try {
cryptoUtils.decrypt(account.apiSecret)
} catch (e: Exception) {
logger.debug("解密 API Secret 失败: ${e.message}")
return null
}
val apiPassphrase = try {
cryptoUtils.decrypt(account.apiPassphrase)
} catch (e: Exception) {
logger.debug("解密 API Passphrase 失败: ${e.message}")
return null
}
// 创建带鉴权的 CLOB API 客户端
retrofitFactory.createClobApi(apiKey, apiSecret, apiPassphrase, account.walletAddress)
} catch (e: Exception) {
logger.debug("获取带鉴权的 CLOB API 失败: ${e.message}")
null
}
}
}
@@ -341,9 +341,9 @@ class CopyTradingStatisticsService(
// 传递 outcomeIndex 参数,确保获取对应 outcome 的价格
val result = accountService.getMarketPrice(marketId, outcomeIndex)
result.onSuccess { response ->
// 使用中间价,如果没有则使用最后价格
val price = response.midpoint ?: response.lastPrice
if (price != null) {
// 使用当前价格
val price = response.currentPrice
if (price.isNotBlank() && price != "0") {
// 使用 "marketId:outcomeIndex" 作为 key
val key = "$marketId:$outcomeIndex"
prices[key] = price