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 f26bbeb..78abcbe 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/AccountController.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/AccountController.kt @@ -35,7 +35,6 @@ class AccountController( val result = accountService.importAccount(request) result.fold( onSuccess = { account -> - logger.info("成功导入账户: ${account.id}") ResponseEntity.ok(ApiResponse.success(account)) }, onFailure = { e -> @@ -61,7 +60,6 @@ class AccountController( val result = accountService.updateAccount(request) result.fold( onSuccess = { account -> - logger.info("成功更新账户: ${account.id}") ResponseEntity.ok(ApiResponse.success(account)) }, onFailure = { e -> @@ -87,7 +85,6 @@ class AccountController( val result = accountService.deleteAccount(request.accountId) result.fold( onSuccess = { - logger.info("成功删除账户: ${request.accountId}") ResponseEntity.ok(ApiResponse.success(Unit)) }, onFailure = { e -> @@ -114,7 +111,6 @@ class AccountController( val result = accountService.getAccountList() result.fold( onSuccess = { response -> - logger.info("成功查询账户列表: ${response.total} 个账户") ResponseEntity.ok(ApiResponse.success(response)) }, onFailure = { e -> @@ -137,7 +133,6 @@ class AccountController( val result = accountService.getAccountDetail(request.accountId) result.fold( onSuccess = { account -> - logger.info("成功查询账户详情: ${account.id}") ResponseEntity.ok(ApiResponse.success(account)) }, onFailure = { e -> @@ -163,7 +158,6 @@ class AccountController( val result = accountService.getAccountBalance(request.accountId) result.fold( onSuccess = { balance -> - logger.info("成功查询账户余额") ResponseEntity.ok(ApiResponse.success(balance)) }, onFailure = { e -> @@ -189,7 +183,6 @@ class AccountController( val result = accountService.setDefaultAccount(request.accountId) result.fold( onSuccess = { - logger.info("成功设置默认账户: ${request.accountId}") ResponseEntity.ok(ApiResponse.success(Unit)) }, onFailure = { e -> @@ -216,7 +209,6 @@ class AccountController( 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 -> @@ -260,7 +252,6 @@ class AccountController( 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 -> @@ -287,7 +278,6 @@ class AccountController( val result = runBlocking { accountService.getRedeemablePositionsSummary(request.accountId) } result.fold( onSuccess = { summary -> - logger.info("获取可赎回仓位统计成功: 账户=${request.accountId}, 数量=${summary.totalCount}, 价值=${summary.totalValue}") ResponseEntity.ok(ApiResponse.success(summary)) }, onFailure = { e -> @@ -331,7 +321,6 @@ class AccountController( val result = runBlocking { accountService.redeemPositions(request) } result.fold( onSuccess = { response -> - logger.info("成功赎回仓位: 账户数=${response.transactions.size}, 交易数=${response.transactions.size}, 总价值=${response.totalRedeemedValue}") ResponseEntity.ok(ApiResponse.success(response)) }, onFailure = { e -> diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/CopyTradingController.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/CopyTradingController.kt index 1d0db10..dfb51fb 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/CopyTradingController.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/CopyTradingController.kt @@ -36,7 +36,6 @@ class CopyTradingController( val result = copyTradingService.createCopyTrading(request) result.fold( onSuccess = { copyTrading -> - logger.info("成功创建跟单: ${copyTrading.id}") ResponseEntity.ok(ApiResponse.success(copyTrading)) }, onFailure = { e -> @@ -88,7 +87,6 @@ class CopyTradingController( val result = copyTradingService.updateCopyTradingStatus(request) result.fold( onSuccess = { copyTrading -> - logger.info("成功更新跟单状态: ${copyTrading.id}, enabled=${copyTrading.enabled}") ResponseEntity.ok(ApiResponse.success(copyTrading)) }, onFailure = { e -> @@ -119,7 +117,6 @@ class CopyTradingController( val result = copyTradingService.deleteCopyTrading(request.copyTradingId) result.fold( onSuccess = { - logger.info("成功删除跟单: ${request.copyTradingId}") ResponseEntity.ok(ApiResponse.success(Unit)) }, onFailure = { e -> diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/CopyTradingStatisticsController.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/CopyTradingStatisticsController.kt index d2f8394..fd1ee29 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/CopyTradingStatisticsController.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/CopyTradingStatisticsController.kt @@ -33,7 +33,6 @@ class CopyTradingStatisticsController( val result = runBlocking { statisticsService.getStatistics(request.copyTradingId) } result.fold( onSuccess = { response -> - logger.info("成功获取统计信息: copyTradingId=${request.copyTradingId}") ResponseEntity.ok(ApiResponse.success(response)) }, onFailure = { e -> @@ -86,7 +85,6 @@ class CopyOrderTrackingController( val result = statisticsService.getOrderList(request) result.fold( onSuccess = { response -> - logger.info("成功查询订单列表: copyTradingId=${request.copyTradingId}, type=${request.type}, total=${response.total}") ResponseEntity.ok(ApiResponse.success(response)) }, onFailure = { e -> diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/CopyTradingTemplateController.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/CopyTradingTemplateController.kt index 2149bdb..e6230c7 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/CopyTradingTemplateController.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/CopyTradingTemplateController.kt @@ -30,7 +30,6 @@ class CopyTradingTemplateController( val result = templateService.createTemplate(request) result.fold( onSuccess = { template -> - logger.info("成功创建模板: ${template.id}") ResponseEntity.ok(ApiResponse.success(template)) }, onFailure = { e -> @@ -60,7 +59,6 @@ class CopyTradingTemplateController( val result = templateService.updateTemplate(request) result.fold( onSuccess = { template -> - logger.info("成功更新模板: ${template.id}") ResponseEntity.ok(ApiResponse.success(template)) }, onFailure = { e -> @@ -90,7 +88,6 @@ class CopyTradingTemplateController( val result = templateService.deleteTemplate(request.templateId) result.fold( onSuccess = { - logger.info("成功删除模板: ${request.templateId}") ResponseEntity.ok(ApiResponse.success(Unit)) }, onFailure = { e -> @@ -124,7 +121,6 @@ class CopyTradingTemplateController( val result = templateService.copyTemplate(request) result.fold( onSuccess = { template -> - logger.info("成功复制模板: ${template.id}") ResponseEntity.ok(ApiResponse.success(template)) }, onFailure = { e -> diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/LeaderController.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/LeaderController.kt index dd83d9d..4695525 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/LeaderController.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/LeaderController.kt @@ -30,7 +30,6 @@ class LeaderController( val result = leaderService.addLeader(request) result.fold( onSuccess = { leader -> - logger.info("成功添加 Leader: ${leader.id}") ResponseEntity.ok(ApiResponse.success(leader)) }, onFailure = { e -> @@ -61,7 +60,6 @@ class LeaderController( val result = leaderService.updateLeader(request) result.fold( onSuccess = { leader -> - logger.info("成功更新 Leader: ${leader.id}") ResponseEntity.ok(ApiResponse.success(leader)) }, onFailure = { e -> @@ -91,7 +89,6 @@ class LeaderController( val result = leaderService.deleteLeader(request.leaderId) result.fold( onSuccess = { - logger.info("成功删除 Leader: ${request.leaderId}") ResponseEntity.ok(ApiResponse.success(Unit)) }, onFailure = { e -> diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/MarketController.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/MarketController.kt index 0587aab..39ca649 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/MarketController.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/MarketController.kt @@ -36,7 +36,6 @@ class MarketController( val result = runBlocking { accountService.getMarketPrice(request.marketId) } result.fold( onSuccess = { response -> - logger.info("成功获取市场价格: 市场=${request.marketId}") ResponseEntity.ok(ApiResponse.success(response)) }, onFailure = { e -> @@ -65,7 +64,6 @@ class MarketController( val result = runBlocking { clobService.getLatestPrice(request.tokenId) } result.fold( onSuccess = { response -> - logger.debug("成功获取最新价: tokenId=${request.tokenId}, bestBid=${response.bestBid}, bestAsk=${response.bestAsk}") ResponseEntity.ok(ApiResponse.success(response)) }, onFailure = { e -> 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 e24b6f4..16989e7 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt @@ -60,7 +60,6 @@ class AccountService( } // 4. 自动获取或创建 API Key(必须成功,否则导入失败) - logger.info("开始自动获取或创建 API Key: ${request.walletAddress}") val apiKeyCreds = runBlocking { val result = apiKeyService.createOrDeriveApiKey( privateKey = request.privateKey, @@ -71,7 +70,6 @@ class AccountService( if (result.isSuccess) { val creds = result.getOrNull() if (creds != null) { - logger.info("成功自动获取 API Key: ${request.walletAddress}") creds } else { logger.error("自动获取 API Key 返回空值") @@ -98,7 +96,6 @@ class AccountService( if (proxyResult.isSuccess) { val address = proxyResult.getOrNull() if (address != null) { - logger.info("成功获取代理地址: ${request.walletAddress} -> $address") address } else { logger.error("获取代理地址返回空值") @@ -127,7 +124,6 @@ class AccountService( ) val saved = accountRepository.save(account) - logger.info("成功导入账户: ${saved.id}, ${saved.walletAddress}, 代理地址: ${saved.proxyAddress}, 启用状态: ${saved.isEnabled}") // 刷新订单推送订阅(如果账户启用且有 API 凭证) orderPushService.refreshSubscriptions() @@ -171,7 +167,6 @@ class AccountService( ) val saved = accountRepository.save(updated) - logger.info("成功更新账户: ${saved.id}, 启用状态: ${saved.isEnabled}") // 刷新订单推送订阅(账户状态变更时) orderPushService.refreshSubscriptions() @@ -212,7 +207,6 @@ class AccountService( } accountRepository.delete(account) - logger.info("成功删除账户: $accountId") // 刷新订单推送订阅(账户删除时) orderPushService.refreshSubscriptions() @@ -373,7 +367,6 @@ class AccountService( val updated = account.copy(isDefault = true, updatedAt = System.currentTimeMillis()) accountRepository.save(updated) - logger.info("成功设置默认账户: $accountId") Result.success(Unit) } catch (e: Exception) { logger.error("设置默认账户失败", e) @@ -807,14 +800,12 @@ class AccountService( account.walletAddress ) - logger.info("创建卖出订单: market=${request.marketId}, side=${request.side}, orderType=${request.orderType}, quantity=${request.quantity}, price=$sellPrice, tokenId=$tokenId") val orderResponse = clobApi.createOrder(newOrderRequest) if (orderResponse.isSuccessful && orderResponse.body() != null) { val response = orderResponse.body()!! if (response.success) { - logger.info("订单创建成功: orderId=${response.orderId}, transactionsHashes=${response.transactionsHashes}") Result.success( PositionSellResponse( orderId = response.orderId ?: "", @@ -1067,7 +1058,6 @@ class AccountService( redeemResult.fold( onSuccess = { txHash -> lastTxHash = txHash - logger.info("账户 $accountId 市场 $marketId 赎回成功: txHash=$txHash, indexSets=$indexSets") }, onFailure = { e -> logger.error("账户 $accountId 市场 $marketId 赎回失败: ${e.message}", e) @@ -1115,7 +1105,6 @@ class AccountService( return try { // 如果账户没有配置 API 凭证,无法查询活跃订单,允许删除 if (account.apiKey == null || account.apiSecret == null || account.apiPassphrase == null) { - logger.debug("账户 ${account.id} 未配置 API 凭证,无法查询活跃订单,允许删除") return false } @@ -1139,7 +1128,6 @@ class AccountService( if (response.isSuccessful && response.body() != null) { val ordersResponse = response.body()!! val hasOrders = ordersResponse.data.isNotEmpty() - logger.debug("账户 ${account.id} 活跃订单检查结果: $hasOrders (订单数: ${ordersResponse.data.size})") hasOrders } else { // 如果查询失败(可能是认证失败或网络问题),记录警告但允许删除 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 235d74b..009409a 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt @@ -129,7 +129,6 @@ class BlockchainService( // 解析代理地址 val proxyAddress = EthereumUtils.decodeAddress(hexResult) - logger.debug("获取代理地址成功: 原始地址=$walletAddress, 代理地址=$proxyAddress") Result.success(proxyAddress) } catch (e: Exception) { logger.error("获取代理地址失败: ${e.message}", e) @@ -158,7 +157,6 @@ class BlockchainService( return Result.failure(IllegalArgumentException("代理地址不能为空")) } - logger.debug("使用代理地址查询余额: $proxyAddress (原始地址: $walletAddress)") // 使用 RPC 查询 USDC 余额(使用代理地址) val balance = queryUsdcBalanceViaRpc(proxyAddress) @@ -235,7 +233,6 @@ class BlockchainService( if (response.isSuccessful && response.body() != null) { val positions = response.body()!! - logger.debug("查询到 ${positions.size} 个仓位") Result.success(positions) } else { val errorMsg = "Data API 请求失败: ${response.code()} ${response.message()}" @@ -388,7 +385,6 @@ class BlockchainService( } else { 0.0 } - logger.debug("查询到仓位总价值: $totalValue") Result.success(totalValue.toString()) } else { val errorMsg = "Data API 请求失败: ${response.code()} ${response.message()}" @@ -452,7 +448,6 @@ class BlockchainService( val credentials = org.web3j.crypto.Credentials.create(privateKeyBigInt.toString(16)) val fromAddress = credentials.address - logger.debug("赎回仓位: from=$fromAddress, proxy=$proxyAddress, conditionId=$conditionId, indexSets=$indexSets") // 1. 构建 ConditionalTokens.redeemPositions 的调用数据 val redeemFunctionSelector = EthereumUtils.getFunctionSelector("redeemPositions(address,bytes32,bytes32,uint256[])") @@ -638,7 +633,6 @@ class BlockchainService( val txHashResult = sendTransaction(rpcApi, transaction) txHashResult.fold( onSuccess = { txHash -> - logger.info("赎回仓位交易已发送: txHash=$txHash, from=$fromAddress, proxy=$proxyAddress, conditionId=$conditionId, indexSets=$indexSets") Result.success(txHash) }, onFailure = { e -> diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyOrderTrackingService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyOrderTrackingService.kt index 1328458..b632e12 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyOrderTrackingService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyOrderTrackingService.kt @@ -50,17 +50,14 @@ class CopyOrderTrackingService( if (existingProcessed != null) { if (existingProcessed.status == "FAILED") { - logger.debug("交易已标记为失败,跳过: leaderId=$leaderId, tradeId=${trade.id}, source=$source") return Result.success(Unit) } - logger.debug("交易已处理,跳过: leaderId=$leaderId, tradeId=${trade.id}, source=$source") return Result.success(Unit) } // 检查是否已记录为失败交易 val failedTrade = failedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id) if (failedTrade != null) { - logger.debug("交易已记录为失败,跳过: leaderId=$leaderId, tradeId=${trade.id}, source=$source") return Result.success(Unit) } @@ -91,17 +88,14 @@ class CopyOrderTrackingService( processedAt = System.currentTimeMillis() ) processedTradeRepository.save(processed) - logger.info("成功处理交易: leaderId=$leaderId, tradeId=${trade.id}, source=$source, side=${trade.side}") } catch (e: DataIntegrityViolationException) { // 唯一约束冲突,说明已经处理过了(可能是并发请求) // 再次检查确认状态 val existing = processedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id) if (existing != null) { if (existing.status == "FAILED") { - logger.debug("交易已标记为失败(并发检测): leaderId=$leaderId, tradeId=${trade.id}") return Result.success(Unit) } - logger.debug("交易已处理(并发检测): leaderId=$leaderId, tradeId=${trade.id}, source=$source") return Result.success(Unit) } else { // 如果检查不到,说明可能是其他约束冲突,重新抛出异常 @@ -128,7 +122,6 @@ class CopyOrderTrackingService( val copyTradings = copyTradingRepository.findByLeaderIdAndEnabledTrue(leaderId) if (copyTradings.isEmpty()) { - logger.debug("没有启用的跟单关系: leaderId=$leaderId") return Result.success(Unit) } @@ -151,7 +144,6 @@ class CopyOrderTrackingService( // 验证账户是否启用 if (!account.isEnabled) { - logger.debug("账户未启用,跳过创建订单: accountId=${account.id}") continue } @@ -273,7 +265,6 @@ class CopyOrderTrackingService( ) copyOrderTrackingRepository.save(tracking) - logger.info("成功创建买入订单并记录跟踪: copyTradingId=${copyTrading.id}, orderId=$realOrderId, tradeId=${trade.id}, quantity=$finalBuyQuantity, price=$buyPrice") } catch (e: Exception) { logger.error("处理买入交易失败: copyTradingId=${copyTrading.id}, tradeId=${trade.id}", e) // 继续处理下一个跟单关系 @@ -298,7 +289,6 @@ class CopyOrderTrackingService( val copyTradings = copyTradingRepository.findByLeaderIdAndEnabledTrue(leaderId) if (copyTradings.isEmpty()) { - logger.debug("没有启用的跟单关系: leaderId=$leaderId") return Result.success(Unit) } @@ -311,7 +301,6 @@ class CopyOrderTrackingService( // 检查是否支持卖出 if (!template.supportSell) { - logger.debug("模板不支持卖出,跳过: copyTradingId=${copyTrading.id}, templateId=${template.id}") continue } @@ -377,7 +366,6 @@ class CopyOrderTrackingService( // 验证账户是否启用 if (!account.isEnabled) { - logger.debug("账户未启用,跳过创建卖出订单: accountId=${account.id}") return } @@ -399,7 +387,6 @@ class CopyOrderTrackingService( ) if (unmatchedOrders.isEmpty()) { - logger.debug("没有未匹配的买入订单: copyTradingId=${copyTrading.id}, market=${leaderSellTrade.market}, outcomeIndex=${leaderSellTrade.outcomeIndex}") return } @@ -440,7 +427,6 @@ class CopyOrderTrackingService( } if (totalMatched.lte(BigDecimal.ZERO)) { - logger.debug("没有匹配到任何订单: copyTradingId=${copyTrading.id}, needMatch=$needMatch") return } @@ -533,7 +519,6 @@ class CopyOrderTrackingService( order.updatedAt = System.currentTimeMillis() copyOrderTrackingRepository.save(order) - logger.info("匹配买入订单: copyTradingId=${copyTrading.id}, buyOrderId=${order.buyOrderId}, matchQty=${detail.matchedQuantity}, pnl=${detail.realizedPnl}") } } @@ -560,7 +545,6 @@ class CopyOrderTrackingService( sellMatchDetailRepository.save(savedDetail) } - logger.info("完成卖出匹配并创建订单: copyTradingId=${copyTrading.id}, sellOrderId=$realSellOrderId, totalMatched=$totalMatched, totalPnl=$totalRealizedPnl") } /** diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingMonitorService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingMonitorService.kt index 22c6508..a65815d 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingMonitorService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingMonitorService.kt @@ -31,7 +31,6 @@ class CopyTradingMonitorService( */ @PostConstruct fun init() { - logger.info("跟单监听服务初始化...") scope.launch { try { startMonitoring() @@ -46,7 +45,6 @@ class CopyTradingMonitorService( */ @PreDestroy fun destroy() { - logger.info("停止跟单监听服务...") scope.cancel() // 只使用轮询,不使用WebSocket pollingService.stop() @@ -60,7 +58,6 @@ class CopyTradingMonitorService( val enabledCopyTradings = copyTradingRepository.findByEnabledTrue() if (enabledCopyTradings.isEmpty()) { - logger.info("没有启用的跟单关系,等待添加...") return } @@ -70,7 +67,6 @@ class CopyTradingMonitorService( leaderRepository.findById(leaderId).orElse(null) } - logger.info("开始监听 ${leaders.size} 个Leader的交易: ${leaders.map { it.leaderAddress }}") // 3. 启动轮询监听(使用 /activity 接口,不需要认证) // 注意:WebSocket 需要认证才能订阅其他用户的交易,因此禁用WebSocket,只使用轮询 @@ -86,11 +82,9 @@ class CopyTradingMonitorService( val copyTradings = copyTradingRepository.findByLeaderIdAndEnabledTrue(leaderId) if (copyTradings.isEmpty()) { - logger.debug("Leader $leaderId 没有启用的跟单关系,不启动监听") return } - logger.info("添加Leader监听: ${leader.leaderAddress}") // 只使用轮询,不使用WebSocket(需要认证) pollingService.addLeader(leader) } @@ -101,11 +95,9 @@ class CopyTradingMonitorService( suspend fun removeLeaderMonitoring(leaderId: Long) { val copyTradings = copyTradingRepository.findByLeaderIdAndEnabledTrue(leaderId) if (copyTradings.isNotEmpty()) { - logger.debug("Leader $leaderId 仍有启用的跟单关系,不停止监听") return } - logger.info("移除Leader监听: leaderId=$leaderId") // 只使用轮询,不使用WebSocket pollingService.removeLeader(leaderId) } @@ -114,7 +106,6 @@ class CopyTradingMonitorService( * 重新启动监听(当跟单关系状态改变时调用) */ suspend fun restartMonitoring() { - logger.info("重新启动跟单监听...") // 只使用轮询,不使用WebSocket pollingService.stop() delay(1000) // 等待1秒 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingPollingService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingPollingService.kt index d1339b2..099d54d 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingPollingService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingPollingService.kt @@ -52,12 +52,9 @@ class CopyTradingPollingService( */ fun start(leaders: List) { if (!pollingEnabled) { - logger.info("轮询监听已禁用,跳过启动") return } - logger.info("启动轮询监听,Leader数量: ${leaders.size},轮询间隔: ${pollingInterval}ms") - leaders.forEach { leader -> addLeader(leader) } @@ -81,7 +78,6 @@ class CopyTradingPollingService( cachedTradeIds[leaderId] = mutableSetOf() // 首次轮询标志,用于缓存数据而不处理 isFirstPoll[leaderId] = true - logger.info("添加轮询监听: leaderId=$leaderId, address=${leader.leaderAddress}, 首次轮询将只缓存数据") } /** @@ -91,14 +87,12 @@ class CopyTradingPollingService( monitoredLeaders.remove(leaderId) cachedTradeIds.remove(leaderId) isFirstPoll.remove(leaderId) - logger.info("移除轮询监听: leaderId=$leaderId") } /** * 停止所有监听 */ fun stop() { - logger.info("停止所有轮询监听...") stopPolling() monitoredLeaders.clear() cachedTradeIds.clear() @@ -110,17 +104,14 @@ class CopyTradingPollingService( */ private fun startPolling() { if (pollingJob != null && pollingJob!!.isActive) { - logger.debug("轮询任务已在运行") return } if (monitoredLeaders.isEmpty()) { - logger.debug("没有需要监听的Leader,不启动轮询") return } pollingJob = scope.launch { - logger.info("轮询任务已启动,间隔: ${pollingInterval}ms") while (isActive) { try { @@ -235,7 +226,6 @@ class CopyTradingPollingService( cachedIds.addAll(tradeIds) cachedTradeIds[leaderId] = cachedIds - logger.info("首次轮询,缓存 ${allTrades.size} 笔交易数据,不进行处理: leaderId=$leaderId") // 标记首次轮询完成 isFirstPoll[leaderId] = false } else { @@ -247,7 +237,6 @@ class CopyTradingPollingService( // 找出新增的交易 val incrementalTrades = allTrades.filter { it.id in incrementalTradeIds } - logger.debug("通过 diff 发现 ${incrementalTrades.size} 笔新增交易: leaderId=$leaderId") // 处理新增的交易 incrementalTrades.forEach { trade -> @@ -263,9 +252,7 @@ class CopyTradingPollingService( cachedIds.addAll(incrementalTradeIds) cachedTradeIds[leaderId] = cachedIds - logger.debug("已更新缓存,当前缓存 ${cachedIds.size} 笔交易ID: leaderId=$leaderId") } else { - logger.debug("未发现新增交易: leaderId=$leaderId") } // 限制缓存大小,避免内存溢出(只保留最近1000条) @@ -273,7 +260,6 @@ class CopyTradingPollingService( // 保留最新的1000条(由于查询是按时间戳降序,保留前1000条即可) val sortedTradeIds = allTrades.map { it.id }.take(1000).toSet() cachedTradeIds[leaderId] = sortedTradeIds.toMutableSet() - logger.debug("缓存已满,清理到1000条: leaderId=$leaderId") } } } catch (e: Exception) { diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingService.kt index ce33df9..a7da076 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingService.kt @@ -61,7 +61,6 @@ class CopyTradingService( ) val saved = copyTradingRepository.save(copyTrading) - logger.info("成功创建跟单: ${saved.id}, account=${request.accountId}, template=${request.templateId}, leader=${request.leaderId}") // 如果跟单已启用,启动Leader监听 if (saved.enabled) { @@ -162,7 +161,6 @@ class CopyTradingService( ) val saved = copyTradingRepository.save(updated) - logger.info("成功更新跟单状态: ${saved.id}, enabled=${saved.enabled}") // 更新监听状态 kotlinx.coroutines.runBlocking { @@ -203,7 +201,6 @@ class CopyTradingService( val leaderId = copyTrading.leaderId copyTradingRepository.delete(copyTrading) - logger.info("成功删除跟单: $copyTradingId") // 移除监听(如果该Leader没有其他启用的跟单关系) kotlinx.coroutines.runBlocking { diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingTemplateService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingTemplateService.kt index 9d63a2e..84bc938 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingTemplateService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingTemplateService.kt @@ -62,7 +62,6 @@ class CopyTradingTemplateService( ) val saved = templateRepository.save(template) - logger.info("成功创建模板: ${saved.id}, ${saved.templateName}") Result.success(toDto(saved)) } catch (e: Exception) { @@ -118,7 +117,6 @@ class CopyTradingTemplateService( ) val saved = templateRepository.save(updated) - logger.info("成功更新模板: ${saved.id}") Result.success(toDto(saved)) } catch (e: Exception) { @@ -143,7 +141,6 @@ class CopyTradingTemplateService( } templateRepository.delete(template) - logger.info("成功删除模板: $templateId") Result.success(Unit) } catch (e: Exception) { @@ -186,7 +183,6 @@ class CopyTradingTemplateService( ) val saved = templateRepository.save(newTemplate) - logger.info("成功复制模板: ${sourceTemplate.id} -> ${saved.id}") Result.success(toDto(saved)) } catch (e: Exception) { diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingWebSocketService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingWebSocketService.kt index ed8f974..ec8913d 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingWebSocketService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyTradingWebSocketService.kt @@ -42,7 +42,6 @@ class CopyTradingWebSocketService( * 启动WebSocket监听 */ fun start(leaders: List) { - logger.info("启动WebSocket监听,Leader数量: ${leaders.size}") leaders.forEach { leader -> try { @@ -63,7 +62,6 @@ class CopyTradingWebSocketService( } if (leaderClients.containsKey(leader.id)) { - logger.debug("Leader ${leader.id} 已经在监听中,跳过") return } @@ -98,7 +96,6 @@ class CopyTradingWebSocketService( scope.launch { try { client.connect() - logger.info("已启动WebSocket监听: leaderId=$leaderId, address=$leaderAddress") } catch (e: Exception) { logger.error("连接WebSocket失败: leaderId=$leaderId", e) leaderClients.remove(leaderId) @@ -117,7 +114,6 @@ class CopyTradingWebSocketService( if (client != null) { try { client.closeConnection() - logger.info("已停止WebSocket监听: leaderId=$leaderId") } catch (e: Exception) { logger.error("关闭WebSocket连接失败: leaderId=$leaderId", e) } @@ -128,7 +124,6 @@ class CopyTradingWebSocketService( * 停止所有监听 */ fun stop() { - logger.info("停止所有WebSocket监听...") val leaderIds = leaderClients.keys.toList() leaderIds.forEach { leaderId -> removeLeader(leaderId) @@ -150,7 +145,6 @@ class CopyTradingWebSocketService( """.trimIndent() client.sendMessage(subscribeMessage) - logger.info("已订阅用户交易频道: $userAddress") } catch (e: Exception) { logger.error("订阅用户交易频道失败: $userAddress", e) } @@ -163,7 +157,6 @@ class CopyTradingWebSocketService( try { // 处理PONG响应 if (message.trim() == "PONG") { - logger.debug("收到PONG响应: leaderId=$leaderId") return } @@ -173,7 +166,6 @@ class CopyTradingWebSocketService( // 检查消息类型 val eventType = json.get("event_type")?.asString if (eventType != "trade") { - logger.debug("忽略非交易事件: leaderId=$leaderId, eventType=$eventType") return } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/LeaderService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/LeaderService.kt index e87a193..37e84de 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/LeaderService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/LeaderService.kt @@ -56,7 +56,6 @@ class LeaderService( ) val saved = leaderRepository.save(leader) - logger.info("成功添加 Leader: ${saved.id}, ${saved.leaderAddress}") Result.success(toDto(saved)) } catch (e: Exception) { @@ -86,7 +85,6 @@ class LeaderService( ) val saved = leaderRepository.save(updated) - logger.info("成功更新 Leader: ${saved.id}") Result.success(toDto(saved)) } catch (e: Exception) { @@ -111,7 +109,6 @@ class LeaderService( } leaderRepository.delete(leader) - logger.info("成功删除 Leader: $leaderId") Result.success(Unit) } catch (e: Exception) { diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/OrderPushService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/OrderPushService.kt index bae6a76..c9e0caa 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/OrderPushService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/OrderPushService.kt @@ -48,7 +48,6 @@ class OrderPushService( */ @PostConstruct fun init() { - logger.info("订单推送服务已初始化") scope.launch { connectAllAccounts() } @@ -59,7 +58,6 @@ class OrderPushService( */ @PreDestroy fun destroy() { - logger.info("停止订单推送服务") accountConnections.values.forEach { client -> try { if (client.isConnected()) { @@ -90,7 +88,6 @@ class OrderPushService( * 订阅所有启用的账户 */ fun subscribeAllEnabled(callback: (OrderPushMessage) -> Unit) { - logger.info("订阅所有启用账户的订单推送") val accounts = accountRepository.findAll() accounts.forEach { account -> if (hasApiCredentials(account) && account.isEnabled) { @@ -166,12 +163,10 @@ class OrderPushService( } if (!account.isEnabled) { - logger.debug("账户 ${account.id} 未启用,跳过连接") return } if (accountConnections.containsKey(account.id)) { - logger.debug("账户 ${account.id} 已存在连接,跳过") return } @@ -193,7 +188,6 @@ class OrderPushService( if (currentClient != null) { try { sendSubscribeMessage(currentClient, account) - logger.info("已为账户 ${account.id} (${account.accountName ?: account.walletAddress}) 建立 User Channel 连接并发送订阅消息") } catch (e: Exception) { logger.error("发送订阅消息失败: account=${account.id}, ${e.message}", e) // 如果订阅失败,关闭连接(会触发重连) @@ -210,7 +204,6 @@ class OrderPushService( if (currentClient != null) { try { sendSubscribeMessage(currentClient, account) - logger.info("账户 ${account.id} 重连成功,已重新发送订阅消息") } catch (e: Exception) { logger.error("重连后发送订阅消息失败: account=${account.id}, ${e.message}", e) } @@ -257,7 +250,6 @@ class OrderPushService( val json = objectMapper.writeValueAsString(subscribeMessage) client.sendMessage(json) - logger.info("已发送 User Channel 订阅消息: account=${account.id}, apiKey=${account.apiKey?.take(10)}...") } catch (e: Exception) { logger.error("发送订阅消息失败: account=${account.id}, ${e.message}", e) } @@ -270,7 +262,6 @@ class OrderPushService( try { // 处理心跳响应(PONG),直接返回 if (message.trim() == "PONG" || message.trim() == "pong") { - logger.debug("收到 PONG 响应: account=${account.id}") return } @@ -304,12 +295,10 @@ class OrderPushService( } } else { // 记录其他类型的消息(用于调试) - logger.debug("收到非订单消息: account=${account.id}, eventType=$eventType") } } catch (e: Exception) { // 如果解析失败,可能是非 JSON 消息(如 PONG),记录为 debug 级别 if (message.trim() == "PONG" || message.trim() == "pong") { - logger.debug("收到 PONG 响应: account=${account.id}") } else { logger.error("处理订单消息失败: account=${account.id}, message=${message.take(100)}, ${e.message}", e) } @@ -328,7 +317,6 @@ class OrderPushService( return try { // 检查账户是否有 API 凭证 if (account.apiKey == null || account.apiSecret == null || account.apiPassphrase == null) { - logger.debug("账户 ${account.id} 未配置 API 凭证,无法获取订单详情") return null } @@ -395,18 +383,14 @@ class OrderPushService( val markets = response.body()!! if (markets.isNotEmpty()) { val market = markets.first() - logger.debug("获取市场信息成功: conditionId=$conditionId, question=${market.question}") return market } else { - logger.debug("未找到市场信息: conditionId=$conditionId") return null } } else { - logger.debug("获取市场信息失败: conditionId=$conditionId, code=${response.code()}, message=${response.message()}") null } } catch (e: Exception) { - logger.debug("获取市场信息异常: conditionId=$conditionId, ${e.message}") null } } @@ -415,7 +399,6 @@ class OrderPushService( * 订阅账户的订单推送(保留用于向后兼容) */ fun subscribe(accountId: Long, callback: (OrderPushMessage) -> Unit) { - logger.info("订阅账户订单推送: $accountId") accountCallbacks.getOrPut(accountId) { mutableSetOf() }.add(callback) // 如果账户连接不存在,尝试建立连接 @@ -431,7 +414,6 @@ class OrderPushService( * 取消订阅账户的订单推送(保留用于向后兼容) */ fun unsubscribe(accountId: Long, callback: (OrderPushMessage) -> Unit) { - logger.info("取消订阅账户订单推送: $accountId") accountCallbacks[accountId]?.remove(callback) // 如果没有订阅者了,可以考虑关闭连接(但暂时保持连接,以便后续订阅) @@ -441,7 +423,6 @@ class OrderPushService( * 断开指定账户的连接 */ fun disconnectAccount(accountId: Long) { - logger.info("断开账户连接: $accountId") val client = accountConnections.remove(accountId) client?.let { try { 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 21f87aa..697deeb 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PolymarketClobService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PolymarketClobService.kt @@ -161,7 +161,6 @@ class PolymarketClobService( adjustedPrice > BigDecimal("0.99") -> BigDecimal("0.99") else -> adjustedPrice } - logger.debug("从订单表获取最优价(卖单): tokenId=$tokenId, bestBid=$bestBid, adjustedPrice=${finalPrice.toPlainString()}") return finalPrice.toPlainString() } else { // 市价买单:需要 bestAsk(最低卖出价) @@ -186,7 +185,6 @@ class PolymarketClobService( adjustedPrice > BigDecimal("0.99") -> BigDecimal("0.99") else -> adjustedPrice } - logger.debug("从订单表获取最优价(买单): tokenId=$tokenId, bestAsk=$bestAsk, adjustedPrice=${finalPrice.toPlainString()}") return finalPrice.toPlainString() } } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PositionPushService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PositionPushService.kt index 57cca5e..091af10 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PositionPushService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PositionPushService.kt @@ -45,7 +45,6 @@ class PositionPushService( */ @PostConstruct fun init() { - logger.info("仓位推送服务已初始化,轮询间隔: ${pollingInterval}ms,等待客户端连接...") } /** @@ -53,7 +52,6 @@ class PositionPushService( */ @PreDestroy fun destroy() { - logger.info("停止仓位推送服务") synchronized(lock) { pollingJob?.cancel() pollingJob = null @@ -65,7 +63,6 @@ class PositionPushService( * 订阅仓位推送(新接口) */ fun subscribe(sessionId: String, callback: (PositionPushMessage) -> Unit) { - logger.info("订阅仓位推送: $sessionId") registerSession(sessionId, callback) } @@ -73,7 +70,6 @@ class PositionPushService( * 取消订阅仓位推送(新接口) */ fun unsubscribe(sessionId: String) { - logger.info("取消订阅仓位推送: $sessionId") unregisterSession(sessionId) } @@ -136,7 +132,6 @@ class PositionPushService( // 发送给指定客户端 clientCallbacks[sessionId]?.invoke(message) - logger.debug("已发送全量仓位数据给客户端: $sessionId") } } else { logger.warn("获取仓位数据失败,无法发送全量数据: ${result.exceptionOrNull()?.message}") @@ -156,7 +151,6 @@ class PositionPushService( // 启动新的轮询任务 pollingJob = scope.launch { - logger.info("轮询任务已启动,间隔: ${pollingInterval}ms") while (isActive) { try { pollAndPush() @@ -176,7 +170,6 @@ class PositionPushService( synchronized(lock) { pollingJob?.cancel() pollingJob = null - logger.info("轮询任务已停止") } } @@ -186,7 +179,6 @@ class PositionPushService( private suspend fun pollAndPush() { // 双重检查:如果没有客户端连接,跳过轮询(虽然理论上不应该发生,但作为安全措施) if (clientCallbacks.isEmpty()) { - logger.debug("没有客户端连接,跳过本次轮询") return } @@ -220,7 +212,6 @@ class PositionPushService( } } - logger.debug("已推送仓位增量更新,当前仓位变化: ${incremental.currentPositions.size}, 历史仓位变化: ${incremental.historyPositions.size}, 删除: ${incremental.removedKeys.size}") } // 更新快照 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/WebSocketSubscriptionService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/WebSocketSubscriptionService.kt index 6018897..3013852 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/WebSocketSubscriptionService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/WebSocketSubscriptionService.kt @@ -40,7 +40,6 @@ class WebSocketSubscriptionService( * 注册会话 */ fun registerSession(sessionId: String, callback: (WsMessage) -> Unit) { - logger.info("注册 WebSocket 会话: $sessionId") sessionCallbacks[sessionId] = callback sessionSubscriptions[sessionId] = mutableSetOf() } @@ -49,7 +48,6 @@ class WebSocketSubscriptionService( * 注销会话 */ fun unregisterSession(sessionId: String) { - logger.info("注销 WebSocket 会话: $sessionId") // 取消所有订阅 val channels = sessionSubscriptions.remove(sessionId) ?: emptySet() @@ -67,12 +65,10 @@ class WebSocketSubscriptionService( * 订阅频道 */ fun subscribe(sessionId: String, channel: String, payload: Map<*, *>?) { - logger.info("订阅频道: $sessionId -> $channel") // 检查是否已经订阅 val sessionChannels = sessionSubscriptions.getOrPut(sessionId) { mutableSetOf() } if (sessionChannels.contains(channel)) { - logger.debug("会话 $sessionId 已经订阅了频道 $channel,跳过重复订阅") sendSubscribeAck(sessionId, channel, true) return } @@ -94,7 +90,6 @@ class WebSocketSubscriptionService( scope.launch { try { positionPushService.sendFullData(sessionId) - logger.info("已发送仓位首推数据给会话: $sessionId") } catch (e: Exception) { logger.error("发送仓位首推数据失败: $sessionId, ${e.message}", e) } @@ -107,7 +102,6 @@ class WebSocketSubscriptionService( } orderChannelCallbacks[sessionId] = callback orderPushService.subscribeAllEnabled(callback) - logger.info("已订阅所有启用账户的订单推送: $sessionId") } else -> { logger.warn("未知的频道: $channel") @@ -120,7 +114,6 @@ class WebSocketSubscriptionService( * 取消订阅 */ fun unsubscribe(sessionId: String, channel: String) { - logger.info("取消订阅频道: $sessionId -> $channel") // 移除订阅关系 sessionSubscriptions[sessionId]?.remove(channel) @@ -134,7 +127,6 @@ class WebSocketSubscriptionService( val callback = orderChannelCallbacks.remove(sessionId) if (callback != null) { orderPushService.unsubscribeAll(callback) - logger.debug("已取消订阅订单推送: $sessionId -> $channel") } } } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/PolymarketAuthInterceptor.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/PolymarketAuthInterceptor.kt index 4e2ab3b..09497c3 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/PolymarketAuthInterceptor.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/PolymarketAuthInterceptor.kt @@ -74,9 +74,6 @@ class PolymarketAuthInterceptor( val signature = generateSignature(signString, apiSecret) // 调试日志(仅在 DEBUG 级别输出) - logger.debug("L2 认证签名生成: method=$method, path=$requestPath, bodyLength=${bodyString?.length ?: 0}, timestamp=$timestamp") - logger.debug("签名字符串: $signString") - logger.debug("签名结果: ${signature.take(20)}...") // 重新创建请求体(如果原始请求有请求体) val newRequestBody = originalRequest.body?.let { requestBody -> diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt index 8ea5c5c..124310d 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt @@ -196,7 +196,6 @@ class ResponseLoggingInterceptor : Interceptor { ) } } catch (e: Exception) { - logger.debug("读取响应体失败: ${e.message}") } } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/PolymarketWebSocketClient.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/PolymarketWebSocketClient.kt index 72f6f4f..359f1b6 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/PolymarketWebSocketClient.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/PolymarketWebSocketClient.kt @@ -39,7 +39,6 @@ class PolymarketWebSocketClient( // 如果启用了代理,配置代理 if (proxy != null) { builder.proxy(proxy) - logger.info("已配置 WebSocket 代理: ${proxy.address()}") } builder.build() @@ -50,7 +49,6 @@ class PolymarketWebSocketClient( */ fun connect() { if (webSocket != null && isConnected) { - logger.debug("WebSocket 已连接: $sessionId") return } @@ -61,7 +59,6 @@ class PolymarketWebSocketClient( webSocket = okHttpClient.newWebSocket(request, object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: okhttp3.Response) { - logger.info("已成功连接到 Polymarket RTDS: $sessionId") isConnected = true // 重置重连延迟(连接成功后重置为初始值) @@ -85,17 +82,14 @@ class PolymarketWebSocketClient( } override fun onMessage(webSocket: WebSocket, text: String) { - logger.debug("收到 Polymarket 消息: $sessionId, $text") onMessage(text) } override fun onMessage(webSocket: WebSocket, bytes: ByteString) { - logger.debug("收到 Polymarket 二进制消息: $sessionId") onMessage(bytes.utf8()) } override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { - logger.info("Polymarket 连接正在关闭: $sessionId, code: $code, reason: $reason") isConnected = false stopPing() // 如果不是正常关闭(code != 1000),尝试重连 @@ -105,7 +99,6 @@ class PolymarketWebSocketClient( } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { - logger.info("Polymarket 连接已关闭: $sessionId, code: $code, reason: $reason") isConnected = false stopPing() // 如果不是正常关闭(code != 1000),尝试重连 @@ -138,7 +131,6 @@ class PolymarketWebSocketClient( } }) - logger.info("正在连接到 Polymarket RTDS: $sessionId, URL: $url") } catch (e: Exception) { logger.error("创建 WebSocket 连接失败: $sessionId, ${e.message}", e) throw e @@ -158,7 +150,6 @@ class PolymarketWebSocketClient( if (isConnected) { try { sendMessage("PING") - logger.debug("已发送 PING: $sessionId") } catch (e: Exception) { logger.warn("发送 PING 失败: $sessionId, ${e.message}") break @@ -192,17 +183,14 @@ class PolymarketWebSocketClient( // 检查是否应该重连 if (!shouldReconnect) { - logger.info("重连已禁用,停止重连: $sessionId") return@launch } // 如果已经连接,不需要重连 if (isConnected) { - logger.debug("连接已恢复,取消重连: $sessionId") return@launch } - logger.info("尝试重连 Polymarket WebSocket: $sessionId, 延迟: ${reconnectDelay}ms") // 清理旧的连接 webSocket = null @@ -239,7 +227,6 @@ class PolymarketWebSocketClient( webSocket?.close(1000, "正常关闭") webSocket = null isConnected = false - logger.info("已关闭 WebSocket 连接: $sessionId") } catch (e: Exception) { logger.error("关闭连接失败: $sessionId, ${e.message}", e) } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/PolymarketWebSocketHandler.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/PolymarketWebSocketHandler.kt index 55065de..48284f3 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/PolymarketWebSocketHandler.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/PolymarketWebSocketHandler.kt @@ -23,7 +23,6 @@ class PolymarketWebSocketHandler : WebSocketHandler { private val polymarketConnections = ConcurrentHashMap() override fun afterConnectionEstablished(session: WebSocketSession) { - logger.info("客户端连接建立: ${session.id}") clientSessions[session.id] = session try { @@ -42,7 +41,6 @@ class PolymarketWebSocketHandler : WebSocketHandler { // 异步连接,不阻塞 try { polymarketClient.connect() - logger.info("正在连接到 Polymarket RTDS: ${session.id}") } catch (e: Exception) { logger.error("启动 Polymarket 连接失败: ${e.message}", e) // 连接失败时清理资源 @@ -64,7 +62,6 @@ class PolymarketWebSocketHandler : WebSocketHandler { } override fun handleMessage(session: WebSocketSession, message: WebSocketMessage<*>) { - logger.debug("收到客户端消息: ${session.id}, ${message.payload}") val polymarketClient = polymarketConnections[session.id] if (polymarketClient != null) { @@ -89,7 +86,6 @@ class PolymarketWebSocketHandler : WebSocketHandler { } override fun afterConnectionClosed(session: WebSocketSession, closeStatus: CloseStatus) { - logger.info("客户端连接关闭: ${session.id}, 状态: $closeStatus") cleanup(session.id) } @@ -132,7 +128,6 @@ class PolymarketWebSocketHandler : WebSocketHandler { // 移除客户端会话 clientSessions.remove(sessionId) - logger.debug("已清理资源: $sessionId") } catch (e: Exception) { logger.error("清理资源时发生错误: ${sessionId}, ${e.message}", e) } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/UnifiedWebSocketHandler.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/UnifiedWebSocketHandler.kt index cc36633..acad766 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/UnifiedWebSocketHandler.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/websocket/UnifiedWebSocketHandler.kt @@ -40,19 +40,16 @@ class UnifiedWebSocketHandler( @PostConstruct fun init() { - logger.info("统一 WebSocket 处理器已初始化,心跳超时: ${heartbeatTimeout}ms") startCleanupTask() } @PreDestroy fun destroy() { - logger.info("停止统一 WebSocket 处理器") cleanupJob?.cancel() scope.cancel() } override fun afterConnectionEstablished(session: WebSocketSession) { - logger.info("WebSocket 客户端连接建立: ${session.id}") clientSessions[session.id] = session lastActivityTime[session.id] = System.currentTimeMillis() @@ -70,7 +67,6 @@ class UnifiedWebSocketHandler( lastActivityTime[session.id] = System.currentTimeMillis() try { session.sendMessage(TextMessage("PONG")) - logger.debug("收到心跳并响应: ${session.id}") } catch (e: Exception) { logger.error("发送心跳响应失败: ${session.id}, ${e.message}", e) } @@ -127,7 +123,6 @@ class UnifiedWebSocketHandler( } override fun afterConnectionClosed(session: WebSocketSession, closeStatus: CloseStatus) { - logger.info("WebSocket 客户端连接关闭: ${session.id}, 状态: $closeStatus") cleanup(session.id) } @@ -166,11 +161,9 @@ class UnifiedWebSocketHandler( try { session.close(CloseStatus.NORMAL) } catch (e: Exception) { - logger.debug("关闭会话失败: $sessionId, ${e.message}") } } - logger.info("已清理 WebSocket 资源: $sessionId") } catch (e: Exception) { logger.error("清理 WebSocket 资源时发生错误: $sessionId, ${e.message}", e) } @@ -212,7 +205,6 @@ class UnifiedWebSocketHandler( } if (inactiveSessions.isNotEmpty()) { - logger.info("已清理 ${inactiveSessions.size} 个不活跃连接") } } } diff --git a/docs/copy-trading-frontend-requirements.md b/docs/copy-trading-frontend-requirements.md new file mode 100644 index 0000000..c522fb6 --- /dev/null +++ b/docs/copy-trading-frontend-requirements.md @@ -0,0 +1,355 @@ +# 跟单系统前端需求文档 + +## 1. 页面概述 + +基于订单跟踪与统计设计,前端需要实现以下页面和功能: +- 跟单关系统计页面 +- 买入订单列表页面 +- 卖出订单列表页面 +- 匹配关系列表页面 + +## 2. 跟单关系统计页面 + +### 2.1 页面路径 +`/copy-trading/statistics/:copyTradingId` + +### 2.2 显示内容 + +#### 2.2.1 基本信息卡片 +- 账户名称 +- Leader 名称 +- 模板名称 +- 跟单状态(启用/禁用) + +#### 2.2.2 买入统计卡片 +- **总买入数量**:所有买入订单的数量总和 +- **总买入金额**:所有买入订单的金额总和(数量 × 价格) +- **总买入订单数**:买入订单的数量 +- **平均买入价格**:总买入金额 / 总买入数量 + +#### 2.2.3 卖出统计卡片 +- **总卖出数量**:所有卖出订单的数量总和 +- **总卖出金额**:所有卖出订单的金额总和 +- **总卖出订单数**:卖出订单的数量 + +#### 2.2.4 持仓统计卡片 +- **当前持仓数量**:未匹配的买入数量总和 +- **当前持仓价值**:当前持仓数量 × 当前市场价格 +- **平均买入价格**:已买入订单的平均价格 + +#### 2.2.5 盈亏统计卡片 +- **总已实现盈亏**:所有已匹配订单的盈亏总和 + - 颜色:盈利绿色,亏损红色 + - 图标:盈利↑,亏损↓ +- **总未实现盈亏**:当前持仓的盈亏(持仓数量 × (当前价格 - 平均买入价格)) + - 颜色:盈利绿色,亏损红色 +- **总盈亏**:已实现盈亏 + 未实现盈亏 + - 颜色:盈利绿色,亏损红色 + - 图标:盈利↑,亏损↓ +- **总盈亏百分比**:总盈亏 / 总买入金额 × 100% + - 颜色:盈利绿色,亏损红色 + +### 2.3 UI 布局 + +**桌面端**: +- 使用 `Row` 和 `Col` 布局,每行 3-4 个统计卡片 +- 卡片使用 `Statistic` 组件显示数据 + +**移动端**: +- 每行 1-2 个统计卡片 +- 卡片内容简化,重要数据突出显示 + +### 2.4 数据格式化 + +- **数量**:使用 `formatUSDC` 格式化(最多 4 位小数,自动去除尾随零) +- **金额**:使用 `formatUSDC` 格式化,后缀 "USDC" +- **百分比**:显示 2 位小数,后缀 "%" +- **价格**:使用 `formatUSDC` 格式化 + +## 3. 买入订单列表页面 + +### 3.1 页面路径 +`/copy-trading/orders/buy/:copyTradingId` + +### 3.2 表格列 + +| 列名 | 字段 | 说明 | +|------|------|------| +| 订单ID | buyOrderId | 跟单买入订单ID(可点击查看详情) | +| Leader 交易ID | leaderBuyTradeId | Leader 的买入交易ID | +| 市场 | marketId | 市场地址(可点击查看市场详情) | +| 方向 | side | YES/NO 标签 | +| 买入数量 | quantity | 使用 formatUSDC 格式化 | +| 买入价格 | price | 使用 formatUSDC 格式化 | +| 买入金额 | amount | quantity × price,使用 formatUSDC 格式化 | +| 已匹配数量 | matchedQuantity | 已匹配的卖出数量,使用 formatUSDC 格式化 | +| 剩余数量 | remainingQuantity | 未匹配的数量,使用 formatUSDC 格式化 | +| 订单状态 | status | 标签显示:filled(已完成)、partially_matched(部分匹配)、fully_matched(完全匹配) | +| 创建时间 | createdAt | 时间戳转换为可读格式 | + +### 3.3 状态标签颜色 + +- `filled`:蓝色(processing) +- `partially_matched`:橙色(warning) +- `fully_matched`:绿色(success) + +### 3.4 功能 + +- **分页**:支持分页查询 +- **排序**:默认按创建时间倒序 +- **筛选**:可按市场、方向、状态筛选 +- **详情**:点击订单ID查看详情(可选) + +## 4. 卖出订单列表页面 + +### 4.1 页面路径 +`/copy-trading/orders/sell/:copyTradingId` + +### 4.2 表格列 + +| 列名 | 字段 | 说明 | +|------|------|------| +| 订单ID | sellOrderId | 跟单卖出订单ID(可点击查看详情) | +| Leader 交易ID | leaderSellTradeId | Leader 的卖出交易ID | +| 市场 | marketId | 市场地址(可点击查看市场详情) | +| 方向 | side | YES/NO 标签 | +| 卖出数量 | quantity | 使用 formatUSDC 格式化 | +| 卖出价格 | price | 使用 formatUSDC 格式化 | +| 卖出金额 | amount | quantity × price,使用 formatUSDC 格式化 | +| 已实现盈亏 | realizedPnl | 该卖出订单的盈亏,使用 formatUSDC 格式化,颜色:盈利绿色,亏损红色 | +| 创建时间 | createdAt | 时间戳转换为可读格式 | + +### 4.3 功能 + +- **分页**:支持分页查询 +- **排序**:默认按创建时间倒序 +- **筛选**:可按市场、方向筛选 +- **详情**:点击订单ID查看匹配明细(可选) + +## 5. 匹配关系列表页面 + +### 5.1 页面路径 +`/copy-trading/orders/matched/:copyTradingId` + +### 5.2 表格列 + +| 列名 | 字段 | 说明 | +|------|------|------| +| 卖出订单ID | sellOrderId | 跟单卖出订单ID(可点击查看详情) | +| 买入订单ID | buyOrderId | 匹配的买入订单ID(可点击查看详情) | +| 匹配数量 | matchedQuantity | 匹配的数量,使用 formatUSDC 格式化 | +| 买入价格 | buyPrice | 买入价格,使用 formatUSDC 格式化 | +| 卖出价格 | sellPrice | 卖出价格,使用 formatUSDC 格式化 | +| 盈亏 | realizedPnl | (卖出价格 - 买入价格) × 匹配数量,使用 formatUSDC 格式化,颜色:盈利绿色,亏损红色 | +| 匹配时间 | matchedAt | 时间戳转换为可读格式 | + +### 5.3 功能 + +- **分页**:支持分页查询 +- **排序**:默认按匹配时间倒序 +- **筛选**:可按卖出订单ID、买入订单ID筛选 +- **详情**:点击订单ID查看详情(可选) + +## 6. 跟单列表页面增强 + +### 6.1 在跟单列表中添加统计入口 + +在 `CopyTradingList` 页面中,每个跟单关系添加: +- **查看统计**按钮:跳转到统计页面 +- **查看订单**按钮:跳转到订单列表页面(可选择买入/卖出/匹配) + +### 6.2 快速统计显示 + +在跟单列表表格中,可添加快速统计列: +- **总盈亏**:显示该跟单关系的总盈亏(颜色标识) +- **订单数**:买入订单数 / 卖出订单数 +- **持仓**:当前持仓数量 + +## 7. 类型定义 + +### 7.1 跟单关系统计响应 + +```typescript +export interface CopyTradingStatistics { + copyTradingId: number + accountId: number + accountName: string + leaderId: number + leaderName: string + templateId: number + templateName: string + + // 买入统计 + totalBuyQuantity: string + totalBuyOrders: number + totalBuyAmount: string + + // 卖出统计 + totalSellQuantity: string + totalSellOrders: number + totalSellAmount: string + + // 持仓统计 + currentPositionQuantity: string + currentPositionValue: string + avgBuyPrice: string + + // 盈亏统计 + totalRealizedPnl: string + totalUnrealizedPnl: string + totalPnl: string + totalPnlPercent: string +} +``` + +### 7.2 买入订单信息 + +```typescript +export interface BuyOrderInfo { + orderId: string + leaderTradeId: string + marketId: string + side: string + quantity: string + price: string + amount: string + matchedQuantity: string + remainingQuantity: string + status: 'filled' | 'partially_matched' | 'fully_matched' + createdAt: number +} +``` + +### 7.3 卖出订单信息 + +```typescript +export interface SellOrderInfo { + orderId: string + leaderTradeId: string + marketId: string + side: string + quantity: string + price: string + amount: string + realizedPnl: string + createdAt: number +} +``` + +### 7.4 匹配订单信息 + +```typescript +export interface MatchedOrderInfo { + sellOrderId: string + buyOrderId: string + matchedQuantity: string + buyPrice: string + sellPrice: string + realizedPnl: string + matchedAt: number +} +``` + +## 8. API 接口 + +### 8.1 查询跟单统计 + +``` +POST /api/copy-trading/statistics/detail +Request: { copyTradingId: number } +Response: ApiResponse +``` + +### 8.2 查询买入订单列表 + +``` +POST /api/copy-trading/orders/tracking +Request: { + copyTradingId: number + type: 'buy' + page?: number + limit?: number + marketId?: string + side?: string + status?: string +} +Response: ApiResponse<{ list: BuyOrderInfo[], total: number }> +``` + +### 8.3 查询卖出订单列表 + +``` +POST /api/copy-trading/orders/tracking +Request: { + copyTradingId: number + type: 'sell' + page?: number + limit?: number + marketId?: string + side?: string +} +Response: ApiResponse<{ list: SellOrderInfo[], total: number }> +``` + +### 8.4 查询匹配关系列表 + +``` +POST /api/copy-trading/orders/tracking +Request: { + copyTradingId: number + type: 'matched' + page?: number + limit?: number + sellOrderId?: string + buyOrderId?: string +} +Response: ApiResponse<{ list: MatchedOrderInfo[], total: number }> +``` + +## 9. UI/UX 要求 + +### 9.1 移动端适配 + +- **响应式布局**:使用 `useMediaQuery` 检测移动端 +- **表格优化**:移动端使用卡片布局或横向滚动 +- **统计卡片**:移动端每行 1-2 个,简化显示 + +### 9.2 数据格式化 + +- **统一使用 `formatUSDC`**:所有 USDC 金额显示 +- **时间格式化**:使用相对时间或标准时间格式 +- **百分比显示**:保留 2 位小数 + +### 9.3 颜色规范 + +- **盈利**:绿色(#3f8600) +- **亏损**:红色(#cf1322) +- **状态标签**: + - filled: 蓝色 + - partially_matched: 橙色 + - fully_matched: 绿色 + +### 9.4 交互优化 + +- **加载状态**:使用 `loading` 属性显示加载中 +- **错误处理**:使用 `message.error` 显示错误信息 +- **空状态**:显示友好的空状态提示 +- **分页**:支持每页数量调整 + +## 10. 实现优先级 + +### Phase 1: 核心功能 +1. 跟单关系统计页面(基础统计) +2. 买入订单列表页面 +3. 卖出订单列表页面 + +### Phase 2: 增强功能 +4. 匹配关系列表页面 +5. 跟单列表页面增强(快速统计) +6. 订单详情页面(可选) + +### Phase 3: 优化功能 +7. 数据可视化(图表展示) +8. 导出功能(导出统计报表) +9. 高级筛选和搜索 + diff --git a/docs/copy-trading-order-tracking-design.md b/docs/copy-trading-order-tracking-design.md new file mode 100644 index 0000000..e3c5ae6 --- /dev/null +++ b/docs/copy-trading-order-tracking-design.md @@ -0,0 +1,533 @@ +# 跟单订单跟踪与统计设计文档 + +## 1. 方案概述 + +采用**订单跟踪匹配方案**,精确追踪每笔买入订单,当 Leader 卖出时进行精确匹配,实现: +- 精确的买入-卖出匹配关系 +- 准确的盈亏计算(已实现/未实现) +- 完整的订单统计信息 +- 多维度数据统计 + +## 2. 核心思路 + +### 2.1 事件监听 + +**当前监听的事件类型**:**交易事件(trade)** + +- **事件来源**: + - WebSocket User Channel:`event_type = "trade"` + - 轮询 CLOB API:`GET /trades?user={leaderAddress}` +- **触发时机**:交易已成交 +- **数据字段**:`id`(trade_id)、`market`、`side`(BUY/SELL)、`price`、`size`、`timestamp` +- **去重标识**:`leader_id + trade_id`(trade.id) + +**说明**: +- 只监听已成交的交易事件,不监听订单创建事件 +- 交易事件表示 Leader 已经完成买入或卖出操作 +- 通过 `trade.id` 进行去重,确保同一笔交易只处理一次 + +### 2.2 买入订单跟踪 + +当 Leader 买入时(通过交易事件): +1. 检测到 `side = "BUY"` 的交易事件 +2. 创建跟单买入订单 +3. 记录到 `copy_order_tracking` 表 +4. 记录买入数量、价格、状态等信息 + +### 2.3 卖出订单匹配 + +当 Leader 卖出时(通过交易事件): +1. 检测到 `side = "SELL"` 的交易事件 +2. 查找未匹配的买入订单(FIFO 策略) +3. 按比例匹配卖出数量 +4. 更新买入订单的匹配状态 +5. 记录匹配关系到 `sell_match_record` 和 `sell_match_detail` + +### 2.4 匹配策略 + +- **FIFO(先进先出)**:按买入时间顺序匹配 +- **部分匹配**:支持一个买入订单被多次卖出匹配 +- **状态管理**:`filled` → `partially_matched` → `fully_matched` + +## 3. 数据模型 + +### 3.1 订单跟踪表(copy_order_tracking) + +```sql +CREATE TABLE copy_order_tracking ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + copy_trading_id BIGINT NOT NULL, -- 跟单关系ID + account_id BIGINT NOT NULL, + leader_id BIGINT NOT NULL, + template_id BIGINT NOT NULL, + market_id VARCHAR(100) NOT NULL, + side VARCHAR(10) NOT NULL, -- YES/NO + buy_order_id VARCHAR(100) NOT NULL, -- 跟单买入订单ID + leader_buy_trade_id VARCHAR(100) NOT NULL, -- Leader 买入交易ID + quantity DECIMAL(20, 8) NOT NULL, -- 买入数量 + price DECIMAL(20, 8) NOT NULL, -- 买入价格 + matched_quantity DECIMAL(20, 8) NOT NULL DEFAULT 0, -- 已匹配卖出数量 + remaining_quantity DECIMAL(20, 8) NOT NULL, -- 剩余未匹配数量 + status VARCHAR(20) NOT NULL, -- filled, fully_matched, partially_matched + created_at BIGINT NOT NULL, + updated_at BIGINT NOT NULL, + INDEX idx_copy_trading (copy_trading_id), + INDEX idx_remaining (remaining_quantity, status) +); +``` + +### 3.2 卖出匹配记录表(sell_match_record) + +```sql +CREATE TABLE sell_match_record ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + copy_trading_id BIGINT NOT NULL, + sell_order_id VARCHAR(100) NOT NULL, -- 跟单卖出订单ID + leader_sell_trade_id VARCHAR(100) NOT NULL, -- Leader 卖出交易ID + market_id VARCHAR(100) NOT NULL, + side VARCHAR(10) NOT NULL, + total_matched_quantity DECIMAL(20, 8) NOT NULL, -- 总匹配数量 + sell_price DECIMAL(20, 8) NOT NULL, -- 卖出价格 + total_realized_pnl DECIMAL(20, 8) NOT NULL, -- 总已实现盈亏 + created_at BIGINT NOT NULL, + INDEX idx_copy_trading (copy_trading_id) +); +``` + +### 3.3 匹配明细表(sell_match_detail) + +```sql +CREATE TABLE sell_match_detail ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + match_record_id BIGINT NOT NULL, -- 关联 sell_match_record.id + tracking_id BIGINT NOT NULL, -- 关联 copy_order_tracking.id + buy_order_id VARCHAR(100) NOT NULL, + matched_quantity DECIMAL(20, 8) NOT NULL, -- 匹配的数量 + buy_price DECIMAL(20, 8) NOT NULL, + sell_price DECIMAL(20, 8) NOT NULL, + realized_pnl DECIMAL(20, 8) NOT NULL, -- 盈亏 = (sell_price - buy_price) * matched_quantity + created_at BIGINT NOT NULL, + FOREIGN KEY (match_record_id) REFERENCES sell_match_record(id), + FOREIGN KEY (tracking_id) REFERENCES copy_order_tracking(id) +); +``` + +## 4. 核心流程 + +### 4.1 买入订单跟踪流程 + +``` +Leader 买入交易 + ↓ +检测到交易,计算跟单数量 + ↓ +根据模板模式计算: + - RATIO 模式: 数量 = Leader 数量 × copyRatio + - FIXED 模式: 数量 = fixedAmount / 买入价格 + ↓ +创建跟单买入订单 + ↓ +记录到 copy_order_tracking + - quantity: 买入数量 + - price: 买入价格 + - remaining_quantity: 初始等于 quantity + - status: "filled" +``` + +### 4.2 卖出订单匹配流程 + +``` +Leader 卖出交易 + ↓ +查找未匹配的买入订单(remaining_quantity > 0) + ↓ +按 FIFO 顺序匹配 + ↓ +计算匹配数量(统一按比例,不区分模式) + - 需要匹配数量 = Leader 卖出数量 × copyRatio + - 实际匹配数量 = min(需要匹配数量, 剩余持仓数量) + ↓ +更新买入订单状态 + - matched_quantity += 匹配数量 + - remaining_quantity -= 匹配数量 + - status: 根据剩余数量更新 + ↓ +记录匹配关系 + - sell_match_record: 卖出订单记录 + - sell_match_detail: 匹配明细(每笔买入订单的匹配) +``` + +**重要说明**: +- **买入时**:根据模板的 `copyMode` 计算(RATIO 按比例,FIXED 按固定金额) +- **卖出时**:统一按比例计算(`Leader 卖出数量 × copyRatio`),不区分模式 +- **固定金额模式**:只影响买入时的计算,卖出时仍然按比例 + +### 4.3 匹配计算示例 + +#### 示例1:比例模式 + +``` +场景(比例模式,copyRatio = 100%): +- 买入订单1: quantity=100, remaining=100 +- 买入订单2: quantity=50, remaining=50 +- Leader 卖出: 120 + +匹配过程: +1. 计算需要匹配:120 × 100% = 120 +2. 订单1: 匹配 min(100, 120) = 100,剩余需匹配 = 20 +3. 订单2: 匹配 min(50, 20) = 20,剩余需匹配 = 0 + +结果: +- 订单1: remaining = 0, status = "fully_matched" +- 订单2: remaining = 30, status = "partially_matched" +- 跟单卖出: 120 +``` + +#### 示例2:固定金额模式 + +``` +场景(固定金额模式,fixedAmount = 15 USDC,copyRatio = 100%): +- Leader 买入: 100 数量,价格 0.5 +- 跟单买入: 15 / 0.5 = 30 数量(固定金额) +- Leader 卖出: 50 数量,价格 0.7 + +匹配过程: +1. 计算需要匹配:50 × 100% = 50(按比例,不按固定金额) +2. 订单1: 匹配 min(30, 50) = 30,剩余需匹配 = 20 + +结果: +- 订单1: remaining = 0, status = "fully_matched" +- 跟单卖出: 30(不超过持仓) +- 注意:虽然买入时是固定金额,但卖出时按比例计算 +``` + +#### 示例3:部分比例模式 + +``` +场景(比例模式,copyRatio = 30%): +- Leader 买入: 100 数量 +- 跟单买入: 100 × 30% = 30 数量 +- Leader 卖出: 50 数量 + +匹配过程: +1. 计算需要匹配:50 × 30% = 15 +2. 订单1: 匹配 min(30, 15) = 15,剩余需匹配 = 0 + +结果: +- 订单1: remaining = 15, status = "partially_matched" +- 跟单卖出: 15 +``` + +## 5. 统计功能 + +### 5.1 跟单关系统计 + +**统计维度**: +- 总买入数量/金额/订单数 +- 总卖出数量/金额/订单数 +- 当前持仓数量 +- 平均买入价格 +- 总已实现盈亏 +- 总未实现盈亏(持仓盈亏) +- 总盈亏及百分比 + +**计算方式**: +```kotlin +// 使用 util 方法进行数值计算 +val totalBuyQuantity = buyOrders.sumOf { it.quantity.toSafeBigDecimal() } +val totalSellQuantity = sellOrders.sumOf { it.quantity.toSafeBigDecimal() } +val currentPosition = buyOrders.sumOf { it.remainingQuantity.toSafeBigDecimal() } + +// 已实现盈亏 +val totalRealizedPnl = matchDetails.sumOf { it.realizedPnl.toSafeBigDecimal() } + +// 未实现盈亏(需要当前市场价格) +val currentPrice = getMarketCurrentPrice(marketId) +val avgBuyPrice = totalBuyAmount.div(totalBuyQuantity) +val unrealizedPnl = currentPosition.multi(currentPrice.subtract(avgBuyPrice)) + +// 总盈亏 +val totalPnl = totalRealizedPnl.add(totalUnrealizedPnl) +``` + +### 5.2 订单信息 + +**买入订单列表**: +- 订单ID、Leader 交易ID +- 市场、方向、数量、价格 +- 已匹配数量、剩余数量 +- 订单状态 + +**卖出订单列表**: +- 订单ID、Leader 交易ID +- 市场、方向、数量、价格 +- 已实现盈亏 + +**匹配关系列表**: +- 卖出订单ID +- 匹配的买入订单ID +- 匹配数量 +- 买入价格、卖出价格 +- 盈亏 + +## 6. 数值计算规范 + +**使用 util 扩展方法**: +- `toSafeBigDecimal()`: 安全转换为 BigDecimal +- `multi()`: 乘法运算 +- `div()`: 除法运算 +- `eq()`, `lt()`, `gt()`, `gte()`, `lte()`: 比较运算 + +**示例**: +```kotlin +// 计算匹配数量 +val matchedQty = min(remainingQty.toSafeBigDecimal(), needMatchQty.toSafeBigDecimal()) + +// 计算盈亏 +val pnl = sellPrice.toSafeBigDecimal() + .subtract(buyPrice.toSafeBigDecimal()) + .multi(matchedQty) + +// 比较数量 +if (remainingQty.toSafeBigDecimal().gt(BigDecimal.ZERO)) { + // 还有剩余 +} +``` + +## 7. 关键实现点 + +### 7.1 买入数量计算 + +```kotlin +// 买入时根据模式计算 +fun calculateBuyQuantity(leaderTrade: Trade, template: CopyTradingTemplate): BigDecimal { + return when (template.copyMode) { + "RATIO" -> { + // 比例模式:Leader 数量 × 比例 + leaderTrade.size.toSafeBigDecimal() + .multi(template.copyRatio) + } + "FIXED" -> { + // 固定金额模式:固定金额 / 买入价格 + val fixedAmount = template.fixedAmount?.toSafeBigDecimal() + ?: throw IllegalStateException("固定金额模式下 fixedAmount 不能为空") + val buyPrice = leaderTrade.price.toSafeBigDecimal() + fixedAmount.div(buyPrice) + } + else -> throw IllegalArgumentException("不支持的 copyMode: ${template.copyMode}") + } +} +``` + +### 7.2 卖出匹配算法 + +```kotlin +// 卖出时统一按比例计算(不区分模式) +fun matchSellOrder(leaderSellTrade: Trade, copyTrading: CopyTrading, template: CopyTradingTemplate): BigDecimal { + // 统一按比例计算,不区分 RATIO 或 FIXED 模式 + val needMatch = leaderSellTrade.size.toSafeBigDecimal() + .multi(template.copyRatio) + + val unmatchedOrders = findUnmatchedBuyOrders(copyTrading.id, leaderSellTrade.market, leaderSellTrade.side) + var totalMatched = BigDecimal.ZERO + var remaining = needMatch + + for (order in unmatchedOrders) { + if (remaining.lte(BigDecimal.ZERO)) break + + val matchQty = min(order.remainingQuantity.toSafeBigDecimal(), remaining) + totalMatched = totalMatched.add(matchQty) + remaining = remaining.subtract(matchQty) + + updateOrderTracking(order, matchQty) + recordMatchDetail(order, matchQty, leaderSellTrade) + } + + return totalMatched +} +``` + +### 7.3 状态更新 + +```kotlin +fun updateOrderStatus(tracking: CopyOrderTracking) { + when { + tracking.remainingQuantity.toSafeBigDecimal().eq(BigDecimal.ZERO) -> { + tracking.status = "fully_matched" + } + tracking.matchedQuantity.toSafeBigDecimal().gt(BigDecimal.ZERO) -> { + tracking.status = "partially_matched" + } + else -> { + tracking.status = "filled" + } + } +} +``` + +## 8. API 设计 + +### 8.1 查询跟单统计 + +``` +POST /api/copy-trading/statistics/detail +Request: { copyTradingId: Long } +Response: CopyTradingStatisticsResponse +``` + +### 8.2 查询订单列表 + +``` +POST /api/copy-trading/orders/tracking +Request: { copyTradingId: Long, type: "buy" | "sell" | "matched" } +Response: OrderListResponse +``` + +## 9. 优势 + +1. **精确匹配**:每笔卖出都能追溯到对应的买入订单 +2. **准确盈亏**:可以精确计算每笔交易的盈亏 +3. **完整统计**:支持多维度数据统计和分析 +4. **可追溯性**:完整的买入-卖出匹配关系,便于审计 + +## 10. WebSocket 与轮询去重机制 + +### 10.1 同时运行策略 + +**WebSocket 和轮询可以同时运行**: +- **WebSocket**:作为主要数据源,实时接收交易推送 +- **轮询**:作为补充数据源,定期查询确保不遗漏 +- **去重机制**:通过 trade_id 确保同一笔交易只处理一次 + +### 10.2 去重数据模型 + +#### 已处理交易表(processed_trade) + +```sql +CREATE TABLE processed_trade ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + leader_id BIGINT NOT NULL, + leader_trade_id VARCHAR(100) NOT NULL, -- Leader 的交易ID(trade.id,唯一标识) + trade_type VARCHAR(10) NOT NULL, -- BUY 或 SELL + source VARCHAR(20) NOT NULL, -- 'websocket' 或 'polling' + processed_at BIGINT NOT NULL, + created_at BIGINT NOT NULL, + UNIQUE KEY uk_leader_trade (leader_id, leader_trade_id), + INDEX idx_processed_at (processed_at) +); +``` + +**唯一标识**:`leader_id + leader_trade_id` 组合作为唯一键 + +**重要说明**: +- `leader_trade_id` 对应 `TradeResponse.id`(交易ID) +- 交易事件(trade)只有 `id` 字段,没有 `order_id` 字段 +- 通过 `trade.id` 进行去重,确保同一笔交易只处理一次 + +### 10.3 去重流程 + +```kotlin +/** + * 处理交易事件(WebSocket 或轮询) + */ +suspend fun processTrade(leaderId: Long, trade: TradeResponse, source: String) { + // 1. 检查是否已处理(去重) + // 使用 trade.id 作为唯一标识(TradeResponse 只有 id 字段,没有 order_id) + val isProcessed = processedTradeRepository.existsByLeaderIdAndLeaderTradeId( + leaderId, + trade.id // trade.id 是交易ID,用于去重 + ) + + if (isProcessed) { + logger.debug("交易已处理,跳过: leaderId=$leaderId, tradeId=${trade.id}, source=$source") + return + } + + // 2. 处理交易逻辑 + try { + // 根据 side 判断是买入还是卖出 + when (trade.side.uppercase()) { + "BUY" -> processBuyTrade(leaderId, trade) + "SELL" -> processSellTrade(leaderId, trade) + else -> { + logger.warn("未知的交易方向: ${trade.side}") + return + } + } + + // 3. 标记为已处理 + val processed = ProcessedTrade( + leaderId = leaderId, + leaderTradeId = trade.id, // 使用 trade.id 作为唯一标识 + tradeType = trade.side, + source = source, + processedAt = System.currentTimeMillis() + ) + processedTradeRepository.save(processed) + + logger.info("成功处理交易: leaderId=$leaderId, tradeId=${trade.id}, source=$source, side=${trade.side}") + } catch (e: Exception) { + logger.error("处理交易失败: leaderId=$leaderId, tradeId=${trade.id}", e) + // 失败时不标记为已处理,允许重试 + } +} +``` + +### 10.4 并发安全 + +**使用数据库唯一约束保证并发安全**: +- 数据库唯一约束:`UNIQUE KEY uk_leader_trade (leader_id, leader_trade_id)` +- 如果 WebSocket 和轮询同时收到同一笔交易: + - 第一个请求:成功处理并插入记录 + - 第二个请求:插入失败(唯一约束),跳过处理 + +**或者使用分布式锁**: +```kotlin +// 使用 Redis 分布式锁 +val lockKey = "trade:${leaderId}:${trade.id}" +if (redisLock.tryLock(lockKey, 5, TimeUnit.SECONDS)) { + try { + if (!isProcessed(leaderId, trade.id)) { + processTrade(leaderId, trade) + markAsProcessed(leaderId, trade.id) + } + } finally { + redisLock.unlock(lockKey) + } +} +``` + +### 10.5 清理策略 + +**定期清理过期记录**: +```kotlin +@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点 +fun cleanupProcessedTrades() { + val expireTime = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7) // 保留 7 天 + processedTradeRepository.deleteByProcessedAtBefore(expireTime) +} +``` + +### 10.6 优势 + +1. **高可用性**:WebSocket 断开时,轮询继续工作 +2. **数据完整性**:轮询确保不遗漏任何交易 +3. **实时性**:WebSocket 提供实时推送 +4. **去重保证**:通过唯一标识确保不重复处理 + +## 11. 注意事项 + +1. **匹配策略**:默认使用 FIFO,可根据需求调整 +2. **部分匹配**:支持一个买入订单被多次卖出匹配 +3. **数量计算**:使用 util 方法确保数值计算安全 +4. **状态同步**:及时更新订单状态,确保数据一致性 +5. **模式区别**: + - **买入时**:RATIO 模式按比例计算,FIXED 模式按固定金额计算 + - **卖出时**:统一按比例计算(`Leader 卖出数量 × copyRatio`),不区分模式 + - **固定金额模式**:只影响买入时的计算,卖出时仍然按比例 +6. **去重机制**: + - WebSocket 和轮询可以同时运行 + - 使用 `leader_id + leader_trade_id` 作为唯一标识去重 + - 数据库唯一约束保证并发安全 + - 定期清理过期记录(建议保留 7 天) + diff --git a/frontend/index.html b/frontend/index.html index 8c6abc6..1dc0095 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Polymarket 跟单系统 + PolyHermes
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 23e0ed4..21bf6f8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,6 +18,10 @@ import TemplateAdd from './pages/TemplateAdd' import TemplateEdit from './pages/TemplateEdit' import CopyTradingList from './pages/CopyTradingList' import CopyTradingAdd from './pages/CopyTradingAdd' +import CopyTradingStatistics from './pages/CopyTradingStatistics' +import CopyTradingBuyOrders from './pages/CopyTradingBuyOrders' +import CopyTradingSellOrders from './pages/CopyTradingSellOrders' +import CopyTradingMatchedOrders from './pages/CopyTradingMatchedOrders' import { wsManager } from './services/websocket' import type { OrderPushMessage } from './types' @@ -136,6 +140,10 @@ function App() { } /> } /> } /> + } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index baae66e..2e00ace 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -35,7 +35,7 @@ const Layout: React.FC = ({ children }) => { // 获取当前应该打开的父菜单 const getInitialOpenKeys = (): string[] => { const path = location.pathname - if (path.startsWith('/templates') || path.startsWith('/copy-trading')) { + if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) { return ['/copy-trading-management'] } return [] @@ -46,7 +46,7 @@ const Layout: React.FC = ({ children }) => { // 当路径变化时,自动打开对应的父菜单 useEffect(() => { const path = location.pathname - if (path.startsWith('/templates') || path.startsWith('/copy-trading')) { + if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) { setOpenKeys(['/copy-trading-management']) } }, [location.pathname]) @@ -57,16 +57,16 @@ const Layout: React.FC = ({ children }) => { icon: , label: '账户管理' }, - { - key: '/leaders', - icon: , - label: 'Leader 管理' - }, { key: '/copy-trading-management', icon: , - label: '跟单管理', + label: '跟单交易', children: [ + { + key: '/leaders', + icon: , + label: 'Leader 管理' + }, { key: '/templates', icon: , @@ -118,7 +118,7 @@ const Layout: React.FC = ({ children }) => { justifyContent: 'space-between' }}>
- Polymarket 跟单 + PolyHermes
+

买入订单列表

+ + + +
+ setFilters({ ...filters, marketId: e.target.value || undefined })} + /> + + + + + + +
+ + {isMobile ? ( + // 移动端卡片布局 +
+ {loading ? ( +
+ +
+ ) : orders.length === 0 ? ( +
+ 暂无买入订单 +
+ ) : ( +
+ {orders.map((order) => { + const date = new Date(order.createdAt) + const formattedDate = date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) + const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString() + const displaySide = order.side === '0' ? 'YES' : order.side === '1' ? 'NO' : order.side + + return ( + + {/* 订单ID和状态 */} +
+
+ {order.orderId.slice(0, 8)}...{order.orderId.slice(-6)} +
+
+ {displaySide} + {getStatusTag(order.status)} +
+
+ + + + {/* 买入信息 */} +
+
买入信息
+
+ 数量: {formatUSDC(order.quantity)} | 价格: {formatUSDC(order.price)} +
+
+ 金额: {formatUSDC(amount)} USDC +
+
+ + {/* 匹配信息 */} +
+
匹配信息
+
+ 已匹配: {formatUSDC(order.matchedQuantity)} | 剩余: {formatUSDC(order.remainingQuantity)} +
+
+ + {/* Leader 交易ID */} +
+
Leader 交易ID
+
+ {order.leaderTradeId.slice(0, 8)}...{order.leaderTradeId.slice(-6)} +
+
+ + {/* 市场ID */} +
+
市场ID
+
+ {order.marketId.slice(0, 8)}...{order.marketId.slice(-6)} +
+
+ + {/* 创建时间 */} +
+
+ 创建时间: {formattedDate} +
+
+
+ ) + })} +
+ )} +
+ ) : ( + // 桌面端表格布局 + `共 ${total} 条`, + onChange: (newPage, newLimit) => { + setPage(newPage) + setLimit(newLimit) + } + }} + /> + )} + + + ) +} + +export default CopyTradingBuyOrdersPage + diff --git a/frontend/src/pages/CopyTradingList.tsx b/frontend/src/pages/CopyTradingList.tsx index 9477876..a91405a 100644 --- a/frontend/src/pages/CopyTradingList.tsx +++ b/frontend/src/pages/CopyTradingList.tsx @@ -1,11 +1,13 @@ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { Card, Table, Button, Space, Tag, Popconfirm, Switch, message, Select, Input } from 'antd' -import { PlusOutlined, DeleteOutlined } from '@ant-design/icons' +import { Card, Table, Button, Space, Tag, Popconfirm, Switch, message, Select, Input, Dropdown, Divider, Spin } from 'antd' +import { PlusOutlined, DeleteOutlined, BarChartOutlined, UnorderedListOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons' +import type { MenuProps } from 'antd' import { apiService } from '../services/api' import { useAccountStore } from '../store/accountStore' -import type { CopyTrading, Account, Leader, CopyTradingTemplate } from '../types' +import type { CopyTrading, Account, Leader, CopyTradingTemplate, CopyTradingStatistics } from '../types' import { useMediaQuery } from 'react-responsive' +import { formatUSDC } from '../utils' const { Option } = Select @@ -17,6 +19,8 @@ const CopyTradingList: React.FC = () => { const [leaders, setLeaders] = useState([]) const [templates, setTemplates] = useState([]) const [loading, setLoading] = useState(false) + const [statisticsMap, setStatisticsMap] = useState>({}) + const [loadingStatistics, setLoadingStatistics] = useState>(new Set()) const [filters, setFilters] = useState<{ accountId?: number templateId?: number @@ -62,7 +66,12 @@ const CopyTradingList: React.FC = () => { try { const response = await apiService.copyTrading.list(filters) if (response.data.code === 0 && response.data.data) { - setCopyTradings(response.data.data.list || []) + const list = response.data.data.list || [] + setCopyTradings(list) + // 为每个跟单关系获取统计信息 + list.forEach((ct: CopyTrading) => { + fetchStatistics(ct.id) + }) } else { message.error(response.data.msg || '获取跟单列表失败') } @@ -73,6 +82,50 @@ const CopyTradingList: React.FC = () => { } } + const fetchStatistics = async (copyTradingId: number) => { + // 如果正在加载或已有数据,跳过 + if (loadingStatistics.has(copyTradingId) || statisticsMap[copyTradingId]) { + return + } + + setLoadingStatistics(prev => new Set(prev).add(copyTradingId)) + try { + const response = await apiService.statistics.detail({ copyTradingId }) + if (response.data.code === 0 && response.data.data) { + setStatisticsMap(prev => ({ + ...prev, + [copyTradingId]: response.data.data + })) + } + } catch (error: any) { + console.error(`获取跟单统计失败: copyTradingId=${copyTradingId}`, error) + } finally { + setLoadingStatistics(prev => { + const next = new Set(prev) + next.delete(copyTradingId) + return next + }) + } + } + + const getPnlColor = (value: string): string => { + const num = parseFloat(value) + if (isNaN(num)) return '#666' + return num >= 0 ? '#3f8600' : '#cf1322' + } + + const getPnlIcon = (value: string) => { + const num = parseFloat(value) + if (isNaN(num)) return null + return num >= 0 ? : + } + + const formatPercent = (value: string): string => { + const num = parseFloat(value) + if (isNaN(num)) return '-' + return `${num >= 0 ? '+' : ''}${num.toFixed(2)}%` + } + const handleToggleStatus = async (copyTrading: CopyTrading) => { try { const response = await apiService.copyTrading.updateStatus({ @@ -108,11 +161,17 @@ const CopyTradingList: React.FC = () => { { title: '钱包', key: 'account', + width: isMobile ? 100 : 150, render: (_: any, record: CopyTrading) => (
-
{record.accountName || `账户 ${record.accountId}`}
-
- {record.walletAddress.slice(0, 6)}...{record.walletAddress.slice(-4)} +
+ {record.accountName || `账户 ${record.accountId}`} +
+
+ {isMobile + ? `${record.walletAddress.slice(0, 4)}...${record.walletAddress.slice(-3)}` + : `${record.walletAddress.slice(0, 6)}...${record.walletAddress.slice(-4)}` + }
) @@ -121,16 +180,25 @@ const CopyTradingList: React.FC = () => { title: '模板', dataIndex: 'templateName', key: 'templateName', - render: (text: string) => {text} + width: isMobile ? 100 : 120, + render: (text: string) => ( + {text} + ) }, { title: 'Leader', key: 'leader', + width: isMobile ? 100 : 150, render: (_: any, record: CopyTrading) => (
-
{record.leaderName || `Leader ${record.leaderId}`}
-
- {record.leaderAddress.slice(0, 6)}...{record.leaderAddress.slice(-4)} +
+ {record.leaderName || `Leader ${record.leaderId}`} +
+
+ {isMobile + ? `${record.leaderAddress.slice(0, 4)}...${record.leaderAddress.slice(-3)}` + : `${record.leaderAddress.slice(0, 6)}...${record.leaderAddress.slice(-4)}` + }
) @@ -139,6 +207,7 @@ const CopyTradingList: React.FC = () => { title: '状态', dataIndex: 'enabled', key: 'enabled', + width: isMobile ? 80 : 100, render: (enabled: boolean, record: CopyTrading) => ( { /> ) }, + { + title: '总盈亏', + key: 'totalPnl', + width: isMobile ? 100 : 150, + render: (_: any, record: CopyTrading) => { + const stats = statisticsMap[record.id] + if (!stats) { + return loadingStatistics.has(record.id) ? ( + 加载中... + ) : ( + - + ) + } + return ( +
+
+ {getPnlIcon(stats.totalPnl)} + {isMobile ? formatUSDC(stats.totalPnl) : `${formatUSDC(stats.totalPnl)} USDC`} +
+ {!isMobile && ( +
+ {formatPercent(stats.totalPnlPercent)} +
+ )} +
+ ) + } + }, { title: '操作', key: 'action', - width: isMobile ? 80 : 100, - render: (_: any, record: CopyTrading) => ( - handleDelete(record.id)} - okText="确定" - cancelText="取消" - > - - - ) + width: isMobile ? 100 : 200, + fixed: 'right' as const, + render: (_: any, record: CopyTrading) => { + const menuItems: MenuProps['items'] = [ + { + key: 'statistics', + label: '查看统计', + icon: , + onClick: () => navigate(`/copy-trading/statistics/${record.id}`) + }, + { + key: 'buyOrders', + label: '买入订单', + icon: , + onClick: () => navigate(`/copy-trading/orders/buy/${record.id}`) + }, + { + key: 'sellOrders', + label: '卖出订单', + icon: , + onClick: () => navigate(`/copy-trading/orders/sell/${record.id}`) + }, + { + key: 'matchedOrders', + label: '匹配关系', + icon: , + onClick: () => navigate(`/copy-trading/orders/matched/${record.id}`) + }, + { + type: 'divider' + }, + { + key: 'delete', + label: ( + handleDelete(record.id)} + okText="确定" + cancelText="取消" + onCancel={(e) => e?.stopPropagation()} + > + 删除 + + ), + danger: true + } + ] + + return ( + + {!isMobile && ( + + )} + + + + {!isMobile && ( + handleDelete(record.id)} + okText="确定" + cancelText="取消" + > + + + )} + + ) + } } ] @@ -241,18 +420,205 @@ const CopyTradingList: React.FC = () => {
-
`共 ${total} 条` - }} - scroll={{ x: isMobile ? 800 : 'auto' }} - /> + {isMobile ? ( + // 移动端卡片布局 +
+ {loading ? ( +
+ +
+ ) : copyTradings.length === 0 ? ( +
+ 暂无跟单配置 +
+ ) : ( +
+ {copyTradings.map((record) => { + const stats = statisticsMap[record.id] + const date = new Date(record.createdAt) + const formattedDate = date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) + + return ( + + {/* 基本信息 */} +
+
+ {record.templateName} +
+
+ + {record.enabled ? '启用' : '禁用'} + +
+
+ + + + {/* 账户信息 */} +
+
账户
+
+ {record.accountName || `账户 ${record.accountId}`} +
+
+ {record.walletAddress.slice(0, 6)}...{record.walletAddress.slice(-4)} +
+
+ + {/* Leader 信息 */} +
+
Leader
+
+ {record.leaderName || `Leader ${record.leaderId}`} +
+
+ {record.leaderAddress.slice(0, 6)}...{record.leaderAddress.slice(-4)} +
+
+ + {/* 总盈亏 */} + {stats && ( +
+
总盈亏
+
+ {getPnlIcon(stats.totalPnl)} + {formatUSDC(stats.totalPnl)} USDC +
+
+ {formatPercent(stats.totalPnlPercent)} +
+
+ )} + + {loadingStatistics.has(record.id) && ( +
+ 加载统计中... +
+ )} + + {/* 创建时间 */} +
+
+ 创建时间: {formattedDate} +
+
+ + {/* 操作按钮 */} +
+ + , + onClick: () => navigate(`/copy-trading/statistics/${record.id}`) + }, + { + key: 'buyOrders', + label: '买入订单', + icon: , + onClick: () => navigate(`/copy-trading/orders/buy/${record.id}`) + }, + { + key: 'sellOrders', + label: '卖出订单', + icon: , + onClick: () => navigate(`/copy-trading/orders/sell/${record.id}`) + }, + { + key: 'matchedOrders', + label: '匹配关系', + icon: , + onClick: () => navigate(`/copy-trading/orders/matched/${record.id}`) + } + ] + }} + trigger={['click']} + > + + + handleDelete(record.id)} + okText="确定" + cancelText="取消" + > + + +
+
+ ) + })} +
+ )} +
+ ) : ( + // 桌面端表格布局 +
`共 ${total} 条` + }} + /> + )} ) diff --git a/frontend/src/pages/CopyTradingMatchedOrders.tsx b/frontend/src/pages/CopyTradingMatchedOrders.tsx new file mode 100644 index 0000000..0d47042 --- /dev/null +++ b/frontend/src/pages/CopyTradingMatchedOrders.tsx @@ -0,0 +1,305 @@ +import { useEffect, useState } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import { Card, Table, Button, Input, message, Divider, Spin } from 'antd' +import { LeftOutlined } from '@ant-design/icons' +import { apiService } from '../services/api' +import { formatUSDC } from '../utils' +import { useMediaQuery } from 'react-responsive' +import type { MatchedOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../types' + +const CopyTradingMatchedOrdersPage: React.FC = () => { + const { copyTradingId } = useParams<{ copyTradingId: string }>() + const navigate = useNavigate() + const isMobile = useMediaQuery({ maxWidth: 768 }) + const [loading, setLoading] = useState(false) + const [orders, setOrders] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [limit, setLimit] = useState(20) + const [filters, setFilters] = useState<{ + sellOrderId?: string + buyOrderId?: string + }>({}) + + useEffect(() => { + if (copyTradingId) { + fetchOrders() + } + }, [copyTradingId, page, limit, filters]) + + const fetchOrders = async () => { + if (!copyTradingId) return + + setLoading(true) + try { + const request: OrderTrackingRequest = { + copyTradingId: parseInt(copyTradingId), + type: 'matched', + page, + limit, + ...filters + } + + const response = await apiService.orderTracking.list(request) + if (response.data.code === 0 && response.data.data) { + const data = response.data.data as OrderTrackingListResponse + setOrders((data.list || []) as MatchedOrderInfo[]) + setTotal(data.total || 0) + } else { + message.error(response.data.msg || '获取匹配关系列表失败') + } + } catch (error: any) { + message.error(error.message || '获取匹配关系列表失败') + } finally { + setLoading(false) + } + } + + const getPnlColor = (value: string): string => { + const num = parseFloat(value) + if (isNaN(num)) return '#666' + return num >= 0 ? '#3f8600' : '#cf1322' + } + + const columns = [ + { + title: '卖出订单ID', + dataIndex: 'sellOrderId', + key: 'sellOrderId', + width: isMobile ? 100 : 150, + render: (text: string) => ( + + {isMobile + ? `${text.slice(0, 6)}...${text.slice(-4)}` + : `${text.slice(0, 8)}...${text.slice(-6)}` + } + + ) + }, + { + title: '买入订单ID', + dataIndex: 'buyOrderId', + key: 'buyOrderId', + width: isMobile ? 100 : 150, + render: (text: string) => ( + + {isMobile + ? `${text.slice(0, 6)}...${text.slice(-4)}` + : `${text.slice(0, 8)}...${text.slice(-6)}` + } + + ) + }, + { + title: '匹配数量', + dataIndex: 'matchedQuantity', + key: 'matchedQuantity', + width: isMobile ? 80 : 100, + render: (value: string) => ( + {formatUSDC(value)} + ) + }, + { + title: '买入价格', + dataIndex: 'buyPrice', + key: 'buyPrice', + width: isMobile ? 80 : 100, + render: (value: string) => ( + {formatUSDC(value)} + ) + }, + { + title: '卖出价格', + dataIndex: 'sellPrice', + key: 'sellPrice', + width: isMobile ? 80 : 100, + render: (value: string) => ( + {formatUSDC(value)} + ) + }, + { + title: '盈亏', + dataIndex: 'realizedPnl', + key: 'realizedPnl', + width: isMobile ? 100 : 120, + render: (value: string) => ( + + {isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`} + + ) + }, + { + title: '匹配时间', + dataIndex: 'matchedAt', + key: 'matchedAt', + width: isMobile ? 120 : 160, + render: (timestamp: number) => ( + + {isMobile + ? new Date(timestamp).toLocaleDateString('zh-CN') + : new Date(timestamp).toLocaleString('zh-CN') + } + + ) + } + ] + + return ( +
+ +
+
+ +

匹配关系列表

+
+
+ +
+ setFilters({ ...filters, sellOrderId: e.target.value || undefined })} + /> + + setFilters({ ...filters, buyOrderId: e.target.value || undefined })} + /> + + +
+ + {isMobile ? ( + // 移动端卡片布局 +
+ {loading ? ( +
+ +
+ ) : orders.length === 0 ? ( +
+ 暂无匹配关系 +
+ ) : ( +
+ {orders.map((order) => { + const date = new Date(order.matchedAt) + const formattedDate = date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) + + return ( + + {/* 订单ID */} +
+
卖出订单ID
+
+ {order.sellOrderId.slice(0, 8)}...{order.sellOrderId.slice(-6)} +
+
买入订单ID
+
+ {order.buyOrderId.slice(0, 8)}...{order.buyOrderId.slice(-6)} +
+
+ + + + {/* 匹配信息 */} +
+
匹配数量
+
+ {formatUSDC(order.matchedQuantity)} +
+
+ + {/* 价格信息 */} +
+
价格信息
+
+ 买入: {formatUSDC(order.buyPrice)} | 卖出: {formatUSDC(order.sellPrice)} +
+
+ + {/* 盈亏 */} +
+
盈亏
+
+ {formatUSDC(order.realizedPnl)} USDC +
+
+ + {/* 匹配时间 */} +
+
+ 匹配时间: {formattedDate} +
+
+
+ ) + })} +
+ )} +
+ ) : ( + // 桌面端表格布局 +
`${record.sellOrderId}-${record.buyOrderId}-${record.matchedAt}`} + loading={loading} + pagination={{ + current: page, + pageSize: limit, + total, + showSizeChanger: true, + showTotal: (total) => `共 ${total} 条`, + onChange: (newPage, newLimit) => { + setPage(newPage) + setLimit(newLimit) + } + }} + /> + )} + + + ) +} + +export default CopyTradingMatchedOrdersPage + diff --git a/frontend/src/pages/CopyTradingSellOrders.tsx b/frontend/src/pages/CopyTradingSellOrders.tsx new file mode 100644 index 0000000..d8fb179 --- /dev/null +++ b/frontend/src/pages/CopyTradingSellOrders.tsx @@ -0,0 +1,348 @@ +import { useEffect, useState } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import { Card, Table, Button, Tag, Select, Input, message, Divider, Spin } from 'antd' +import { LeftOutlined } from '@ant-design/icons' +import { apiService } from '../services/api' +import { formatUSDC } from '../utils' +import { useMediaQuery } from 'react-responsive' +import type { SellOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../types' + +const { Option } = Select + +const CopyTradingSellOrdersPage: React.FC = () => { + const { copyTradingId } = useParams<{ copyTradingId: string }>() + const navigate = useNavigate() + const isMobile = useMediaQuery({ maxWidth: 768 }) + const [loading, setLoading] = useState(false) + const [orders, setOrders] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [limit, setLimit] = useState(20) + const [filters, setFilters] = useState<{ + marketId?: string + side?: string + }>({}) + + useEffect(() => { + if (copyTradingId) { + fetchOrders() + } + }, [copyTradingId, page, limit, filters]) + + const fetchOrders = async () => { + if (!copyTradingId) return + + setLoading(true) + try { + const request: OrderTrackingRequest = { + copyTradingId: parseInt(copyTradingId), + type: 'sell', + page, + limit, + ...filters + } + + const response = await apiService.orderTracking.list(request) + if (response.data.code === 0 && response.data.data) { + const data = response.data.data as OrderTrackingListResponse + setOrders((data.list || []) as SellOrderInfo[]) + setTotal(data.total || 0) + } else { + message.error(response.data.msg || '获取卖出订单列表失败') + } + } catch (error: any) { + message.error(error.message || '获取卖出订单列表失败') + } finally { + setLoading(false) + } + } + + const getPnlColor = (value: string): string => { + const num = parseFloat(value) + if (isNaN(num)) return '#666' + return num >= 0 ? '#3f8600' : '#cf1322' + } + + const columns = [ + { + title: '订单ID', + dataIndex: 'orderId', + key: 'orderId', + width: isMobile ? 100 : 150, + render: (text: string) => ( + + {isMobile + ? `${text.slice(0, 6)}...${text.slice(-4)}` + : `${text.slice(0, 8)}...${text.slice(-6)}` + } + + ) + }, + { + title: 'Leader 交易ID', + dataIndex: 'leaderTradeId', + key: 'leaderTradeId', + width: isMobile ? 100 : 150, + render: (text: string) => ( + + {isMobile + ? `${text.slice(0, 6)}...${text.slice(-4)}` + : `${text.slice(0, 8)}...${text.slice(-6)}` + } + + ) + }, + { + title: '市场', + dataIndex: 'marketId', + key: 'marketId', + width: isMobile ? 100 : 150, + render: (text: string) => ( + + {isMobile + ? `${text.slice(0, 6)}...${text.slice(-4)}` + : `${text.slice(0, 8)}...${text.slice(-6)}` + } + + ) + }, + { + title: '方向', + dataIndex: 'side', + key: 'side', + width: isMobile ? 60 : 80, + render: (side: string) => { + // 将0/1转换为YES/NO + const displaySide = side === '0' ? 'YES' : side === '1' ? 'NO' : side + return {displaySide} + } + }, + { + title: '卖出数量', + dataIndex: 'quantity', + key: 'quantity', + width: isMobile ? 80 : 100, + render: (value: string) => ( + {formatUSDC(value)} + ) + }, + { + title: '卖出价格', + dataIndex: 'price', + key: 'price', + width: isMobile ? 80 : 100, + render: (value: string) => ( + {formatUSDC(value)} + ) + }, + { + title: '卖出金额', + key: 'amount', + width: isMobile ? 100 : 120, + render: (_: any, record: SellOrderInfo) => { + const amount = (parseFloat(record.quantity) * parseFloat(record.price)).toString() + return ( + + {isMobile ? formatUSDC(amount) : `${formatUSDC(amount)} USDC`} + + ) + } + }, + { + title: '已实现盈亏', + dataIndex: 'realizedPnl', + key: 'realizedPnl', + width: isMobile ? 100 : 120, + render: (value: string) => ( + + {isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`} + + ) + }, + { + title: '创建时间', + dataIndex: 'createdAt', + key: 'createdAt', + width: isMobile ? 120 : 160, + render: (timestamp: number) => ( + + {isMobile + ? new Date(timestamp).toLocaleDateString('zh-CN') + : new Date(timestamp).toLocaleString('zh-CN') + } + + ) + } + ] + + return ( +
+ +
+
+ +

卖出订单列表

+
+
+ +
+ setFilters({ ...filters, marketId: e.target.value || undefined })} + /> + + + + +
+ + {isMobile ? ( + // 移动端卡片布局 +
+ {loading ? ( +
+ +
+ ) : orders.length === 0 ? ( +
+ 暂无卖出订单 +
+ ) : ( +
+ {orders.map((order) => { + const date = new Date(order.createdAt) + const formattedDate = date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) + const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString() + const displaySide = order.side === '0' ? 'YES' : order.side === '1' ? 'NO' : order.side + + return ( + + {/* 订单ID和方向 */} +
+
+ {order.orderId.slice(0, 8)}...{order.orderId.slice(-6)} +
+
+ {displaySide} +
+
+ + + + {/* 卖出信息 */} +
+
卖出信息
+
+ 数量: {formatUSDC(order.quantity)} | 价格: {formatUSDC(order.price)} +
+
+ 金额: {formatUSDC(amount)} USDC +
+
+ + {/* 已实现盈亏 */} +
+
已实现盈亏
+
+ {formatUSDC(order.realizedPnl)} USDC +
+
+ + {/* Leader 交易ID */} +
+
Leader 交易ID
+
+ {order.leaderTradeId.slice(0, 8)}...{order.leaderTradeId.slice(-6)} +
+
+ + {/* 市场ID */} +
+
市场ID
+
+ {order.marketId.slice(0, 8)}...{order.marketId.slice(-6)} +
+
+ + {/* 创建时间 */} +
+
+ 创建时间: {formattedDate} +
+
+
+ ) + })} +
+ )} +
+ ) : ( + // 桌面端表格布局 +
`共 ${total} 条`, + onChange: (newPage, newLimit) => { + setPage(newPage) + setLimit(newLimit) + } + }} + /> + )} + + + ) +} + +export default CopyTradingSellOrdersPage + diff --git a/frontend/src/pages/CopyTradingStatistics.tsx b/frontend/src/pages/CopyTradingStatistics.tsx new file mode 100644 index 0000000..34838b8 --- /dev/null +++ b/frontend/src/pages/CopyTradingStatistics.tsx @@ -0,0 +1,275 @@ +import { useEffect, useState } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import { Card, Row, Col, Statistic, Tag, Button, message, Spin } from 'antd' +import { ArrowUpOutlined, ArrowDownOutlined, LeftOutlined } from '@ant-design/icons' +import { apiService } from '../services/api' +import { formatUSDC } from '../utils' +import { useMediaQuery } from 'react-responsive' +import type { CopyTradingStatistics } from '../types' + +const CopyTradingStatisticsPage: React.FC = () => { + const { copyTradingId } = useParams<{ copyTradingId: string }>() + const navigate = useNavigate() + const isMobile = useMediaQuery({ maxWidth: 768 }) + const [loading, setLoading] = useState(false) + const [statistics, setStatistics] = useState(null) + + useEffect(() => { + if (copyTradingId) { + fetchStatistics() + } + }, [copyTradingId]) + + const fetchStatistics = async () => { + if (!copyTradingId) return + + setLoading(true) + try { + const response = await apiService.statistics.detail({ copyTradingId: parseInt(copyTradingId) }) + if (response.data.code === 0 && response.data.data) { + setStatistics(response.data.data) + } else { + message.error(response.data.msg || '获取统计信息失败') + } + } catch (error: any) { + message.error(error.message || '获取统计信息失败') + } finally { + setLoading(false) + } + } + + const getPnlColor = (value: string): string => { + const num = parseFloat(value) + if (isNaN(num)) return '#666' + return num >= 0 ? '#3f8600' : '#cf1322' + } + + const getPnlIcon = (value: string) => { + const num = parseFloat(value) + if (isNaN(num)) return null + return num >= 0 ? : + } + + const formatPercent = (value: string): string => { + const num = parseFloat(value) + if (isNaN(num)) return '-' + return `${num >= 0 ? '+' : ''}${num.toFixed(2)}%` + } + + if (loading) { + return ( +
+ +
+ ) + } + + if (!statistics) { + return ( + +
+

暂无统计数据

+ +
+
+ ) + } + + return ( +
+ +
+
+ +

跟单关系统计

+
+
+ + + +
+
+
+ + {/* 基本信息卡片 */} + + +
+
+
账户名称
+
+ {statistics.accountName || `账户 ${statistics.accountId}`} +
+
+ + +
+
Leader 名称
+
+ {statistics.leaderName || `Leader ${statistics.leaderId}`} +
+
+ + +
+
模板名称
+
+ {statistics.templateName || `模板 ${statistics.templateId}`} +
+
+ + +
+
跟单状态
+
+ + {statistics.enabled ? '启用' : '禁用'} + +
+
+ + + + + {/* 买入统计卡片 */} + + + + + + + + + + + + + + + + + + {/* 卖出统计卡片 */} + + + + + + + + + + + + + + + {/* 持仓统计卡片 */} + + + + + + + + + + + + + + + {/* 盈亏统计卡片 */} + + + + + + + + + + + + + + + + + + ) +} + +export default CopyTradingStatisticsPage + diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index a979c8a..6a5f5c4 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -284,7 +284,24 @@ export const apiService = { * 获取分类统计 */ category: (data: { category: string; startTime?: number; endTime?: number }) => - apiClient.post>('/copy-trading/statistics/category', data) + apiClient.post>('/copy-trading/statistics/category', data), + + /** + * 获取跟单关系统计详情 + */ + detail: (data: { copyTradingId: number }) => + apiClient.post>('/copy-trading/statistics/detail', data) + }, + + /** + * 订单跟踪 API + */ + orderTracking: { + /** + * 查询订单列表(买入/卖出/匹配) + */ + list: (data: any) => + apiClient.post>('/copy-trading/orders/tracking', data) } } diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index f305655..72d02ce 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -496,3 +496,108 @@ export interface RedeemablePositionsSummary { positions: RedeemablePositionInfo[] } +/** + * 跟单关系统计信息 + */ +export interface CopyTradingStatistics { + copyTradingId: number + accountId: number + accountName: string | null + leaderId: number + leaderName: string | null + templateId: number + templateName: string | null + enabled: boolean + + // 买入统计 + totalBuyQuantity: string + totalBuyOrders: number + totalBuyAmount: string + avgBuyPrice: string + + // 卖出统计 + totalSellQuantity: string + totalSellOrders: number + totalSellAmount: string + + // 持仓统计 + currentPositionQuantity: string + currentPositionValue: string + + // 盈亏统计 + totalRealizedPnl: string + totalUnrealizedPnl: string + totalPnl: string + totalPnlPercent: string +} + +/** + * 买入订单信息 + */ +export interface BuyOrderInfo { + orderId: string + leaderTradeId: string + marketId: string + side: string + quantity: string + price: string + amount: string + matchedQuantity: string + remainingQuantity: string + status: 'filled' | 'partially_matched' | 'fully_matched' + createdAt: number +} + +/** + * 卖出订单信息 + */ +export interface SellOrderInfo { + orderId: string + leaderTradeId: string + marketId: string + side: string + quantity: string + price: string + amount: string + realizedPnl: string + createdAt: number +} + +/** + * 匹配订单信息 + */ +export interface MatchedOrderInfo { + sellOrderId: string + buyOrderId: string + matchedQuantity: string + buyPrice: string + sellPrice: string + realizedPnl: string + matchedAt: number +} + +/** + * 订单跟踪列表响应 + */ +export interface OrderTrackingListResponse { + list: BuyOrderInfo[] | SellOrderInfo[] | MatchedOrderInfo[] + total: number + page: number + limit: number +} + +/** + * 订单跟踪查询请求 + */ +export interface OrderTrackingRequest { + copyTradingId: number + type: 'buy' | 'sell' | 'matched' + page?: number + limit?: number + marketId?: string + side?: string + status?: string + sellOrderId?: string + buyOrderId?: string +} +