diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/AccountController.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/AccountController.kt index 1812aea..ce845e1 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/AccountController.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/AccountController.kt @@ -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> { + 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}")) + } + } } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt index 5bd21a8..85150d5 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt @@ -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, + val historyPositions: List +) + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt index 90b718b..ea6565b 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt @@ -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 { + return try { + val accounts = accountRepository.findAll() + val currentPositions = mutableListOf() + val historyPositions = mutableListOf() + + // 遍历所有账户,查询每个账户的仓位 + 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 查询该账户的活跃订单 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt index 0048388..120ebd8 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt @@ -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> { + suspend fun getPositions(proxyWalletAddress: String, sortBy: String? = "CURRENT"): Result> { return try { // 使用代理钱包地址查询仓位 + // sortBy=CURRENT 表示只返回当前仓位 val response = dataApi.getPositions( user = proxyWalletAddress, limit = 500, // 最大限制 - offset = 0 + offset = 0, + sortBy = sortBy ) if (response.isSuccessful && response.body() != null) { diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/MathExt.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/MathExt.kt index 96a0f34..8073ac6 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/MathExt.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/MathExt.kt @@ -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 } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 356e667..e064b14 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,7 +9,7 @@ import AccountEdit from './pages/AccountEdit' import LeaderList from './pages/LeaderList' import LeaderAdd from './pages/LeaderAdd' import ConfigPage from './pages/ConfigPage' -import OrderList from './pages/OrderList' +import PositionList from './pages/PositionList' import Statistics from './pages/Statistics' function App() { @@ -26,7 +26,7 @@ function App() { } /> } /> } /> - } /> + } /> } /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 6d60f55..89d31e6 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -41,9 +41,9 @@ const Layout: React.FC = ({ children }) => { label: '跟单配置' }, { - key: '/orders', + key: '/positions', icon: , - label: '订单管理' + label: '仓位管理' }, { key: '/statistics', @@ -108,8 +108,18 @@ const Layout: React.FC = ({ children }) => { // 桌面端布局 return ( - - + +
= ({ children }) => { justifyContent: 'center', color: '#fff', fontSize: '18px', - fontWeight: 'bold' + fontWeight: 'bold', + flexShrink: 0 }}> Polymarket 跟单
@@ -126,11 +137,20 @@ const Layout: React.FC = ({ children }) => { selectedKeys={[location.pathname]} items={menuItems} onClick={({ key }) => handleMenuClick(key)} - style={{ height: 'calc(100vh - 64px)', borderRight: 0 }} + style={{ + height: 'calc(100vh - 64px)', + borderRight: 0, + overflowY: 'auto' + }} />
- - + + {children} diff --git a/frontend/src/pages/PositionList.tsx b/frontend/src/pages/PositionList.tsx new file mode 100644 index 0000000..87a7c57 --- /dev/null +++ b/frontend/src/pages/PositionList.tsx @@ -0,0 +1,835 @@ +import { useEffect, useState, useMemo } from 'react' +import { Card, Table, Tag, message, Space, Input, Radio, Select, Button, Row, Col, Empty } from 'antd' +import { SearchOutlined, AppstoreOutlined, UnorderedListOutlined, UpOutlined, DownOutlined } from '@ant-design/icons' +import { apiService } from '../services/api' +import type { AccountPosition, Account } from '../types' +import { useMediaQuery } from 'react-responsive' + +type PositionFilter = 'current' | 'historical' +type ViewMode = 'card' | 'list' + +const PositionList: React.FC = () => { + const isMobile = useMediaQuery({ maxWidth: 768 }) + const [currentPositions, setCurrentPositions] = useState([]) + const [historyPositions, setHistoryPositions] = useState([]) + const [accounts, setAccounts] = useState([]) + const [loading, setLoading] = useState(false) + const [accountsLoading, setAccountsLoading] = useState(false) + const [searchKeyword, setSearchKeyword] = useState('') + const [positionFilter, setPositionFilter] = useState('current') + const [selectedAccountId, setSelectedAccountId] = useState(undefined) + const [viewMode, setViewMode] = useState(isMobile ? 'card' : 'list') + const [expandedCards, setExpandedCards] = useState>(new Set()) + + useEffect(() => { + fetchAccounts() + fetchPositions() + }, []) + + const fetchAccounts = async () => { + setAccountsLoading(true) + try { + const response = await apiService.accounts.list() + if (response.data.code === 0 && response.data.data) { + setAccounts(response.data.data.list || []) + } else { + message.error(response.data.msg || '获取账户列表失败') + } + } catch (error: any) { + message.error(error.message || '获取账户列表失败') + } finally { + setAccountsLoading(false) + } + } + + const fetchPositions = async () => { + setLoading(true) + try { + const response = await apiService.accounts.positionsList() + if (response.data.code === 0 && response.data.data) { + setCurrentPositions(response.data.data.currentPositions || []) + setHistoryPositions(response.data.data.historyPositions || []) + } else { + message.error(response.data.msg || '获取仓位列表失败') + } + } catch (error: any) { + message.error(error.message || '获取仓位列表失败') + } finally { + setLoading(false) + } + } + + // 根据筛选器选择对应的仓位列表 + const basePositions = useMemo(() => { + return positionFilter === 'current' ? currentPositions : historyPositions + }, [positionFilter, currentPositions, historyPositions]) + + // 本地搜索和筛选过滤 + const filteredPositions = useMemo(() => { + let filtered = basePositions + + // 1. 先按账户筛选 + if (selectedAccountId !== undefined) { + filtered = filtered.filter(p => p.accountId === selectedAccountId) + } + + // 2. 最后按关键词搜索 + if (searchKeyword.trim()) { + const keyword = searchKeyword.trim().toLowerCase() + filtered = filtered.filter(position => { + // 搜索账户名 + if (position.accountName?.toLowerCase().includes(keyword)) { + return true + } + // 搜索钱包地址 + if (position.walletAddress.toLowerCase().includes(keyword)) { + return true + } + // 搜索市场标题 + if (position.marketTitle?.toLowerCase().includes(keyword)) { + return true + } + // 搜索市场slug + if (position.marketSlug?.toLowerCase().includes(keyword)) { + return true + } + // 搜索市场ID + if (position.marketId.toLowerCase().includes(keyword)) { + return true + } + // 搜索方向(YES/NO) + if (position.side.toLowerCase().includes(keyword)) { + return true + } + return false + }) + } + + return filtered + }, [basePositions, searchKeyword, selectedAccountId]) + + const getSideColor = (side: string) => { + return side === 'YES' ? 'green' : 'red' + } + + const formatNumber = (value: string | undefined, decimals: number = 2) => { + if (!value) return '-' + const num = parseFloat(value) + if (isNaN(num)) return value + return num.toFixed(decimals) + } + + const formatPercent = (value: string | undefined) => { + if (!value) return '-' + const num = parseFloat(value) + if (isNaN(num)) return value + return `${num >= 0 ? '+' : ''}${num.toFixed(2)}%` + } + + // 切换卡片展开/折叠状态 + const toggleCard = (cardKey: string) => { + setExpandedCards(prev => { + const newSet = new Set(prev) + if (newSet.has(cardKey)) { + newSet.delete(cardKey) + } else { + newSet.add(cardKey) + } + return newSet + }) + } + + // 渲染卡片视图 + const renderCardView = () => { + if (filteredPositions.length === 0) { + return ( + + ) + } + + return ( + + {filteredPositions.map((position, index) => { + const pnlNum = parseFloat(position.pnl || '0') + const isProfit = pnlNum >= 0 + // 只有当前仓位才根据盈亏显示边框颜色 + const borderColor = positionFilter === 'current' + ? (isProfit ? 'rgba(82, 196, 26, 0.2)' : 'rgba(245, 34, 45, 0.2)') + : 'rgba(0,0,0,0.06)' + + const cardKey = `${position.accountId}-${position.marketId}-${index}` + const isExpanded = expandedCards.has(cardKey) + // 移动端需要折叠功能,桌面端始终展开 + const shouldCollapse = isMobile && !isExpanded + + return ( + + isMobile && toggleCard(cardKey)} + style={{ + height: '100%', + borderRadius: '12px', + boxShadow: '0 2px 8px rgba(0,0,0,0.08)', + transition: 'all 0.3s ease', + border: `1px solid ${borderColor}`, + cursor: isMobile ? 'pointer' : 'default' + }} + bodyStyle={{ padding: '16px' }} + > + {/* 头部:市场图标和标题 */} +
+
+ {position.marketIcon && ( + {position.marketTitle { + e.currentTarget.style.display = 'none' + }} + /> + )} +
+ {position.marketTitle ? ( + position.marketSlug ? ( + e.stopPropagation()} + style={{ + fontWeight: 'bold', + color: '#1890ff', + textDecoration: 'none', + fontSize: '15px', + lineHeight: '1.4', + overflow: 'hidden', + textOverflow: 'ellipsis', + display: '-webkit-box', + WebkitLineClamp: 2, + WebkitBoxOrient: 'vertical' + }} + > + {position.marketTitle} + + ) : ( +
+ {position.marketTitle} +
+ ) + ) : ( +
+ {position.marketId.slice(0, 16)}... +
+ )} + {position.marketSlug && ( +
+ {position.marketSlug} +
+ )} +
+
+
+ + {/* 账户信息 */} +
+
+
+
+ {position.accountName || `账户 ${position.accountId}`} +
+
+ {position.walletAddress.slice(0, 6)}...{position.walletAddress.slice(-4)} +
+
+ + {position.side} + +
+
+ + {/* 关键数据 */} +
+ {/* 移动端折叠时,显示盈亏(使用简单样式) */} + {shouldCollapse && positionFilter === 'current' && ( +
+ 盈亏 + + {pnlNum >= 0 ? '+' : ''}{formatNumber(position.pnl, 2)} USDC + +
+ )} + + {/* 展开时显示所有数据 */} + {!shouldCollapse && ( + <> +
+ 数量 + + {formatNumber(position.quantity, 4)} + +
+
+ 平均价格 + + {formatNumber(position.avgPrice, 4)} + +
+ {positionFilter === 'current' && position.currentPrice && ( + <> +
+ 当前价格 + + {formatNumber(position.currentPrice, 4)} + +
+
+ 当前价值 + + {formatNumber(position.currentValue, 2)} USDC + +
+ + )} + + )} + + {/* 移动端展开/折叠指示器 */} + {isMobile && ( +
+ {isExpanded ? ( + + ) : ( + + )} +
+ )} +
+ + {/* 盈亏信息 - 突出显示(仅当前仓位显示,仅展开时显示) */} + {positionFilter === 'current' && !shouldCollapse && ( +
+
+ 盈亏 + + {pnlNum >= 0 ? '+' : ''}{formatNumber(position.pnl, 2)} USDC + +
+
+ + {formatPercent(position.percentPnl)} + +
+ {position.realizedPnl && ( +
+ 已实现盈亏 + = 0 ? '#52c41a' : '#f5222d', + fontWeight: '500' + }}> + {parseFloat(position.realizedPnl) >= 0 ? '+' : ''}{formatNumber(position.realizedPnl, 2)} USDC + +
+ )} +
+ )} + + {/* 状态标签(移动端折叠时隐藏) */} + {positionFilter === 'current' && !shouldCollapse && (position.redeemable || position.mergeable) && ( +
+ {position.redeemable && ( + 可赎回 + )} + {position.mergeable && ( + 可合并 + )} +
+ )} +
+ + ) + })} +
+ ) + } + + // 根据仓位类型动态生成列(历史仓位不显示当前价格、当前价值、状态列) + const columns = useMemo(() => { + const baseColumns: any[] = [ + { + title: '', + key: 'icon', + width: 50, + render: (_: any, record: AccountPosition) => { + if (!record.marketIcon) return null + return ( + {record.marketTitle { + // 图片加载失败时隐藏 + e.currentTarget.style.display = 'none' + }} + /> + ) + }, + fixed: isMobile ? ('left' as const) : undefined + }, + { + title: '账户', + dataIndex: 'accountName', + key: 'accountName', + render: (text: string | undefined, record: AccountPosition) => ( +
+
+ {text || `账户 ${record.accountId}`} +
+
+ {record.walletAddress.slice(0, 6)}...{record.walletAddress.slice(-6)} +
+
+ ), + fixed: isMobile ? ('left' as const) : undefined, + width: isMobile ? 150 : 200 + }, + { + title: '市场', + dataIndex: 'marketTitle', + key: 'marketTitle', + render: (text: string | undefined, record: AccountPosition) => { + const url = record.marketSlug + ? `https://polymarket.com/event/${record.marketSlug}` + : null + + const handleTitleClick = (e: React.MouseEvent) => { + e.stopPropagation() + if (url) { + window.open(url, '_blank', 'noopener,noreferrer') + } + } + + return ( +
+ {text ? ( +
+ {url ? ( + + {text} + + ) : ( +
{text}
+ )} +
+ ) : ( +
+ {record.marketId.slice(0, 10)}... +
+ )} + {record.marketSlug && ( +
{record.marketSlug}
+ )} +
+ ) + }, + width: isMobile ? 200 : 250 + }, + { + title: '方向', + dataIndex: 'side', + key: 'side', + render: (side: string) => ( + {side} + ), + width: 80 + }, + { + title: '数量', + dataIndex: 'quantity', + key: 'quantity', + render: (quantity: string) => formatNumber(quantity, 4), + align: 'right' as const, + width: 100 + }, + { + title: '平均价格', + dataIndex: 'avgPrice', + key: 'avgPrice', + render: (price: string) => formatNumber(price, 4), + align: 'right' as const, + width: 120 + }, + ] + + // 只有当前仓位才显示当前价格和当前价值列 + if (positionFilter === 'current') { + baseColumns.push( + { + title: '当前价格', + dataIndex: 'currentPrice', + key: 'currentPrice', + render: (price: string) => formatNumber(price, 4), + align: 'right' as const, + width: 120 + }, + { + title: '当前价值', + dataIndex: 'currentValue', + key: 'currentValue', + render: (value: string) => ( + + {formatNumber(value, 2)} USDC + + ), + align: 'right' as const, + width: 120, + sorter: (a: AccountPosition, b: AccountPosition) => { + const valA = parseFloat(a.currentValue || '0') + const valB = parseFloat(b.currentValue || '0') + return valA - valB + }, + defaultSortOrder: 'descend' as const + } + ) + } + + // 只有当前仓位才显示盈亏和已实现盈亏列 + if (positionFilter === 'current') { + baseColumns.push( + { + title: '盈亏', + dataIndex: 'pnl', + key: 'pnl', + render: (pnl: string, record: AccountPosition) => { + const pnlNum = parseFloat(pnl || '0') + const percentPnl = parseFloat(record.percentPnl || '0') + return ( +
+
= 0 ? '#3f8600' : '#cf1322', + fontWeight: 'bold' + }}> + {pnlNum >= 0 ? '+' : ''}{formatNumber(pnl, 2)} USDC +
+
= 0 ? '#3f8600' : '#cf1322' + }}> + {formatPercent(record.percentPnl)} +
+
+ ) + }, + align: 'right' as const, + width: 150, + sorter: (a: AccountPosition, b: AccountPosition) => { + const pnlA = parseFloat(a.pnl || '0') + const pnlB = parseFloat(b.pnl || '0') + return pnlA - pnlB + } + }, + { + title: '已实现盈亏', + dataIndex: 'realizedPnl', + key: 'realizedPnl', + render: (realizedPnl: string | undefined, record: AccountPosition) => { + if (!realizedPnl) return '-' + const pnlNum = parseFloat(realizedPnl) + const percentPnl = parseFloat(record.percentRealizedPnl || '0') + return ( +
+
= 0 ? '#3f8600' : '#cf1322', + fontWeight: 'bold' + }}> + {pnlNum >= 0 ? '+' : ''}{formatNumber(realizedPnl, 2)} USDC +
+ {record.percentRealizedPnl && ( +
= 0 ? '#3f8600' : '#cf1322' + }}> + {formatPercent(record.percentRealizedPnl)} +
+ )} +
+ ) + }, + align: 'right' as const, + width: 150 + } + ) + } + + // 只有当前仓位才显示状态列 + if (positionFilter === 'current') { + baseColumns.push({ + title: '状态', + key: 'status', + render: (_: any, record: AccountPosition) => ( + + {record.redeemable && 可赎回} + {record.mergeable && 可合并} + + ), + width: 120 + }) + } + + return baseColumns + }, [positionFilter, isMobile]) + + // 统计当前和历史仓位数量(根据账户筛选) + const filteredCurrentPositions = useMemo(() => { + if (selectedAccountId === undefined) return currentPositions + return currentPositions.filter(p => p.accountId === selectedAccountId) + }, [currentPositions, selectedAccountId]) + + const filteredHistoryPositions = useMemo(() => { + if (selectedAccountId === undefined) return historyPositions + return historyPositions.filter(p => p.accountId === selectedAccountId) + }, [historyPositions, selectedAccountId]) + + const currentCount = filteredCurrentPositions.length + const historicalCount = filteredHistoryPositions.length + + return ( +
+
+
+

仓位管理

+
+ } + value={searchKeyword} + onChange={(e) => setSearchKeyword(e.target.value)} + allowClear + style={{ width: isMobile ? '100%' : 300 }} + /> + {!isMobile && ( + +
+
+
+