From a31ed31c98918624ec80c0521728df328b11dc53 Mon Sep 17 00:00:00 2001 From: WrBug Date: Thu, 11 Dec 2025 03:17:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=92=8C=E7=BB=9F=E8=AE=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 系统配置支持显示完整的 Builder API Key(不再部分显示) - Secret 和 Passphrase 使用密码输入框,支持眼睛图标切换显示/隐藏 - 移除已配置提示,简化用户界面 - 修复总卖出金额统计:使用 SellMatchDetail 计算,确保准确性 - 修复 matchSellOrder 中价格不一致问题:matchDetails 使用实际卖出价格 - 移动端 Leader 列表移除网站显示行 - 优化订单创建前订单簿匹配检查,避免 FAK 订单失败 --- .../com/wrbug/polymarketbot/dto/AccountDto.kt | 3 ++ .../statistics/CopyOrderTrackingService.kt | 49 ++++++++++--------- .../CopyTradingStatisticsService.kt | 4 +- .../service/system/SystemConfigService.kt | 28 +++++++++++ frontend/src/pages/LeaderList.tsx | 19 +------ frontend/src/pages/SystemSettings.tsx | 25 +++++----- frontend/src/types/index.ts | 3 ++ 7 files changed, 77 insertions(+), 54 deletions(-) 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 83acb9b..d7130d3 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt @@ -36,6 +36,9 @@ data class SystemConfigDto( val builderApiKeyConfigured: Boolean, // Builder API Key 是否已配置 val builderSecretConfigured: Boolean, // Builder Secret 是否已配置 val builderPassphraseConfigured: Boolean, // Builder Passphrase 是否已配置 + val builderApiKeyDisplay: String? = null, // Builder API Key 显示值(部分显示,用于前端展示) + val builderSecretDisplay: String? = null, // Builder Secret 显示值(部分显示,用于前端展示) + val builderPassphraseDisplay: String? = null, // Builder Passphrase 显示值(部分显示,用于前端展示) val autoRedeemEnabled: Boolean = true // 自动赎回(系统级别配置,默认开启) ) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt index f30229b..fd33965 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt @@ -754,7 +754,28 @@ open class CopyOrderTrackingService( return } - // 4. 按FIFO顺序匹配,计算实际可以卖出的数量 + // 4. 获取tokenId(直接使用outcomeIndex,支持多元市场) + val tokenIdResult = blockchainService.getTokenId(leaderSellTrade.market, leaderSellTrade.outcomeIndex) + if (tokenIdResult.isFailure) { + logger.error("获取tokenId失败: market=${leaderSellTrade.market}, outcomeIndex=${leaderSellTrade.outcomeIndex}, error=${tokenIdResult.exceptionOrNull()?.message}") + return + } + val tokenId = tokenIdResult.getOrNull() ?: return + + // 5. 计算卖出价格(优先使用订单簿 bestBid,失败则使用 Leader 价格,固定按90%计算) + // 注意:需要先计算卖出价格,因为后续创建 matchDetails 需要使用实际卖出价格 + val leaderPrice = leaderSellTrade.price.toSafeBigDecimal() + val sellPrice = runCatching { + clobService.getOrderbookByTokenId(tokenId) + .getOrNull() + ?.let { calculateMarketSellPrice(it) } + } + .onFailure { e -> logger.warn("获取订单簿或计算 bestBid 失败,使用 Leader 价格: tokenId=$tokenId, error=${e.message}") } + .getOrNull() + ?: calculateFallbackSellPrice(leaderPrice) + + // 6. 按FIFO顺序匹配,计算实际可以卖出的数量 + // 使用计算出的实际卖出价格(而不是 Leader 价格)来创建匹配明细 var totalMatched = BigDecimal.ZERO var remaining = needMatch val matchDetails = mutableListOf() @@ -769,19 +790,18 @@ open class CopyOrderTrackingService( if (matchQty.lte(BigDecimal.ZERO)) continue - // 计算盈亏 + // 计算盈亏(使用实际卖出价格) val buyPrice = order.price.toSafeBigDecimal() - val sellPrice = leaderSellTrade.price.toSafeBigDecimal() val realizedPnl = sellPrice.subtract(buyPrice).multi(matchQty) - // 创建匹配明细(稍后保存) + // 创建匹配明细(使用实际卖出价格) val detail = SellMatchDetail( matchRecordId = 0, // 稍后设置 trackingId = order.id!!, buyOrderId = order.buyOrderId, matchedQuantity = matchQty, buyPrice = buyPrice, - sellPrice = sellPrice, + sellPrice = sellPrice, // 使用实际卖出价格,与 SellMatchRecord 保持一致 realizedPnl = realizedPnl ) matchDetails.add(detail) @@ -794,25 +814,6 @@ open class CopyOrderTrackingService( return } - // 5. 获取tokenId(直接使用outcomeIndex,支持多元市场) - val tokenIdResult = blockchainService.getTokenId(leaderSellTrade.market, leaderSellTrade.outcomeIndex) - if (tokenIdResult.isFailure) { - logger.error("获取tokenId失败: market=${leaderSellTrade.market}, outcomeIndex=${leaderSellTrade.outcomeIndex}, error=${tokenIdResult.exceptionOrNull()?.message}") - return - } - val tokenId = tokenIdResult.getOrNull() ?: return - - // 6. 计算卖出价格(优先使用订单簿 bestBid,失败则使用 Leader 价格,固定按90%计算) - val leaderPrice = leaderSellTrade.price.toSafeBigDecimal() - val sellPrice = runCatching { - clobService.getOrderbookByTokenId(tokenId) - .getOrNull() - ?.let { calculateMarketSellPrice(it) } - } - .onFailure { e -> logger.warn("获取订单簿或计算 bestBid 失败,使用 Leader 价格: tokenId=$tokenId, error=${e.message}") } - .getOrNull() - ?: calculateFallbackSellPrice(leaderPrice) - // 7. 解密私钥(在方法开始时解密一次,后续复用) val decryptedPrivateKey = decryptPrivateKey(account) // 8. 创建并签名卖出订单 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingStatisticsService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingStatisticsService.kt index 7b3fbc1..9bffd2a 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingStatisticsService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingStatisticsService.kt @@ -298,8 +298,10 @@ class CopyTradingStatisticsService( } // 卖出统计 + // 使用 SellMatchDetail 计算总卖出金额,确保准确性 + // 因为每个明细都记录了准确的匹配数量和卖出价格 val totalSellQuantity = sellRecords.sumOf { it.totalMatchedQuantity.toSafeBigDecimal() } - val totalSellAmount = sellRecords.sumOf { it.totalMatchedQuantity.toSafeBigDecimal().multi(it.sellPrice) } + val totalSellAmount = matchDetails.sumOf { it.matchedQuantity.toSafeBigDecimal().multi(it.sellPrice) } val totalSellOrders = sellRecords.size.toLong() // 持仓统计 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/SystemConfigService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/SystemConfigService.kt index 5a8fe57..db20e6c 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/SystemConfigService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/SystemConfigService.kt @@ -36,10 +36,38 @@ class SystemConfigService( val builderPassphrase = getConfigValue(CONFIG_KEY_BUILDER_PASSPHRASE) val autoRedeem = isAutoRedeemEnabled() + // 获取完整的 API Key(用于前端展示) + val builderApiKeyDisplay = builderApiKey?.let { + try { + cryptoUtils.decrypt(it) + } catch (e: Exception) { + null + } + } + + val builderSecretDisplay = builderSecret?.let { + try { + cryptoUtils.decrypt(it) + } catch (e: Exception) { + null + } + } + + val builderPassphraseDisplay = builderPassphrase?.let { + try { + cryptoUtils.decrypt(it) + } catch (e: Exception) { + null + } + } + return SystemConfigDto( builderApiKeyConfigured = builderApiKey != null, builderSecretConfigured = builderSecret != null, builderPassphraseConfigured = builderPassphrase != null, + builderApiKeyDisplay = builderApiKeyDisplay, + builderSecretDisplay = builderSecretDisplay, + builderPassphraseDisplay = builderPassphraseDisplay, autoRedeemEnabled = autoRedeem ) } diff --git a/frontend/src/pages/LeaderList.tsx b/frontend/src/pages/LeaderList.tsx index a4919af..a4affd7 100644 --- a/frontend/src/pages/LeaderList.tsx +++ b/frontend/src/pages/LeaderList.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { Card, Table, Button, Space, Tag, Popconfirm, message, List, Empty, Spin, Divider, Typography } from 'antd' -import { PlusOutlined, EditOutlined, DeleteOutlined, LinkOutlined, GlobalOutlined } from '@ant-design/icons' +import { PlusOutlined, EditOutlined, DeleteOutlined, GlobalOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { apiService } from '../services/api' import type { Leader } from '../types' @@ -225,23 +225,6 @@ const LeaderList: React.FC = () => { )} - {/* 网站 */} - {leader.website && ( -
- - {t('leaderList.website') || '网站'}: - - - {leader.website} - -
- )} - {/* 跟单关系数 */} diff --git a/frontend/src/pages/SystemSettings.tsx b/frontend/src/pages/SystemSettings.tsx index e44f22e..0447ecf 100644 --- a/frontend/src/pages/SystemSettings.tsx +++ b/frontend/src/pages/SystemSettings.tsx @@ -348,10 +348,11 @@ const SystemSettings: React.FC = () => { if (response.data.code === 0 && response.data.data) { const config = response.data.data setSystemConfig(config) + // 将已配置的值填充到输入框中 relayerForm.setFieldsValue({ - builderApiKey: '', - builderSecret: '', - builderPassphrase: '', + builderApiKey: config.builderApiKeyDisplay || '', + builderSecret: config.builderSecretDisplay || '', + builderPassphrase: config.builderPassphraseDisplay || '', }) autoRedeemForm.setFieldsValue({ autoRedeemEnabled: config.autoRedeemEnabled @@ -685,30 +686,32 @@ const SystemSettings: React.FC = () => { - (visible ? 👁️ : 👁️‍🗨️)} /> - (visible ? 👁️ : 👁️‍🗨️)} /> diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 76671e7..b41a86c 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -770,6 +770,9 @@ export interface SystemConfig { builderApiKeyConfigured: boolean builderSecretConfigured: boolean builderPassphraseConfigured: boolean + builderApiKeyDisplay?: string // Builder API Key 显示值(部分显示) + builderSecretDisplay?: string // Builder Secret 显示值(部分显示) + builderPassphraseDisplay?: string // Builder Passphrase 显示值(部分显示) autoRedeemEnabled: boolean // 自动赎回(系统级别配置,默认开启) }