优化市场价格服务:移除降级查询逻辑,仅保留链上和订单簿查询
- 移除 CLOB Trades、Gamma Market Status、Gamma Market Price 查询逻辑 - 只保留链上 RPC 查询(市场结算结果)和 CLOB 订单簿查询 - 如果所有数据源都失败,抛出 IllegalStateException 异常 - 价格截位到 4 位小数(向下截断,不四舍五入) - 移除未使用的导入和方法(MarketResponse, JsonUtils, OutcomeResult 等) - 所有调用方都已正确处理异常,无需额外修改
This commit is contained in:
+15
-11
@@ -4,8 +4,10 @@ import com.wrbug.polymarketbot.api.LatestPriceResponse
|
|||||||
import com.wrbug.polymarketbot.dto.*
|
import com.wrbug.polymarketbot.dto.*
|
||||||
import com.wrbug.polymarketbot.enums.ErrorCode
|
import com.wrbug.polymarketbot.enums.ErrorCode
|
||||||
import com.wrbug.polymarketbot.service.accounts.AccountService
|
import com.wrbug.polymarketbot.service.accounts.AccountService
|
||||||
|
import com.wrbug.polymarketbot.service.common.MarketPriceService
|
||||||
import com.wrbug.polymarketbot.service.common.PolymarketClobService
|
import com.wrbug.polymarketbot.service.common.PolymarketClobService
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.math.BigDecimal
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.MessageSource
|
import org.springframework.context.MessageSource
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
@@ -20,14 +22,16 @@ import org.springframework.web.bind.annotation.*
|
|||||||
class MarketController(
|
class MarketController(
|
||||||
private val accountService: AccountService,
|
private val accountService: AccountService,
|
||||||
private val clobService: PolymarketClobService,
|
private val clobService: PolymarketClobService,
|
||||||
|
private val marketPriceService: MarketPriceService,
|
||||||
private val messageSource: MessageSource
|
private val messageSource: MessageSource
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(MarketController::class.java)
|
private val logger = LoggerFactory.getLogger(MarketController::class.java)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取市场价格(通过 Gamma API)
|
* 获取市场价格
|
||||||
* 使用 Gamma API 获取价格信息,因为 Gamma API 支持 condition_ids 参数
|
* 使用 MarketPriceService 获取当前市场价格(支持多数据源降级)
|
||||||
|
* 返回当前价格,前端接收后自行填充到 bestBid 字段
|
||||||
*/
|
*/
|
||||||
@PostMapping("/price")
|
@PostMapping("/price")
|
||||||
fun getMarketPrice(@RequestBody request: MarketPriceRequest): ResponseEntity<ApiResponse<MarketPriceResponse>> {
|
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))
|
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY, messageSource = messageSource))
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = runBlocking { accountService.getMarketPrice(request.marketId, request.outcomeIndex) }
|
val outcomeIndex = request.outcomeIndex ?: 0
|
||||||
result.fold(
|
val price = runBlocking {
|
||||||
onSuccess = { response ->
|
marketPriceService.getCurrentMarketPrice(request.marketId, outcomeIndex)
|
||||||
ResponseEntity.ok(ApiResponse.success(response))
|
}
|
||||||
},
|
|
||||||
onFailure = { e ->
|
val response = MarketPriceResponse(
|
||||||
logger.error("获取市场价格失败: ${e.message}", e)
|
marketId = request.marketId,
|
||||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_MARKET_PRICE_FETCH_FAILED, e.message, messageSource))
|
currentPrice = price.toString()
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
ResponseEntity.ok(ApiResponse.success(response))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error("获取市场价格异常: ${e.message}", e)
|
logger.error("获取市场价格异常: ${e.message}", e)
|
||||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_MARKET_PRICE_FETCH_FAILED, e.message, messageSource))
|
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_MARKET_PRICE_FETCH_FAILED, e.message, messageSource))
|
||||||
|
|||||||
@@ -195,14 +195,11 @@ data class LatestPriceRequest(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 市场价格响应
|
* 市场当前价格响应
|
||||||
*/
|
*/
|
||||||
data class MarketPriceResponse(
|
data class MarketPriceResponse(
|
||||||
val marketId: String,
|
val marketId: String,
|
||||||
val lastPrice: String?, // 最新成交价
|
val currentPrice: String // 当前价格(通过 MarketPriceService 获取,支持多数据源降级)
|
||||||
val bestBid: String?, // 最优买价(用于卖出参考)
|
|
||||||
val bestAsk: String?, // 最优卖价(用于买入参考)
|
|
||||||
val midpoint: String? // 中间价
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1154,13 +1154,13 @@ class AccountService(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 优先使用 lastPrice(最近成交价),如果没有则使用 bestBid,最后使用 midpoint
|
||||||
|
val currentPrice = lastPrice ?: bestBid ?: midpoint ?: "0"
|
||||||
|
|
||||||
Result.success(
|
Result.success(
|
||||||
MarketPriceResponse(
|
MarketPriceResponse(
|
||||||
marketId = marketId,
|
marketId = marketId,
|
||||||
lastPrice = lastPrice,
|
currentPrice = currentPrice
|
||||||
bestBid = bestBid,
|
|
||||||
bestAsk = bestAsk,
|
|
||||||
midpoint = midpoint
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+4
-199
@@ -21,12 +21,9 @@ import org.springframework.context.i18n.LocaleContextHolder
|
|||||||
import com.wrbug.polymarketbot.service.system.SystemConfigService
|
import com.wrbug.polymarketbot.service.system.SystemConfigService
|
||||||
import com.wrbug.polymarketbot.service.system.RelayClientService
|
import com.wrbug.polymarketbot.service.system.RelayClientService
|
||||||
import com.wrbug.polymarketbot.service.system.TelegramNotificationService
|
import com.wrbug.polymarketbot.service.system.TelegramNotificationService
|
||||||
import com.wrbug.polymarketbot.util.RetrofitFactory
|
import com.wrbug.polymarketbot.service.common.MarketPriceService
|
||||||
import com.wrbug.polymarketbot.util.JsonUtils
|
|
||||||
import com.wrbug.polymarketbot.service.common.BlockchainService
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.math.BigInteger
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,9 +44,7 @@ class PositionCheckService(
|
|||||||
private val telegramNotificationService: TelegramNotificationService?,
|
private val telegramNotificationService: TelegramNotificationService?,
|
||||||
private val accountRepository: AccountRepository,
|
private val accountRepository: AccountRepository,
|
||||||
private val messageSource: MessageSource,
|
private val messageSource: MessageSource,
|
||||||
private val retrofitFactory: RetrofitFactory,
|
private val marketPriceService: MarketPriceService
|
||||||
private val blockchainService: BlockchainService,
|
|
||||||
private val jsonUtils: JsonUtils
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(PositionCheckService::class.java)
|
private val logger = LoggerFactory.getLogger(PositionCheckService::class.java)
|
||||||
@@ -431,200 +426,10 @@ class PositionCheckService(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前市场最新价(用于更新订单卖出价)
|
* 获取当前市场最新价(用于更新订单卖出价)
|
||||||
* 优先使用链上查询获取市场结算结果,如果未结算则使用 API 查询
|
* 委托给 MarketPriceService 处理
|
||||||
* 如果市场已关闭:
|
|
||||||
* - 该 outcome 赢了,返回 1
|
|
||||||
* - 该 outcome 输了,返回 0
|
|
||||||
*/
|
*/
|
||||||
private suspend fun getCurrentMarketPrice(marketId: String, outcomeIndex: Int): BigDecimal {
|
private suspend fun getCurrentMarketPrice(marketId: String, outcomeIndex: Int): BigDecimal {
|
||||||
return try {
|
return marketPriceService.getCurrentMarketPrice(marketId, outcomeIndex)
|
||||||
// 优先从链上查询市场结算结果(实时性高)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
+3
-3
@@ -341,9 +341,9 @@ class CopyTradingStatisticsService(
|
|||||||
// 传递 outcomeIndex 参数,确保获取对应 outcome 的价格
|
// 传递 outcomeIndex 参数,确保获取对应 outcome 的价格
|
||||||
val result = accountService.getMarketPrice(marketId, outcomeIndex)
|
val result = accountService.getMarketPrice(marketId, outcomeIndex)
|
||||||
result.onSuccess { response ->
|
result.onSuccess { response ->
|
||||||
// 使用中间价,如果没有则使用最后价格
|
// 使用当前价格
|
||||||
val price = response.midpoint ?: response.lastPrice
|
val price = response.currentPrice
|
||||||
if (price != null) {
|
if (price.isNotBlank() && price != "0") {
|
||||||
// 使用 "marketId:outcomeIndex" 作为 key
|
// 使用 "marketId:outcomeIndex" 作为 key
|
||||||
val key = "$marketId:$outcomeIndex"
|
val key = "$marketId:$outcomeIndex"
|
||||||
prices[key] = price
|
prices[key] = price
|
||||||
|
|||||||
@@ -399,10 +399,10 @@ const PositionList: React.FC = () => {
|
|||||||
})
|
})
|
||||||
if (response.data.code === 0 && response.data.data) {
|
if (response.data.code === 0 && response.data.data) {
|
||||||
setMarketPrice(response.data.data)
|
setMarketPrice(response.data.data)
|
||||||
// 默认使用最优买价作为限价
|
// 默认使用当前价格作为限价
|
||||||
if (response.data.data.bestBid) {
|
if (response.data.data.currentPrice) {
|
||||||
setLimitPrice(response.data.data.bestBid)
|
setLimitPrice(response.data.data.currentPrice)
|
||||||
form.setFieldsValue({ limitPrice: response.data.data.bestBid })
|
form.setFieldsValue({ limitPrice: response.data.data.currentPrice })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -449,12 +449,10 @@ const PositionList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前卖出价格(市价或限价)
|
// 获取当前卖出价格(市价或限价)
|
||||||
// 卖出操作应该使用 bestBid(最优买价),因为你要卖给愿意买入的人
|
|
||||||
const getCurrentSellPrice = (): string => {
|
const getCurrentSellPrice = (): string => {
|
||||||
if (orderType === 'MARKET') {
|
if (orderType === 'MARKET') {
|
||||||
// 市价订单(卖出):优先使用最优买价(bestBid),因为卖出是卖给买单
|
// 市价订单(卖出):使用当前价格
|
||||||
// 如果没有 bestBid,则使用当前价格,最后使用最新成交价
|
return marketPrice?.currentPrice || selectedPosition?.currentPrice || '0'
|
||||||
return marketPrice?.bestBid || selectedPosition?.currentPrice || marketPrice?.lastPrice || '0'
|
|
||||||
}
|
}
|
||||||
return limitPrice || '0'
|
return limitPrice || '0'
|
||||||
}
|
}
|
||||||
@@ -1377,7 +1375,7 @@ const PositionList: React.FC = () => {
|
|||||||
// 切换订单类型时重新计算收益
|
// 切换订单类型时重新计算收益
|
||||||
if (sellQuantity) {
|
if (sellQuantity) {
|
||||||
const price = e.target.value === 'MARKET'
|
const price = e.target.value === 'MARKET'
|
||||||
? (marketPrice?.bestBid || selectedPosition?.currentPrice || marketPrice?.lastPrice || '0')
|
? (marketPrice?.currentPrice || selectedPosition?.currentPrice || '0')
|
||||||
: limitPrice || '0'
|
: limitPrice || '0'
|
||||||
calculatePnl(sellQuantity, price)
|
calculatePnl(sellQuantity, price)
|
||||||
}
|
}
|
||||||
@@ -1469,21 +1467,14 @@ const PositionList: React.FC = () => {
|
|||||||
<div style={{ marginBottom: '16px', padding: '12px', background: '#f0f7ff', borderRadius: '8px' }}>
|
<div style={{ marginBottom: '16px', padding: '12px', background: '#f0f7ff', borderRadius: '8px' }}>
|
||||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>市价参考(卖出)</div>
|
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>市价参考(卖出)</div>
|
||||||
<div style={{ fontSize: '14px' }}>
|
<div style={{ fontSize: '14px' }}>
|
||||||
{marketPrice?.bestBid ? (
|
{marketPrice?.currentPrice ? (
|
||||||
<>最优买价(卖出参考): <span style={{ fontWeight: '500' }}>{formatNumber(marketPrice.bestBid, 4)}</span></>
|
<>当前价格: <span style={{ fontWeight: '500' }}>{formatNumber(marketPrice.currentPrice, 4)}</span></>
|
||||||
) : selectedPosition?.currentPrice ? (
|
) : selectedPosition?.currentPrice ? (
|
||||||
<>当前价格: <span style={{ fontWeight: '500' }}>{formatNumber(selectedPosition.currentPrice, 4)}</span></>
|
<>当前价格: <span style={{ fontWeight: '500' }}>{formatNumber(selectedPosition.currentPrice, 4)}</span></>
|
||||||
) : marketPrice?.lastPrice ? (
|
|
||||||
<>最新成交价: <span style={{ fontWeight: '500' }}>{formatNumber(marketPrice.lastPrice, 4)}</span></>
|
|
||||||
) : (
|
) : (
|
||||||
<span style={{ color: '#999' }}>暂无价格数据</span>
|
<span style={{ color: '#999' }}>暂无价格数据</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{marketPrice?.bestAsk && (
|
|
||||||
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
|
|
||||||
最优卖价(买入参考): {formatNumber(marketPrice.bestAsk, 4)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -434,14 +434,11 @@ export interface MarketPriceRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 市场价格响应
|
* 市场当前价格响应
|
||||||
*/
|
*/
|
||||||
export interface MarketPriceResponse {
|
export interface MarketPriceResponse {
|
||||||
marketId: string
|
marketId: string
|
||||||
lastPrice?: string
|
currentPrice: string
|
||||||
bestBid?: string
|
|
||||||
bestAsk?: string
|
|
||||||
midpoint?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user