From b4fb16af9e6eca0245c80eb3bf78938b3e1ebb12 Mon Sep 17 00:00:00 2001 From: WrBug Date: Fri, 28 Nov 2025 04:11:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E4=BB=93=E4=BD=8D?= =?UTF-8?q?=E5=87=BA=E5=94=AE=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BB=B7=E6=A0=BC=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增仓位出售功能,支持市价和限价卖出 - 添加卖出模态框,包含订单类型选择、数量快捷按钮、限价输入 - 实现实时平仓收益计算和显示 - 修复价格接口:改用Gamma API获取价格(支持condition_ids参数) - 优化价格选择逻辑:卖出使用bestBid,买入使用bestAsk - 添加提交订单的加载状态和错误处理 - 优化UI提示,明确标注价格用途(买入/卖出参考) --- .../polymarketbot/api/PolymarketClobApi.kt | 11 +- .../controller/AccountController.kt | 74 ++++ .../com/wrbug/polymarketbot/dto/AccountDto.kt | 44 ++ .../polymarketbot/service/AccountService.kt | 159 ++++++++ .../service/PolymarketClobService.kt | 3 +- frontend/src/pages/PositionList.tsx | 379 +++++++++++++++++- frontend/src/services/api.ts | 14 +- frontend/src/types/index.ts | 44 ++ 8 files changed, 709 insertions(+), 19 deletions(-) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketClobApi.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketClobApi.kt index 70eda9d..2247e8d 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketClobApi.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketClobApi.kt @@ -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 /** * 获取价格信息 + * 注意: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 /** 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 ce845e1..53d4eb3 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/AccountController.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/AccountController.kt @@ -229,5 +229,79 @@ class AccountController( ResponseEntity.ok(ApiResponse.serverError("查询仓位列表失败: ${e.message}")) } } + + /** + * 卖出仓位 + */ + @PostMapping("/positions/sell") + fun sellPosition(@RequestBody request: PositionSellRequest): ResponseEntity> { + 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> { + 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}")) + } + } } 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 aac0392..a1a43b2 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt @@ -135,3 +135,47 @@ data class PositionListResponse( val historyPositions: List ) +/** + * 仓位卖出请求 + */ +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? // 中间价 +) + 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 e713a2c..674161e 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt @@ -648,6 +648,165 @@ class AccountService( } } + /** + * 卖出仓位 + */ + suspend fun sellPosition(request: PositionSellRequest): Result { + 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 { + 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 查询该账户的活跃订单 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PolymarketClobService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PolymarketClobService.kt index 9e2b4b4..d9b2693 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PolymarketClobService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PolymarketClobService.kt @@ -19,10 +19,11 @@ class PolymarketClobService( /** * 获取订单簿 + * 使用 market 参数(condition ID) */ suspend fun getOrderbook(market: String): Result { 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 { diff --git a/frontend/src/pages/PositionList.tsx b/frontend/src/pages/PositionList.tsx index 08b1dd0..a8f91d1 100644 --- a/frontend/src/pages/PositionList.tsx +++ b/frontend/src/pages/PositionList.tsx @@ -1,8 +1,8 @@ import { useEffect, useState, useMemo } from 'react' -import { Card, Table, Tag, message, Space, Input, Radio, Select, Button, Row, Col, Empty } from 'antd' +import { Card, Table, Tag, message, Space, Input, Radio, Select, Button, Row, Col, Empty, Modal, Form } from 'antd' import { SearchOutlined, AppstoreOutlined, UnorderedListOutlined, UpOutlined, DownOutlined } from '@ant-design/icons' import { apiService } from '../services/api' -import type { AccountPosition, Account, PositionPushMessage } from '../types' +import type { AccountPosition, Account, PositionPushMessage, PositionSellRequest, MarketPriceResponse } from '../types' import { getPositionKey } from '../types' import { useMediaQuery } from 'react-responsive' import { useWebSocketSubscription } from '../hooks/useWebSocket' @@ -23,6 +23,14 @@ const PositionList: React.FC = () => { const [selectedAccountId, setSelectedAccountId] = useState(undefined) const [viewMode, setViewMode] = useState(isMobile ? 'card' : 'list') const [expandedCards, setExpandedCards] = useState>(new Set()) + const [sellModalVisible, setSellModalVisible] = useState(false) + const [selectedPosition, setSelectedPosition] = useState(null) + const [marketPrice, setMarketPrice] = useState(null) + const [orderType, setOrderType] = useState<'MARKET' | 'LIMIT'>('LIMIT') + const [sellQuantity, setSellQuantity] = useState('') + const [limitPrice, setLimitPrice] = useState('') + const [form] = Form.useForm() + const [submitting, setSubmitting] = useState(false) const [wsConnected, setWsConnected] = useState(false) useEffect(() => { @@ -233,6 +241,127 @@ const PositionList: React.FC = () => { }) } + // 处理卖出按钮点击 + const handleSellClick = async (position: AccountPosition) => { + setSelectedPosition(position) + setSellModalVisible(true) + setOrderType('LIMIT') + setSellQuantity('') + setLimitPrice('') + form.resetFields() + + // 加载市场价格 + try { + const response = await apiService.accounts.getMarketPrice({ marketId: position.marketId }) + if (response.data.code === 0 && response.data.data) { + setMarketPrice(response.data.data) + // 默认使用最优买价作为限价 + if (response.data.data.bestBid) { + setLimitPrice(response.data.data.bestBid) + form.setFieldsValue({ limitPrice: response.data.data.bestBid }) + } + } + } catch (error: any) { + message.error('获取市场价格失败: ' + (error.message || '未知错误')) + } + } + + // 处理数量快捷按钮 + const handleQuantityQuickSelect = (percent: number) => { + if (!selectedPosition) return + const quantity = parseFloat(selectedPosition.quantity) + const sellQty = (quantity * percent / 100).toFixed(4) + setSellQuantity(sellQty) + form.setFieldsValue({ quantity: sellQty }) + // 使用当前卖出价格计算收益 + const price = getCurrentSellPrice() + if (price && price !== '0') { + calculatePnl(sellQty, price) + } + } + + // 计算平仓收益 + const calculatePnl = (quantity: string, price: string) => { + if (!selectedPosition || !quantity || !price) return { pnl: 0, percentPnl: 0 } + + const avgPrice = parseFloat(selectedPosition.avgPrice || '0') + const sellPrice = parseFloat(price || '0') + const qty = parseFloat(quantity || '0') + + // 验证数据有效性 + if (isNaN(avgPrice) || isNaN(sellPrice) || isNaN(qty) || avgPrice <= 0 || sellPrice <= 0 || qty <= 0) { + return { pnl: 0, percentPnl: 0 } + } + + // 计算收益:收益金额 = (卖出价格 - 平均买入价格) × 卖出数量 + const pnl = (sellPrice - avgPrice) * qty + // 计算收益率:收益率 = (卖出价格 - 平均买入价格) / 平均买入价格 × 100% + const percentPnl = ((sellPrice - avgPrice) / avgPrice) * 100 + + return { pnl, percentPnl } + } + + // 获取当前卖出价格(市价或限价) + // 卖出操作应该使用 bestBid(最优买价),因为你要卖给愿意买入的人 + const getCurrentSellPrice = (): string => { + if (orderType === 'MARKET') { + // 市价订单(卖出):优先使用最优买价(bestBid),因为卖出是卖给买单 + // 如果没有 bestBid,则使用当前价格,最后使用最新成交价 + return marketPrice?.bestBid || selectedPosition?.currentPrice || marketPrice?.lastPrice || '0' + } + return limitPrice || '0' + } + + // 提交卖出订单 + const handleSellSubmit = async () => { + if (!selectedPosition || submitting) return + + try { + await form.validateFields() + + setSubmitting(true) + + const request: PositionSellRequest = { + accountId: selectedPosition.accountId, + marketId: selectedPosition.marketId, + side: selectedPosition.side as 'YES' | 'NO', + orderType: orderType, + quantity: sellQuantity, + price: orderType === 'LIMIT' ? limitPrice : undefined + } + + const response = await apiService.accounts.sellPosition(request) + + if (response.data.code === 0) { + message.success('卖出订单创建成功') + setSellModalVisible(false) + // 重置表单 + setSellQuantity('') + setLimitPrice('') + form.resetFields() + // 仓位列表会通过WebSocket自动更新 + } else { + message.error(response.data.msg || '创建卖出订单失败') + } + } catch (error: any) { + if (error.errorFields) { + // 表单验证错误 + return + } + message.error('创建卖出订单失败: ' + (error.message || '未知错误')) + } finally { + setSubmitting(false) + } + } + + // 实时计算收益(用于显示) + const currentPnl = useMemo(() => { + if (!selectedPosition || !sellQuantity) return { pnl: 0, percentPnl: 0 } + const price = getCurrentSellPrice() + if (!price || price === '0') return { pnl: 0, percentPnl: 0 } + return calculatePnl(sellQuantity, price) + }, [selectedPosition, sellQuantity, orderType, limitPrice, marketPrice]) + // 渲染卡片视图 const renderCardView = () => { if (filteredPositions.length === 0) { @@ -477,14 +606,27 @@ const PositionList: React.FC = () => { )} - {/* 状态标签(移动端折叠时隐藏) */} - {positionFilter === 'current' && !shouldCollapse && (position.redeemable || position.mergeable) && ( -
+ {/* 操作按钮(移动端折叠时隐藏) */} + {positionFilter === 'current' && !shouldCollapse && ( +
+ {position.redeemable && ( - 可赎回 - )} - {position.mergeable && ( - 可合并 + )}
)} @@ -715,18 +857,34 @@ const PositionList: React.FC = () => { ) } - // 只有当前仓位才显示状态列 + // 只有当前仓位才显示操作列 if (positionFilter === 'current') { baseColumns.push({ - title: '状态', - key: 'status', + title: '操作', + key: 'action', render: (_: any, record: AccountPosition) => ( - {record.redeemable && 可赎回} - {record.mergeable && 可合并} + + {record.redeemable && ( + + )} ), - width: 120 + width: 150, + fixed: isMobile ? ('right' as const) : undefined }) } @@ -939,6 +1097,197 @@ const PositionList: React.FC = () => { /> )} + + {/* 出售模态框 */} + { + if (!submitting) { + setSellModalVisible(false) + } + }} + onOk={handleSellSubmit} + okText="确认卖出" + cancelText="取消" + width={isMobile ? '90%' : 600} + destroyOnClose + confirmLoading={submitting} + maskClosable={!submitting} + > + {selectedPosition && ( +
+
+
+ 账户: + {selectedPosition.accountName || `账户 ${selectedPosition.accountId}`} +
+
+ 方向: + {selectedPosition.side} +
+
+ 当前持仓: + {formatNumber(selectedPosition.quantity, 4)} +
+
+ 平均价格: + {formatNumber(selectedPosition.avgPrice, 4)} +
+ {selectedPosition.currentPrice && ( +
+ 当前价格: + {formatNumber(selectedPosition.currentPrice, 4)} +
+ )} +
+ + + { + setOrderType(e.target.value) + // 切换订单类型时重新计算收益 + if (sellQuantity) { + const price = e.target.value === 'MARKET' + ? (marketPrice?.bestBid || selectedPosition?.currentPrice || marketPrice?.lastPrice || '0') + : limitPrice || '0' + calculatePnl(sellQuantity, price) + } + }} + > + 市价出售 + 限价出售 + + + + { + if (!value || parseFloat(value) <= 0) { + return Promise.reject('卖出数量必须大于0') + } + if (parseFloat(value) > parseFloat(selectedPosition.quantity)) { + return Promise.reject('卖出数量不能超过持仓数量') + } + return Promise.resolve() + } + } + ]} + > + { + const newQuantity = e.target.value + setSellQuantity(newQuantity) + if (newQuantity) { + const price = getCurrentSellPrice() + calculatePnl(newQuantity, price) + } + }} + placeholder="请输入卖出数量" + suffix={ + + + + + + + } + /> + + + {orderType === 'LIMIT' && ( + { + if (!value || parseFloat(value) <= 0) { + return Promise.reject('价格必须大于0') + } + return Promise.resolve() + } + } + ]} + > + { + const newPrice = e.target.value + setLimitPrice(newPrice) + if (sellQuantity && newPrice) { + calculatePnl(sellQuantity, newPrice) + } + }} + placeholder="请输入限价价格" + /> + {marketPrice?.bestBid && ( +
+ 参考价格(最优买价,卖出参考): {formatNumber(marketPrice.bestBid, 4)} +
+ )} +
+ )} + + {orderType === 'MARKET' && ( +
+
市价参考(卖出)
+
+ {marketPrice?.bestBid ? ( + <>最优买价(卖出参考): {formatNumber(marketPrice.bestBid, 4)} + ) : selectedPosition?.currentPrice ? ( + <>当前价格: {formatNumber(selectedPosition.currentPrice, 4)} + ) : marketPrice?.lastPrice ? ( + <>最新成交价: {formatNumber(marketPrice.lastPrice, 4)} + ) : ( + 暂无价格数据 + )} +
+ {marketPrice?.bestAsk && ( +
+ 最优卖价(买入参考): {formatNumber(marketPrice.bestAsk, 4)} +
+ )} +
+ )} + + {/* 预计平仓收益 */} + {sellQuantity && ( +
= 0 ? 'rgba(82, 196, 26, 0.08)' : 'rgba(245, 34, 45, 0.08)', + border: `1px solid ${currentPnl.pnl >= 0 ? 'rgba(82, 196, 26, 0.2)' : 'rgba(245, 34, 45, 0.2)'}`, + borderRadius: '8px' + }}> +
预计平仓收益
+
= 0 ? '#52c41a' : '#f5222d', + marginBottom: '4px' + }}> + {currentPnl.pnl >= 0 ? '+' : ''}{currentPnl.pnl.toFixed(2)} USDC +
+
= 0 ? '#52c41a' : '#f5222d', + fontWeight: '500' + }}> + {currentPnl.percentPnl >= 0 ? '+' : ''}{currentPnl.percentPnl.toFixed(2)}% +
+
+ )} +
+ )} +
) } diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index abcdd81..2792ac3 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -97,7 +97,19 @@ export const apiService = { * 查询所有账户的仓位列表 */ positionsList: () => - apiClient.post>('/copy-trading/accounts/positions/list', {}) + apiClient.post>('/copy-trading/accounts/positions/list', {}), + + /** + * 卖出仓位 + */ + sellPosition: (data: any) => + apiClient.post>('/copy-trading/accounts/positions/sell', data), + + /** + * 获取市场价格 + */ + getMarketPrice: (data: any) => + apiClient.post>('/copy-trading/accounts/markets/price', data) }, /** diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 9131b66..0b06504 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -187,6 +187,50 @@ export interface PositionListResponse { historyPositions: AccountPosition[] } +/** + * 仓位卖出请求 + */ +export interface PositionSellRequest { + accountId: number + marketId: string + side: 'YES' | 'NO' + orderType: 'MARKET' | 'LIMIT' + quantity: string + price?: string // 限价订单必需 +} + +/** + * 仓位卖出响应 + */ +export interface PositionSellResponse { + orderId: string + marketId: string + side: string + orderType: string + quantity: string + price?: string + status: string + createdAt: number +} + +/** + * 市场价格请求 + */ +export interface MarketPriceRequest { + marketId: string +} + +/** + * 市场价格响应 + */ +export interface MarketPriceResponse { + marketId: string + lastPrice?: string + bestBid?: string + bestAsk?: string + midpoint?: string +} + /** * 仓位推送消息类型 */