feat: 优化仓位管理页面功能和样式
- 添加卡片和列表视图切换功能 - 移动端支持卡片折叠/展开,默认只显示关键信息 - 历史仓位不显示盈亏和平仓收益 - 优化账户筛选:移除搜索功能,按账户名称排序 - 移动端强制使用卡片视图,隐藏视图切换按钮 - 优化侧边栏布局:固定高度,内容区域可滚动 - 优化当前仓位/历史仓位切换组件样式 - 修复账户筛选全部账户选项不生效的问题
This commit is contained in:
@@ -2,6 +2,7 @@ package com.wrbug.polymarketbot.controller
|
||||
|
||||
import com.wrbug.polymarketbot.dto.*
|
||||
import com.wrbug.polymarketbot.service.AccountService
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
@@ -204,5 +205,29 @@ class AccountController(
|
||||
ResponseEntity.ok(ApiResponse.serverError("设置默认账户失败: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有账户的仓位列表
|
||||
*/
|
||||
@PostMapping("/positions/list")
|
||||
fun getAllPositions(): ResponseEntity<ApiResponse<PositionListResponse>> {
|
||||
return try {
|
||||
val result = runBlocking { accountService.getAllPositions() }
|
||||
result.fold(
|
||||
onSuccess = { positionListResponse ->
|
||||
val total = positionListResponse.currentPositions.size + positionListResponse.historyPositions.size
|
||||
logger.info("成功查询仓位列表: 当前仓位 ${positionListResponse.currentPositions.size} 个,历史仓位 ${positionListResponse.historyPositions.size} 个,共 $total 个")
|
||||
ResponseEntity.ok(ApiResponse.success(positionListResponse))
|
||||
},
|
||||
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}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,3 +96,39 @@ data class PositionDto(
|
||||
val pnl: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* 账户仓位信息(用于仓位管理页面)
|
||||
*/
|
||||
data class AccountPositionDto(
|
||||
val accountId: Long,
|
||||
val accountName: String?,
|
||||
val walletAddress: String,
|
||||
val proxyAddress: String,
|
||||
val marketId: String,
|
||||
val marketTitle: String?,
|
||||
val marketSlug: String?,
|
||||
val marketIcon: String?, // 市场图标 URL
|
||||
val side: String, // YES 或 NO
|
||||
val quantity: String,
|
||||
val avgPrice: String,
|
||||
val currentPrice: String,
|
||||
val currentValue: String,
|
||||
val initialValue: String,
|
||||
val pnl: String,
|
||||
val percentPnl: String,
|
||||
val realizedPnl: String?,
|
||||
val percentRealizedPnl: String?,
|
||||
val redeemable: Boolean,
|
||||
val mergeable: Boolean,
|
||||
val endDate: String?,
|
||||
val isCurrent: Boolean = true // true: 当前仓位(有持仓),false: 历史仓位(已平仓)
|
||||
)
|
||||
|
||||
/**
|
||||
* 仓位列表响应
|
||||
*/
|
||||
data class PositionListResponse(
|
||||
val currentPositions: List<AccountPositionDto>,
|
||||
val historyPositions: List<AccountPositionDto>
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.wrbug.polymarketbot.entity.Account
|
||||
import com.wrbug.polymarketbot.repository.AccountRepository
|
||||
import com.wrbug.polymarketbot.util.RetrofitFactory
|
||||
import com.wrbug.polymarketbot.util.toSafeBigDecimal
|
||||
import com.wrbug.polymarketbot.util.eq
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
@@ -549,6 +550,84 @@ class AccountService(
|
||||
return cleanKey.length == 64 && cleanKey.matches(Regex("^[0-9a-fA-F]{64}$"))
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有账户的仓位列表
|
||||
* 返回所有账户的仓位信息,包括账户信息
|
||||
*/
|
||||
suspend fun getAllPositions(): Result<PositionListResponse> {
|
||||
return try {
|
||||
val accounts = accountRepository.findAll()
|
||||
val currentPositions = mutableListOf<AccountPositionDto>()
|
||||
val historyPositions = mutableListOf<AccountPositionDto>()
|
||||
|
||||
// 遍历所有账户,查询每个账户的仓位
|
||||
accounts.forEach { account ->
|
||||
if (account.proxyAddress.isNotBlank()) {
|
||||
try {
|
||||
// 查询所有仓位(不限制 sortBy,获取当前和历史仓位)
|
||||
val positionsResult = blockchainService.getPositions(account.proxyAddress, sortBy = null)
|
||||
if (positionsResult.isSuccess) {
|
||||
val positions = positionsResult.getOrNull() ?: emptyList()
|
||||
// 遍历所有仓位,区分当前仓位和历史仓位
|
||||
positions.forEach { pos ->
|
||||
val currentValue = pos.currentValue?.toSafeBigDecimal() ?: BigDecimal.ZERO
|
||||
val curPrice = pos.curPrice?.toSafeBigDecimal() ?: BigDecimal.ZERO
|
||||
|
||||
// 判断是否为当前仓位:currentValue != 0 且 curPrice != 0
|
||||
// 使用 eq 方法判断值是否等于 0
|
||||
val isCurrent = !currentValue.eq(BigDecimal.ZERO) && !curPrice.eq(BigDecimal.ZERO)
|
||||
|
||||
val positionDto = AccountPositionDto(
|
||||
accountId = account.id!!,
|
||||
accountName = account.accountName,
|
||||
walletAddress = account.walletAddress,
|
||||
proxyAddress = account.proxyAddress,
|
||||
marketId = pos.conditionId ?: "",
|
||||
marketTitle = pos.title ?: "",
|
||||
marketSlug = pos.slug ?: "",
|
||||
marketIcon = pos.icon, // 市场图标
|
||||
side = pos.outcome ?: "",
|
||||
quantity = pos.size?.toString() ?: "0",
|
||||
avgPrice = pos.avgPrice?.toString() ?: "0",
|
||||
currentPrice = pos.curPrice?.toString() ?: "0",
|
||||
currentValue = pos.currentValue?.toString() ?: "0",
|
||||
initialValue = pos.initialValue?.toString() ?: "0",
|
||||
pnl = pos.cashPnl?.toString() ?: "0",
|
||||
percentPnl = pos.percentPnl?.toString() ?: "0",
|
||||
realizedPnl = pos.realizedPnl?.toString(),
|
||||
percentRealizedPnl = pos.percentRealizedPnl?.toString(),
|
||||
redeemable = pos.redeemable ?: false,
|
||||
mergeable = pos.mergeable ?: false,
|
||||
endDate = pos.endDate,
|
||||
isCurrent = isCurrent // 标识是当前仓位还是历史仓位
|
||||
)
|
||||
|
||||
// 根据 isCurrent 分别添加到对应的列表
|
||||
if (isCurrent) {
|
||||
currentPositions.add(positionDto)
|
||||
} else {
|
||||
historyPositions.add(positionDto)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("查询账户 ${account.id} 仓位失败: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按照接口返回的顺序返回,不进行排序
|
||||
// 前端负责本地排序
|
||||
Result.success(PositionListResponse(
|
||||
currentPositions = currentPositions,
|
||||
historyPositions = historyPositions
|
||||
))
|
||||
} catch (e: Exception) {
|
||||
logger.error("查询所有仓位失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账户是否有活跃订单
|
||||
* 使用账户的 API Key 查询该账户的活跃订单
|
||||
|
||||
@@ -216,13 +216,15 @@ class BlockchainService(
|
||||
* 通过 Polymarket Data API 查询
|
||||
* 文档: https://docs.polymarket.com/api-reference/core/get-current-positions-for-a-user
|
||||
*/
|
||||
suspend fun getPositions(proxyWalletAddress: String): Result<List<PositionResponse>> {
|
||||
suspend fun getPositions(proxyWalletAddress: String, sortBy: String? = "CURRENT"): Result<List<PositionResponse>> {
|
||||
return try {
|
||||
// 使用代理钱包地址查询仓位
|
||||
// sortBy=CURRENT 表示只返回当前仓位
|
||||
val response = dataApi.getPositions(
|
||||
user = proxyWalletAddress,
|
||||
limit = 500, // 最大限制
|
||||
offset = 0
|
||||
offset = 0,
|
||||
sortBy = sortBy
|
||||
)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
|
||||
@@ -61,6 +61,7 @@ fun BigInteger.multi(value: Any): BigDecimal {
|
||||
/**
|
||||
* 大于比较扩展函数
|
||||
* 安全地比较两个任意类型的数值大小
|
||||
* 使用 compareTo 方法比较,避免 BigDecimal 的 scale 问题
|
||||
* @param target 比较目标值
|
||||
* @return 如果当前值大于目标值返回true,否则返回false(null值返回false)
|
||||
*/
|
||||
@@ -68,12 +69,16 @@ fun Any?.gt(target: Any?): Boolean {
|
||||
if (this == null || target == null) {
|
||||
return false
|
||||
}
|
||||
return this.toSafeBigDecimal() > target.toSafeBigDecimal()
|
||||
val thisValue = this.toSafeBigDecimal()
|
||||
val targetValue = target.toSafeBigDecimal()
|
||||
// 使用 compareTo 方法比较,避免 BigDecimal 的 scale 问题
|
||||
return thisValue.compareTo(targetValue) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 大于等于比较扩展函数
|
||||
* 安全地比较两个任意类型的数值大小
|
||||
* 使用 compareTo 方法比较,避免 BigDecimal 的 scale 问题
|
||||
* @param target 比较目标值
|
||||
* @return 如果当前值大于等于目标值返回true,否则返回false(null值返回false)
|
||||
*/
|
||||
@@ -81,12 +86,16 @@ fun Any?.gte(target: Any?): Boolean {
|
||||
if (this == null || target == null) {
|
||||
return false
|
||||
}
|
||||
return this.toSafeBigDecimal() >= target.toSafeBigDecimal()
|
||||
val thisValue = this.toSafeBigDecimal()
|
||||
val targetValue = target.toSafeBigDecimal()
|
||||
// 使用 compareTo 方法比较,避免 BigDecimal 的 scale 问题
|
||||
return thisValue.compareTo(targetValue) >= 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 小于比较扩展函数
|
||||
* 安全地比较两个任意类型的数值大小
|
||||
* 使用 compareTo 方法比较,避免 BigDecimal 的 scale 问题
|
||||
* @param target 比较目标值
|
||||
* @return 如果当前值小于目标值返回true,否则返回false(null值返回false)
|
||||
*/
|
||||
@@ -94,12 +103,16 @@ fun Any?.lt(target: Any?): Boolean {
|
||||
if (this == null || target == null) {
|
||||
return false
|
||||
}
|
||||
return this.toSafeBigDecimal() < target.toSafeBigDecimal()
|
||||
val thisValue = this.toSafeBigDecimal()
|
||||
val targetValue = target.toSafeBigDecimal()
|
||||
// 使用 compareTo 方法比较,避免 BigDecimal 的 scale 问题
|
||||
return thisValue.compareTo(targetValue) < 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 小于等于比较扩展函数
|
||||
* 安全地比较两个任意类型的数值大小
|
||||
* 使用 compareTo 方法比较,避免 BigDecimal 的 scale 问题
|
||||
* @param target 比较目标值
|
||||
* @return 如果当前值小于等于目标值返回true,否则返回false(null值返回false)
|
||||
*/
|
||||
@@ -107,12 +120,16 @@ fun Any?.lte(target: Any?): Boolean {
|
||||
if (this == null || target == null) {
|
||||
return false
|
||||
}
|
||||
return this.toSafeBigDecimal() <= target.toSafeBigDecimal()
|
||||
val thisValue = this.toSafeBigDecimal()
|
||||
val targetValue = target.toSafeBigDecimal()
|
||||
// 使用 compareTo 方法比较,避免 BigDecimal 的 scale 问题
|
||||
return thisValue.compareTo(targetValue) <= 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 等于比较扩展函数
|
||||
* 安全地比较两个任意类型的数值是否相等
|
||||
* 使用 compareTo 方法比较,避免 BigDecimal 的 scale 问题
|
||||
* @param target 比较目标值
|
||||
* @return 如果当前值等于目标值返回true,否则返回false(null值返回false)
|
||||
*/
|
||||
@@ -120,12 +137,17 @@ fun Any?.eq(target: Any?): Boolean {
|
||||
if (this == null || target == null) {
|
||||
return false
|
||||
}
|
||||
return this.toSafeBigDecimal() == target.toSafeBigDecimal()
|
||||
val thisValue = this.toSafeBigDecimal()
|
||||
val targetValue = target.toSafeBigDecimal()
|
||||
// 使用 compareTo 方法比较,避免 BigDecimal 的 scale 问题
|
||||
// 例如:"0.0" 和 "0" 在数值上相等,但 scale 不同
|
||||
return thisValue.compareTo(targetValue) == 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 不等于比较扩展函数
|
||||
* 安全地比较两个任意类型的数值是否不相等
|
||||
* 使用 compareTo 方法比较,避免 BigDecimal 的 scale 问题
|
||||
* @param target 比较目标值
|
||||
* @return 如果当前值不等于目标值返回true,否则返回false(null值返回false)
|
||||
*/
|
||||
@@ -133,6 +155,9 @@ fun Any?.neq(target: Any?): Boolean {
|
||||
if (this == null || target == null) {
|
||||
return false
|
||||
}
|
||||
return this.toSafeBigDecimal() != target.toSafeBigDecimal()
|
||||
val thisValue = this.toSafeBigDecimal()
|
||||
val targetValue = target.toSafeBigDecimal()
|
||||
// 使用 compareTo 方法比较,避免 BigDecimal 的 scale 问题
|
||||
return thisValue.compareTo(targetValue) != 0
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user