feat: add real copy-trading pnl statistics
This commit is contained in:
+12
-1
@@ -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
|
||||
|
||||
@@ -24,6 +24,7 @@ data class CopyTradingStatisticsResponse(
|
||||
|
||||
// 持仓统计
|
||||
val currentPositionQuantity: String,
|
||||
val currentPositionCost: String,
|
||||
val currentPositionValue: String,
|
||||
|
||||
// 盈亏统计
|
||||
|
||||
+120
@@ -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<CopyOrderTracking>,
|
||||
sellRecords: List<SellMatchRecord>,
|
||||
matchDetails: List<SellMatchDetail>,
|
||||
quotes: List<PositionValuationQuote> = 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>
|
||||
): 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
|
||||
)
|
||||
+63
-96
@@ -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<PositionValuationQuote> {
|
||||
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<CopyOrderTracking>,
|
||||
sellRecords: List<SellMatchRecord>,
|
||||
matchDetails: List<SellMatchDetail>
|
||||
): 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
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取按市场分组的买入订单列表
|
||||
*/
|
||||
|
||||
+147
@@ -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)
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
@@ -197,7 +197,6 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
||||
setSelectedProxyType('')
|
||||
setStep('input')
|
||||
form.setFieldsValue({ walletAddress: '', privateKey: '', mnemonic: '' })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [importType])
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
|
||||
@@ -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 = () => {
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>
|
||||
{t('copyTradingList.totalPnl') || '总盈亏'}
|
||||
总盈亏(含未实现)
|
||||
</div>
|
||||
{stats ? (
|
||||
<div style={{
|
||||
|
||||
@@ -118,7 +118,23 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.totalPnl') || '总盈亏'}
|
||||
{t('copyTradingOrders.currentPositionCost') || '当前持仓成本'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333', flex: '1', textAlign: 'right' }}>
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.currentPositionCost)} USDC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.currentPositionValue') || '当前持仓市值'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333', flex: '1', textAlign: 'right' }}>
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.currentPositionValue)} USDC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.totalPnl') || '总盈亏(含未实现)'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: 'bold', color: getPnlColor(statistics.totalPnl), flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
|
||||
{getPnlIcon(statistics.totalPnl)}
|
||||
@@ -179,7 +195,21 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.totalPnl') || '总盈亏'}
|
||||
title={t('copyTradingOrders.currentPositionCost') || '当前持仓成本'}
|
||||
value={formatUSDC(statistics.currentPositionCost)}
|
||||
suffix="USDC"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.currentPositionValue') || '当前持仓市值'}
|
||||
value={formatUSDC(statistics.currentPositionValue)}
|
||||
suffix="USDC"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.totalPnl') || '总盈亏(含未实现)'}
|
||||
value={formatUSDC(statistics.totalPnl)}
|
||||
suffix="USDC"
|
||||
valueStyle={{ color: getPnlColor(statistics.totalPnl) }}
|
||||
|
||||
@@ -195,14 +195,28 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
||||
{/* 持仓统计卡片 */}
|
||||
<Card title="持仓统计" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={12}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="当前持仓数量"
|
||||
value={formatNumber(statistics.currentPositionQuantity, 4)}
|
||||
suffix=""
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={12}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="当前持仓成本"
|
||||
value={formatUSDC(statistics.currentPositionCost)}
|
||||
suffix="USDC"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="当前持仓市值"
|
||||
value={formatUSDC(statistics.currentPositionValue)}
|
||||
suffix="USDC"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="平均买入价格"
|
||||
value={formatNumber(statistics.avgBuyPrice, 4)}
|
||||
@@ -235,7 +249,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="总盈亏"
|
||||
title="总盈亏(含未实现)"
|
||||
value={formatUSDC(statistics.totalPnl)}
|
||||
valueStyle={{ color: getPnlColor(statistics.totalPnl) }}
|
||||
prefix={getPnlIcon(statistics.totalPnl)}
|
||||
|
||||
@@ -705,7 +705,8 @@ export interface CopyTradingStatistics {
|
||||
|
||||
// 持仓统计
|
||||
currentPositionQuantity: string
|
||||
currentPositionValue: string // 当前实现总是返回 "0",保留用于未来扩展
|
||||
currentPositionCost: string
|
||||
currentPositionValue: string // 按当前价格估算的持仓市值
|
||||
|
||||
// 盈亏统计
|
||||
totalRealizedPnl: string
|
||||
|
||||
Reference in New Issue
Block a user