feat: 实现仓位出售功能并修复价格接口
- 新增仓位出售功能,支持市价和限价卖出 - 添加卖出模态框,包含订单类型选择、数量快捷按钮、限价输入 - 实现实时平仓收益计算和显示 - 修复价格接口:改用Gamma API获取价格(支持condition_ids参数) - 优化价格选择逻辑:卖出使用bestBid,买入使用bestAsk - 添加提交订单的加载状态和错误处理 - 优化UI提示,明确标注价格用途(买入/卖出参考)
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user