From ac1d6d64f217506f2348d410d60e186becc963ff Mon Sep 17 00:00:00 2001 From: codychen123 Date: Sun, 26 Apr 2026 15:10:58 +0800 Subject: [PATCH] feat: add real copy-trading pnl statistics --- Dockerfile | 13 +- .../dto/CopyTradingStatisticsDto.kt | 1 + .../statistics/CopyTradingPnlCalculator.kt | 120 +++++++++++++ .../CopyTradingStatisticsService.kt | 159 +++++++----------- .../CopyTradingPnlCalculatorTest.kt | 147 ++++++++++++++++ frontend/.eslintrc.cjs | 29 ++++ frontend/src/components/AccountImportForm.tsx | 1 - frontend/src/pages/CopyTradingList.tsx | 4 +- .../CopyTradingOrders/StatisticsModal.tsx | 34 +++- frontend/src/pages/CopyTradingStatistics.tsx | 20 ++- frontend/src/types/index.ts | 3 +- 11 files changed, 425 insertions(+), 106 deletions(-) create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingPnlCalculator.kt create mode 100644 backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingPnlCalculatorTest.kt create mode 100644 frontend/.eslintrc.cjs diff --git a/Dockerfile b/Dockerfile index bd408f3..bcb049c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -100,6 +100,17 @@ RUN if [ "$BUILD_IN_DOCKER" = "true" ]; then \ fi; \ fi +# 统一选出可执行 JAR,避免 *-plain.jar 和 bootJar 同时存在导致最终 COPY 匹配多个文件 +RUN set -e; \ + JAR="$(find build/libs -maxdepth 1 -type f -name '*.jar' ! -name '*-plain.jar' | head -n 1)"; \ + if [ -z "$JAR" ]; then \ + echo "❌ 错误:找不到可执行 JAR(已排除 *-plain.jar)"; \ + exit 1; \ + fi; \ + if [ "$JAR" != "build/libs/app.jar" ]; then \ + cp "$JAR" build/libs/app.jar; \ + fi + # ==================== 阶段3:运行环境 ==================== FROM eclipse-temurin:17-jre-jammy @@ -114,7 +125,7 @@ RUN apt-get update && \ # 从构建阶段复制文件 # 当 BUILD_IN_DOCKER=false 时,构建阶段已经复制了外部产物 COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html -COPY --from=backend-build /app/backend/build/libs/*.jar app.jar +COPY --from=backend-build /app/backend/build/libs/app.jar app.jar # 复制 Nginx 配置 COPY docker/nginx.conf /etc/nginx/nginx.conf diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingStatisticsDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingStatisticsDto.kt index 723e103..f51cdc2 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingStatisticsDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingStatisticsDto.kt @@ -24,6 +24,7 @@ data class CopyTradingStatisticsResponse( // 持仓统计 val currentPositionQuantity: String, + val currentPositionCost: String, val currentPositionValue: String, // 盈亏统计 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingPnlCalculator.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingPnlCalculator.kt new file mode 100644 index 0000000..c876ade --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingPnlCalculator.kt @@ -0,0 +1,120 @@ +package com.wrbug.polymarketbot.service.copytrading.statistics + +import com.wrbug.polymarketbot.entity.CopyOrderTracking +import com.wrbug.polymarketbot.entity.SellMatchDetail +import com.wrbug.polymarketbot.entity.SellMatchRecord +import com.wrbug.polymarketbot.util.div +import com.wrbug.polymarketbot.util.gt +import com.wrbug.polymarketbot.util.lte +import com.wrbug.polymarketbot.util.multi +import com.wrbug.polymarketbot.util.toSafeBigDecimal +import java.math.BigDecimal +import java.math.RoundingMode + +/** + * Pure calculator for copy-trading PnL. + * + * The statistics API used to expose totalPnl as realized-only PnL and hard-code + * unrealized PnL/current position value to zero. That makes active or expired + * open positions invisible. This calculator keeps the accounting explicit: + * + * - currentPositionCost: remaining shares at their tracked buy cost + * - currentPositionValue: remaining shares marked by current Polymarket price + * - totalUnrealizedPnl: current value - current cost + * - totalPnl: realized + unrealized + */ +object CopyTradingPnlCalculator { + fun calculate( + buyOrders: List, + sellRecords: List, + matchDetails: List, + quotes: List = emptyList() + ): CopyTradingPnlStatistics { + val totalBuyQuantity = buyOrders.sumOf { it.quantity.toSafeBigDecimal() } + val totalBuyAmount = buyOrders.sumOf { it.quantity.toSafeBigDecimal().multi(it.price) } + val totalBuyOrders = buyOrders.size.toLong() + val avgBuyPrice = if (totalBuyQuantity.gt(BigDecimal.ZERO)) { + totalBuyAmount.div(totalBuyQuantity) + } else { + BigDecimal.ZERO + } + + val totalSellQuantity = sellRecords.sumOf { it.totalMatchedQuantity.toSafeBigDecimal() } + val totalSellAmount = matchDetails.sumOf { it.matchedQuantity.toSafeBigDecimal().multi(it.sellPrice) } + val totalSellOrders = sellRecords.size.toLong() + + val openOrders = buyOrders.filter { it.remainingQuantity.toSafeBigDecimal().gt(BigDecimal.ZERO) } + val currentPositionQuantity = openOrders.sumOf { it.remainingQuantity.toSafeBigDecimal() } + val currentPositionCost = openOrders.sumOf { it.remainingQuantity.toSafeBigDecimal().multi(it.price) } + val currentPositionValue = openOrders.sumOf { order -> + val currentPrice = findQuote(order, quotes)?.currentPrice ?: BigDecimal.ZERO + order.remainingQuantity.toSafeBigDecimal().multi(currentPrice) + } + + val totalRealizedPnl = matchDetails.sumOf { it.realizedPnl.toSafeBigDecimal() } + val totalUnrealizedPnl = currentPositionValue.subtract(currentPositionCost) + val totalPnl = totalRealizedPnl.add(totalUnrealizedPnl) + + return CopyTradingPnlStatistics( + totalBuyQuantity = totalBuyQuantity, + totalBuyOrders = totalBuyOrders, + totalBuyAmount = totalBuyAmount, + avgBuyPrice = avgBuyPrice, + totalSellQuantity = totalSellQuantity, + totalSellOrders = totalSellOrders, + totalSellAmount = totalSellAmount, + currentPositionQuantity = currentPositionQuantity, + currentPositionCost = currentPositionCost, + currentPositionValue = currentPositionValue, + totalRealizedPnl = totalRealizedPnl, + totalUnrealizedPnl = totalUnrealizedPnl, + totalPnl = totalPnl, + totalPnlPercent = calculatePnlPercent(totalBuyAmount, totalPnl) + ) + } + + private fun findQuote( + order: CopyOrderTracking, + quotes: List + ): PositionValuationQuote? { + return quotes.firstOrNull { quote -> + quote.marketId == order.marketId && + order.outcomeIndex != null && + quote.outcomeIndex == order.outcomeIndex + } ?: quotes.firstOrNull { quote -> + quote.marketId == order.marketId && + order.outcomeIndex == null && + !quote.side.isNullOrBlank() && + quote.side.equals(order.side, ignoreCase = true) + } + } + + private fun calculatePnlPercent(totalBuyAmount: BigDecimal, totalPnl: BigDecimal): BigDecimal { + if (totalBuyAmount.lte(BigDecimal.ZERO)) return BigDecimal.ZERO.setScale(2) + return totalPnl.div(totalBuyAmount).multi(100).setScale(2, RoundingMode.HALF_UP) + } +} + +data class PositionValuationQuote( + val marketId: String, + val outcomeIndex: Int?, + val side: String?, + val currentPrice: BigDecimal +) + +data class CopyTradingPnlStatistics( + val totalBuyQuantity: BigDecimal, + val totalBuyOrders: Long, + val totalBuyAmount: BigDecimal, + val avgBuyPrice: BigDecimal, + val totalSellQuantity: BigDecimal, + val totalSellOrders: Long, + val totalSellAmount: BigDecimal, + val currentPositionQuantity: BigDecimal, + val currentPositionCost: BigDecimal, + val currentPositionValue: BigDecimal, + val totalRealizedPnl: BigDecimal, + val totalUnrealizedPnl: BigDecimal, + val totalPnl: BigDecimal, + val totalPnlPercent: BigDecimal +) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingStatisticsService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingStatisticsService.kt index dda46f8..80b7b57 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingStatisticsService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingStatisticsService.kt @@ -7,13 +7,11 @@ import com.wrbug.polymarketbot.util.toSafeBigDecimal import com.wrbug.polymarketbot.util.multi import com.wrbug.polymarketbot.util.div import com.wrbug.polymarketbot.util.gt -import com.wrbug.polymarketbot.util.eq import com.wrbug.polymarketbot.util.lte import org.slf4j.LoggerFactory import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable import org.springframework.data.domain.Sort -import com.wrbug.polymarketbot.service.accounts.AccountService import com.wrbug.polymarketbot.service.common.BlockchainService import org.springframework.stereotype.Service import java.math.BigDecimal @@ -31,7 +29,8 @@ class CopyTradingStatisticsService( private val sellMatchDetailRepository: SellMatchDetailRepository, private val accountRepository: AccountRepository, private val leaderRepository: LeaderRepository, - private val marketService: com.wrbug.polymarketbot.service.common.MarketService + private val marketService: com.wrbug.polymarketbot.service.common.MarketService, + private val blockchainService: BlockchainService ) { private val logger = LoggerFactory.getLogger(CopyTradingStatisticsService::class.java) @@ -58,15 +57,14 @@ class CopyTradingStatisticsService( // 5. 获取匹配明细 val matchDetails = sellMatchDetailRepository.findByCopyTradingId(copyTradingId) - // 6. 计算统计信息 - val statistics = calculateStatistics(buyOrders, sellRecords, matchDetails) + // 6. 获取当前价格并计算真实口径统计 + // currentPositionCost 使用跟单系统记录的剩余仓位成本;currentPositionValue 使用 + // Polymarket Data API 当前价格按剩余份额估值。若某个未平仓仓位没有报价,按 0 + // 估值,避免已归零/待赎回仓位继续被统计成成本价。 + val quotes = buildPositionValuationQuotes(account?.proxyAddress) + val statistics = CopyTradingPnlCalculator.calculate(buyOrders, sellRecords, matchDetails, quotes) - // 7. 不再计算未实现盈亏和持仓价值(优化性能) - // 未实现盈亏计算需要查询链上持仓和市场价格,性能开销大 - val unrealizedPnl = "0" - val positionValue = "0" - - // 8. 构建响应(总盈亏 = 已实现盈亏) + // 7. 构建响应(总盈亏 = 已实现盈亏 + 未实现盈亏) val response = CopyTradingStatisticsResponse( copyTradingId = copyTradingId, accountId = copyTrading.accountId, @@ -74,19 +72,20 @@ class CopyTradingStatisticsService( leaderId = copyTrading.leaderId, leaderName = leader?.leaderName, enabled = copyTrading.enabled, - totalBuyQuantity = statistics.totalBuyQuantity, + totalBuyQuantity = statistics.totalBuyQuantity.toString(), totalBuyOrders = statistics.totalBuyOrders, - totalBuyAmount = statistics.totalBuyAmount, - avgBuyPrice = statistics.avgBuyPrice, - totalSellQuantity = statistics.totalSellQuantity, + totalBuyAmount = statistics.totalBuyAmount.toString(), + avgBuyPrice = statistics.avgBuyPrice.toString(), + totalSellQuantity = statistics.totalSellQuantity.toString(), totalSellOrders = statistics.totalSellOrders, - totalSellAmount = statistics.totalSellAmount, - currentPositionQuantity = statistics.currentPositionQuantity, - currentPositionValue = positionValue, - totalRealizedPnl = statistics.totalRealizedPnl, - totalUnrealizedPnl = unrealizedPnl, - totalPnl = statistics.totalRealizedPnl, - totalPnlPercent = calculatePnlPercentOnlyRealized(statistics.totalBuyAmount, statistics.totalRealizedPnl) + totalSellAmount = statistics.totalSellAmount.toString(), + currentPositionQuantity = statistics.currentPositionQuantity.toString(), + currentPositionCost = statistics.currentPositionCost.toString(), + currentPositionValue = statistics.currentPositionValue.toString(), + totalRealizedPnl = statistics.totalRealizedPnl.toString(), + totalUnrealizedPnl = statistics.totalUnrealizedPnl.toString(), + totalPnl = statistics.totalPnl.toString(), + totalPnlPercent = statistics.totalPnlPercent.toString() ) Result.success(response) @@ -96,6 +95,48 @@ class CopyTradingStatisticsService( } } + /** + * 获取账户当前仓位报价,用于给跟单系统中仍有 remainingQuantity 的订单做市值估算。 + * + * 注意:报价只用于估值,不直接使用 Data API 的 size/currentValue 汇总;这样可以按 + * copyTradingId 归因,避免同一钱包下多个 Leader 或手工仓位混在一起。 + */ + private suspend fun buildPositionValuationQuotes(proxyAddress: String?): List { + if (proxyAddress.isNullOrBlank()) return emptyList() + + return try { + val positionsResult = blockchainService.getPositions(proxyAddress) + if (positionsResult.isFailure) { + logger.warn("获取持仓报价失败: proxyAddress=${proxyAddress.take(10)}..., error=${positionsResult.exceptionOrNull()?.message}") + return emptyList() + } + + positionsResult.getOrNull().orEmpty().mapNotNull { position -> + val marketId = position.conditionId?.takeIf { it.isNotBlank() } ?: return@mapNotNull null + val currentPrice = position.curPrice?.toSafeBigDecimal() + ?: derivePriceFromPositionValue(position.currentValue, position.size) + ?: BigDecimal.ZERO + + PositionValuationQuote( + marketId = marketId, + outcomeIndex = position.outcomeIndex, + side = position.outcome, + currentPrice = currentPrice + ) + } + } catch (e: Exception) { + logger.warn("获取持仓报价异常: proxyAddress=${proxyAddress.take(10)}..., error=${e.message}", e) + emptyList() + } + } + + private fun derivePriceFromPositionValue(currentValue: Double?, size: Double?): BigDecimal? { + val value = currentValue?.toSafeBigDecimal() ?: return null + val quantity = size?.toSafeBigDecimal() ?: return null + if (quantity.lte(BigDecimal.ZERO)) return null + return value.div(quantity) + } + /** * 查询订单列表 */ @@ -337,65 +378,6 @@ class CopyTradingStatisticsService( return Pair(list, total) } - /** - * 计算统计信息 - */ - private fun calculateStatistics( - buyOrders: List, - sellRecords: List, - matchDetails: List - ): StatisticsData { - // 买入统计 - val totalBuyQuantity = buyOrders.sumOf { it.quantity.toSafeBigDecimal() } - val totalBuyAmount = buyOrders.sumOf { it.quantity.toSafeBigDecimal().multi(it.price) } - val totalBuyOrders = buyOrders.size.toLong() - val avgBuyPrice = if (totalBuyQuantity.gt(BigDecimal.ZERO)) { - totalBuyAmount.div(totalBuyQuantity) - } else { - BigDecimal.ZERO - } - - // 卖出统计 - // 使用 SellMatchDetail 计算总卖出金额,确保准确性 - // 因为每个明细都记录了准确的匹配数量和卖出价格 - val totalSellQuantity = sellRecords.sumOf { it.totalMatchedQuantity.toSafeBigDecimal() } - val totalSellAmount = matchDetails.sumOf { it.matchedQuantity.toSafeBigDecimal().multi(it.sellPrice) } - val totalSellOrders = sellRecords.size.toLong() - - // 持仓统计 - val currentPositionQuantity = buyOrders.sumOf { it.remainingQuantity.toSafeBigDecimal() } - - // 已实现盈亏 - val totalRealizedPnl = matchDetails.sumOf { it.realizedPnl.toSafeBigDecimal() } - - return StatisticsData( - totalBuyQuantity = totalBuyQuantity.toString(), - totalBuyOrders = totalBuyOrders, - totalBuyAmount = totalBuyAmount.toString(), - avgBuyPrice = avgBuyPrice.toString(), - totalSellQuantity = totalSellQuantity.toString(), - totalSellOrders = totalSellOrders, - totalSellAmount = totalSellAmount.toString(), - currentPositionQuantity = currentPositionQuantity.toString(), - totalRealizedPnl = totalRealizedPnl.toString() - ) - } - - /** - * 计算盈亏百分比(仅基于已实现盈亏) - */ - private fun calculatePnlPercentOnlyRealized( - totalBuyAmount: String, - totalRealizedPnl: String - ): String { - val buyAmount = totalBuyAmount.toSafeBigDecimal() - if (buyAmount.lte(BigDecimal.ZERO)) return "0" - - val percent = totalRealizedPnl.toSafeBigDecimal().div(buyAmount).multi(100) - - return percent.setScale(2, RoundingMode.HALF_UP).toString() - } - /** * 获取全局统计 */ @@ -556,21 +538,6 @@ class CopyTradingStatisticsService( ) } - /** - * 统计数据结构 - */ - private data class StatisticsData( - val totalBuyQuantity: String, - val totalBuyOrders: Long, - val totalBuyAmount: String, - val avgBuyPrice: String, - val totalSellQuantity: String, - val totalSellOrders: Long, - val totalSellAmount: String, - val currentPositionQuantity: String, - val totalRealizedPnl: String - ) - /** * 获取按市场分组的买入订单列表 */ diff --git a/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingPnlCalculatorTest.kt b/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingPnlCalculatorTest.kt new file mode 100644 index 0000000..92597b3 --- /dev/null +++ b/backend/src/test/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingPnlCalculatorTest.kt @@ -0,0 +1,147 @@ +package com.wrbug.polymarketbot.service.copytrading.statistics + +import com.wrbug.polymarketbot.entity.CopyOrderTracking +import com.wrbug.polymarketbot.entity.SellMatchDetail +import com.wrbug.polymarketbot.entity.SellMatchRecord +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +class CopyTradingPnlCalculatorTest { + + @Test + fun `marks open positions with current prices and combines realized and unrealized pnl`() { + val buyOrders = listOf( + buyOrder( + id = 1, + marketId = "market-a", + outcomeIndex = 0, + quantity = "10", + price = "0.60", + matchedQuantity = "6", + remainingQuantity = "4" + ), + buyOrder( + id = 2, + marketId = "market-b", + outcomeIndex = 1, + quantity = "5", + price = "0.20", + matchedQuantity = "0", + remainingQuantity = "5" + ) + ) + val sellRecords = listOf( + sellRecord(quantity = "6", price = "0.85", pnl = "1.50") + ) + val matchDetails = listOf( + matchDetail(trackingId = 1, buyOrderId = "buy-1", quantity = "6", buyPrice = "0.60", sellPrice = "0.85", pnl = "1.50") + ) + val quotes = listOf( + PositionValuationQuote(marketId = "market-a", outcomeIndex = 0, side = "0", currentPrice = bd("0.40")), + PositionValuationQuote(marketId = "market-b", outcomeIndex = 1, side = "1", currentPrice = bd("0.05")) + ) + + val stats = CopyTradingPnlCalculator.calculate(buyOrders, sellRecords, matchDetails, quotes) + + assertEquals("3.40", stats.currentPositionCost.toPlainString()) + assertEquals("1.85", stats.currentPositionValue.toPlainString()) + assertEquals("-1.55", stats.totalUnrealizedPnl.toPlainString()) + assertEquals("1.50", stats.totalRealizedPnl.toPlainString()) + assertEquals("-0.05", stats.totalPnl.toPlainString()) + assertEquals("-0.71", stats.totalPnlPercent.toPlainString()) + } + + @Test + fun `treats tracked open positions without a quote as zero current value`() { + val buyOrders = listOf( + buyOrder( + id = 1, + marketId = "expired-market", + outcomeIndex = 0, + quantity = "8", + price = "0.25", + matchedQuantity = "0", + remainingQuantity = "8" + ) + ) + + val stats = CopyTradingPnlCalculator.calculate( + buyOrders = buyOrders, + sellRecords = emptyList(), + matchDetails = emptyList(), + quotes = emptyList() + ) + + assertEquals("2.00", stats.currentPositionCost.toPlainString()) + assertEquals("0", stats.currentPositionValue.toPlainString()) + assertEquals("-2.00", stats.totalUnrealizedPnl.toPlainString()) + assertEquals("-2.00", stats.totalPnl.toPlainString()) + assertEquals("-100.00", stats.totalPnlPercent.toPlainString()) + } + + private fun buyOrder( + id: Long, + marketId: String, + outcomeIndex: Int?, + quantity: String, + price: String, + matchedQuantity: String, + remainingQuantity: String + ) = CopyOrderTracking( + id = id, + copyTradingId = 1, + accountId = 1, + leaderId = 1, + marketId = marketId, + side = outcomeIndex?.toString() ?: "YES", + outcomeIndex = outcomeIndex, + buyOrderId = "buy-$id", + leaderBuyTradeId = "leader-buy-$id", + leaderBuyQuantity = null, + quantity = bd(quantity), + price = bd(price), + matchedQuantity = bd(matchedQuantity), + remainingQuantity = bd(remainingQuantity), + status = if (bd(remainingQuantity).signum() == 0) "fully_matched" else "filled", + source = "test", + createdAt = id, + updatedAt = id + ) + + private fun sellRecord(quantity: String, price: String, pnl: String) = SellMatchRecord( + id = 1, + copyTradingId = 1, + sellOrderId = "sell-1", + leaderSellTradeId = "leader-sell-1", + marketId = "market-a", + side = "0", + outcomeIndex = 0, + totalMatchedQuantity = bd(quantity), + sellPrice = bd(price), + totalRealizedPnl = bd(pnl), + priceUpdated = true, + createdAt = 1 + ) + + private fun matchDetail( + trackingId: Long, + buyOrderId: String, + quantity: String, + buyPrice: String, + sellPrice: String, + pnl: String + ) = SellMatchDetail( + id = trackingId, + matchRecordId = 1, + trackingId = trackingId, + buyOrderId = buyOrderId, + matchedQuantity = bd(quantity), + buyPrice = bd(buyPrice), + sellPrice = bd(sellPrice), + realizedPnl = bd(pnl), + createdAt = 1 + ) + + private fun bd(value: String) = BigDecimal(value) +} diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..16580f9 --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,29 @@ +module.exports = { + root: true, + env: { + browser: true, + es2020: true, + }, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: ['@typescript-eslint', 'react-hooks', 'react-refresh'], + extends: [ + 'eslint:recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', 'build', 'node_modules', '.eslintrc.cjs'], + rules: { + // TypeScript 编译阶段已经开启 strict/noUnused*,这里避免历史代码里 + // 大量 any、依赖数组 warning 让 lint 从“可运行检查”变成一次性大迁移。 + 'react-hooks/exhaustive-deps': 'off', + 'react-refresh/only-export-components': 'off', + 'no-undef': 'off', + 'no-unused-vars': 'off', + 'no-useless-catch': 'off', + 'no-useless-escape': 'off', + 'no-redeclare': 'off', + }, +} diff --git a/frontend/src/components/AccountImportForm.tsx b/frontend/src/components/AccountImportForm.tsx index 265283a..ebeaf51 100644 --- a/frontend/src/components/AccountImportForm.tsx +++ b/frontend/src/components/AccountImportForm.tsx @@ -197,7 +197,6 @@ const AccountImportForm: React.FC = ({ setSelectedProxyType('') setStep('input') form.setFieldsValue({ walletAddress: '', privateKey: '', mnemonic: '' }) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [importType]) const handleSubmit = async (values: any) => { diff --git a/frontend/src/pages/CopyTradingList.tsx b/frontend/src/pages/CopyTradingList.tsx index 5e204f8..9f97484 100644 --- a/frontend/src/pages/CopyTradingList.tsx +++ b/frontend/src/pages/CopyTradingList.tsx @@ -256,7 +256,7 @@ const CopyTradingList: React.FC = () => { ) }, { - title: t('copyTradingList.totalPnl') || '总盈亏', + title: '总盈亏(含未实现)', key: 'totalPnl', width: isMobile ? 100 : 150, render: (_: any, record: CopyTrading) => { @@ -537,7 +537,7 @@ const CopyTradingList: React.FC = () => {
- {t('copyTradingList.totalPnl') || '总盈亏'} + 总盈亏(含未实现)
{stats ? (
= ({
- {t('copyTradingOrders.totalPnl') || '总盈亏'} + {t('copyTradingOrders.currentPositionCost') || '当前持仓成本'} +
+
+ {formatUSDC(statistics.currentPositionCost)} USDC +
+
+
+
+ {t('copyTradingOrders.currentPositionValue') || '当前持仓市值'} +
+
+ {formatUSDC(statistics.currentPositionValue)} USDC +
+
+
+
+ {t('copyTradingOrders.totalPnl') || '总盈亏(含未实现)'}
{getPnlIcon(statistics.totalPnl)} @@ -179,7 +195,21 @@ const StatisticsModal: React.FC = ({ + + + + + + { {/* 持仓统计卡片 */} - + - + + + + + + + {