feat: 实现仓位出售功能并修复价格接口

- 新增仓位出售功能,支持市价和限价卖出
- 添加卖出模态框,包含订单类型选择、数量快捷按钮、限价输入
- 实现实时平仓收益计算和显示
- 修复价格接口:改用Gamma API获取价格(支持condition_ids参数)
- 优化价格选择逻辑:卖出使用bestBid,买入使用bestAsk
- 添加提交订单的加载状态和错误处理
- 优化UI提示,明确标注价格用途(买入/卖出参考)
This commit is contained in:
WrBug
2025-11-28 04:11:13 +08:00
parent 5b458b9b0c
commit b4fb16af9e
8 changed files with 709 additions and 19 deletions
@@ -12,18 +12,25 @@ interface PolymarketClobApi {
/**
* 获取订单簿
* 注意:Polymarket CLOB API 的 /book 接口需要 token_id 参数
* 但我们先尝试使用 market 参数,如果不支持再修改
*/
@GET("/book")
suspend fun getOrderbook(
@Query("market") market: String
@Query("token_id") tokenId: String? = null,
@Query("market") market: String? = null
): Response<OrderbookResponse>
/**
* 获取价格信息
* 注意:Polymarket CLOB API 的 /price 接口需要 token_id 和 side 参数
* 但我们使用订单簿来获取价格,因为订单簿支持 market 参数
*/
@GET("/price")
suspend fun getPrice(
@Query("market") market: String
@Query("token_id") tokenId: String? = null,
@Query("side") side: String? = null,
@Query("market") market: String? = null
): Response<PriceResponse>
/**
@@ -229,5 +229,79 @@ class AccountController(
ResponseEntity.ok(ApiResponse.serverError("查询仓位列表失败: ${e.message}"))
}
}
/**
* 卖出仓位
*/
@PostMapping("/positions/sell")
fun sellPosition(@RequestBody request: PositionSellRequest): ResponseEntity<ApiResponse<PositionSellResponse>> {
return try {
// 参数验证
if (request.accountId <= 0) {
return ResponseEntity.ok(ApiResponse.paramError("账户ID无效"))
}
if (request.marketId.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("市场ID不能为空"))
}
if (request.side !in listOf("YES", "NO")) {
return ResponseEntity.ok(ApiResponse.paramError("方向必须是YES或NO"))
}
if (request.orderType !in listOf("MARKET", "LIMIT")) {
return ResponseEntity.ok(ApiResponse.paramError("订单类型必须是MARKET或LIMIT"))
}
if (request.quantity.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("卖出数量不能为空"))
}
if (request.orderType == "LIMIT" && (request.price == null || request.price.isBlank())) {
return ResponseEntity.ok(ApiResponse.paramError("限价订单必须提供价格"))
}
val result = runBlocking { accountService.sellPosition(request) }
result.fold(
onSuccess = { response ->
logger.info("成功创建卖出订单: 账户=${request.accountId}, 市场=${request.marketId}, 订单ID=${response.orderId}")
ResponseEntity.ok(ApiResponse.success(response))
},
onFailure = { e ->
logger.error("创建卖出订单失败: ${e.message}", e)
when (e) {
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.paramError(e.message ?: "参数错误"))
is IllegalStateException -> ResponseEntity.ok(ApiResponse.businessError(e.message ?: "业务逻辑错误"))
else -> ResponseEntity.ok(ApiResponse.serverError("创建卖出订单失败: ${e.message}"))
}
}
)
} catch (e: Exception) {
logger.error("创建卖出订单异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.serverError("创建卖出订单失败: ${e.message}"))
}
}
/**
* 获取市场价格
*/
@PostMapping("/markets/price")
fun getMarketPrice(@RequestBody request: MarketPriceRequest): ResponseEntity<ApiResponse<MarketPriceResponse>> {
return try {
if (request.marketId.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("市场ID不能为空"))
}
val result = runBlocking { accountService.getMarketPrice(request.marketId) }
result.fold(
onSuccess = { response ->
logger.info("成功获取市场价格: 市场=${request.marketId}")
ResponseEntity.ok(ApiResponse.success(response))
},
onFailure = { e ->
logger.error("获取市场价格失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.serverError("获取市场价格失败: ${e.message}"))
}
)
} catch (e: Exception) {
logger.error("获取市场价格异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.serverError("获取市场价格失败: ${e.message}"))
}
}
}
@@ -135,3 +135,47 @@ data class PositionListResponse(
val historyPositions: List<AccountPositionDto>
)
/**
* 仓位卖出请求
*/
data class PositionSellRequest(
val accountId: Long, // 账户ID(必需)
val marketId: String, // 市场ID(必需)
val side: String, // 方向:YES 或 NO(必需)
val orderType: String, // 订单类型:MARKET(市价)或 LIMIT(限价)(必需)
val quantity: String, // 卖出数量(必需,BigDecimal字符串)
val price: String? = null // 限价价格(限价订单必需,市价订单不需要)
)
/**
* 仓位卖出响应
*/
data class PositionSellResponse(
val orderId: String, // 订单ID
val marketId: String, // 市场ID
val side: String, // 方向
val orderType: String, // 订单类型
val quantity: String, // 订单数量
val price: String?, // 订单价格(限价订单)
val status: String, // 订单状态
val createdAt: Long // 创建时间戳
)
/**
* 市场价格请求
*/
data class MarketPriceRequest(
val marketId: String // 市场ID
)
/**
* 市场价格响应
*/
data class MarketPriceResponse(
val marketId: String,
val lastPrice: String?, // 最新成交价
val bestBid: String?, // 最优买价(用于卖出参考)
val bestAsk: String?, // 最优卖价(用于买入参考)
val midpoint: String? // 中间价
)
@@ -648,6 +648,165 @@ class AccountService(
}
}
/**
* 卖出仓位
*/
suspend fun sellPosition(request: PositionSellRequest): Result<PositionSellResponse> {
return try {
// 1. 验证账户是否存在且已配置API凭证
val account = accountRepository.findById(request.accountId).orElse(null)
?: return Result.failure(IllegalArgumentException("账户不存在"))
if (account.apiKey == null || account.apiSecret == null || account.apiPassphrase == null) {
return Result.failure(IllegalStateException("账户未配置API凭证,无法创建订单"))
}
// 2. 验证仓位是否存在且数量足够
val positionsResult = getAllPositions()
positionsResult.fold(
onSuccess = { positionListResponse ->
val position = positionListResponse.currentPositions.find {
it.accountId == request.accountId &&
it.marketId == request.marketId &&
it.side == request.side
}
if (position == null) {
return Result.failure(IllegalArgumentException("仓位不存在"))
}
val positionQuantity = position.quantity.toSafeBigDecimal()
val sellQuantity = request.quantity.toSafeBigDecimal()
if (sellQuantity <= BigDecimal.ZERO) {
return Result.failure(IllegalArgumentException("卖出数量必须大于0"))
}
if (sellQuantity > positionQuantity) {
return Result.failure(IllegalArgumentException("卖出数量不能超过持仓数量"))
}
},
onFailure = { e ->
return Result.failure(Exception("查询仓位失败: ${e.message}"))
}
)
// 3. 确定卖出价格
val sellPrice = if (request.orderType == "MARKET") {
// 市价订单:获取当前最优买价
val priceResult = clobService.getPrice(request.marketId)
priceResult.fold(
onSuccess = { priceResponse ->
priceResponse.bestBid ?: priceResponse.lastPrice
?: return Result.failure(IllegalStateException("无法获取市场价格,请稍后重试"))
},
onFailure = { e ->
return Result.failure(Exception("获取市场价格失败: ${e.message}"))
}
)
} else {
// 限价订单:使用用户输入的价格
request.price ?: return Result.failure(IllegalArgumentException("限价订单必须提供价格"))
}
// 4. 验证价格
val priceDecimal = sellPrice.toSafeBigDecimal()
if (priceDecimal <= BigDecimal.ZERO) {
return Result.failure(IllegalArgumentException("价格必须大于0"))
}
// 5. 创建订单请求
val orderRequest = com.wrbug.polymarketbot.api.CreateOrderRequest(
market = request.marketId,
side = "SELL", // 卖出订单
price = sellPrice,
size = request.quantity,
type = if (request.orderType == "MARKET") "MARKET" else "LIMIT"
)
// 6. 使用账户的API凭证创建订单
val clobApi = retrofitFactory.createClobApi(
account.apiKey!!,
account.apiSecret!!,
account.apiPassphrase!!,
account.walletAddress
)
val orderResponse = clobApi.createOrder(orderRequest)
if (orderResponse.isSuccessful && orderResponse.body() != null) {
val order = orderResponse.body()!!
Result.success(
PositionSellResponse(
orderId = order.id,
marketId = request.marketId,
side = request.side,
orderType = request.orderType,
quantity = request.quantity,
price = if (request.orderType == "LIMIT") sellPrice else null,
status = order.status,
createdAt = System.currentTimeMillis()
)
)
} else {
Result.failure(Exception("创建订单失败: ${orderResponse.code()} ${orderResponse.message()}"))
}
} catch (e: Exception) {
logger.error("卖出仓位异常: ${e.message}", e)
Result.failure(e)
}
}
/**
* 获取市场价格
* 使用 Gamma API 获取价格信息,因为 Gamma API 支持 condition_ids 参数
*/
suspend fun getMarketPrice(marketId: String): Result<MarketPriceResponse> {
return try {
// 使用 Gamma API 获取市场信息(支持 condition_ids 参数)
val gammaApi = retrofitFactory.createGammaApi()
val response = gammaApi.listMarkets(conditionIds = listOf(marketId))
if (response.isSuccessful && response.body() != null) {
val markets = response.body()!!
val market = markets.firstOrNull()
if (market != null) {
// 从 Gamma API 响应中提取价格信息
val bestBid = market.bestBid?.toString()
val bestAsk = market.bestAsk?.toString()
val lastPrice = market.lastTradePrice?.toString()
// 计算中间价 = (bestBid + bestAsk) / 2
val midpoint = if (bestBid != null && bestAsk != null) {
val bid = bestBid.toSafeBigDecimal()
val ask = bestAsk.toSafeBigDecimal()
bid.add(ask).divide(BigDecimal("2"), 8, java.math.RoundingMode.HALF_UP).toString()
} else {
null
}
Result.success(
MarketPriceResponse(
marketId = marketId,
lastPrice = lastPrice,
bestBid = bestBid,
bestAsk = bestAsk,
midpoint = midpoint
)
)
} else {
Result.failure(Exception("未找到市场信息: $marketId"))
}
} else {
Result.failure(Exception("获取市场价格失败: ${response.code()} ${response.message()}"))
}
} catch (e: Exception) {
logger.error("获取市场价格异常: ${e.message}", e)
Result.failure(e)
}
}
/**
* 检查账户是否有活跃订单
* 使用账户的 API Key 查询该账户的活跃订单
@@ -19,10 +19,11 @@ class PolymarketClobService(
/**
* 获取订单簿
* 使用 market 参数(condition ID
*/
suspend fun getOrderbook(market: String): Result<OrderbookResponse> {
return try {
val response = clobApi.getOrderbook(market)
val response = clobApi.getOrderbook(tokenId = null, market = market)
if (response.isSuccessful && response.body() != null) {
Result.success(response.body()!!)
} else {