From 748c871af4484d69527fbba94417f79f83008923 Mon Sep 17 00:00:00 2001 From: WrBug Date: Sat, 28 Mar 2026 18:06:06 +0800 Subject: [PATCH] fix: backtest equity curve bugs (#39) - Fix SELL logic: correctly reduce position.quantity after selling (core bug) Previously positions were never decremented, causing sold positions to be settled again at backtest end, inflating final balance. - Fix settleRemainingPositions: profitLoss now correctly computes settlementValue - cost (was incorrectly using settlementValue.negate()) - Fix max drawdown calculation: use current balance instead of previous iteration's runningBalance - Frontend: add comment noting chart shows cash balance, not total equity --- .../backtest/BacktestExecutionService.kt | 25 ++++++++++++++++--- frontend/src/pages/BacktestChart.tsx | 2 ++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/backtest/BacktestExecutionService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/backtest/BacktestExecutionService.kt index 193eac0f..fb10e8eb 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/backtest/BacktestExecutionService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/backtest/BacktestExecutionService.kt @@ -408,10 +408,25 @@ class BacktestExecutionService( val cost = actualSellQuantity.multiply(position.avgPrice) val profitLoss = netAmount.subtract(cost) - // 更新余额和持仓 + // Bug #39 Fix: correctly reduce position quantity after sell currentBalance += netAmount - if (position.quantity <= BigDecimal.ZERO) { + val remainingQuantity = position.quantity - actualSellQuantity + val remainingLeaderBuyQuantity = if (position.leaderBuyQuantity != null && position.leaderBuyQuantity > BigDecimal.ZERO) { + val totalQty = position.quantity + val leaderReduction = actualSellQuantity.divide( + totalQty, 8, java.math.RoundingMode.DOWN + ) + (position.leaderBuyQuantity - leaderReduction).coerceAtLeast(BigDecimal.ZERO) + } else { + position.leaderBuyQuantity + } + if (remainingQuantity <= BigDecimal.ZERO) { positions.remove(positionKey) + } else { + positions[positionKey] = position.copy( + quantity = remainingQuantity, + leaderBuyQuantity = remainingLeaderBuyQuantity + ) } // 记录交易到当前页列表 @@ -632,7 +647,8 @@ class BacktestExecutionService( val settlementPrice = avgPrice val settlementValue = quantity.multiply(settlementPrice) - val profitLoss = settlementValue.negate() + // Bug #39 Fix: profitLoss for closed settlement at avgPrice should be ~0 + val profitLoss = settlementValue.subtract(quantity.multiply(avgPrice)) balance += settlementValue @@ -702,7 +718,8 @@ class BacktestExecutionService( if (balance > peakBalance) { peakBalance = balance } - val drawdown = peakBalance - runningBalance + // Bug #39 Fix: use current balance, not runningBalance from previous iteration + val drawdown = peakBalance - balance if (drawdown > maxDrawdown) { maxDrawdown = drawdown } diff --git a/frontend/src/pages/BacktestChart.tsx b/frontend/src/pages/BacktestChart.tsx index 8e9b8745..7d53a489 100644 --- a/frontend/src/pages/BacktestChart.tsx +++ b/frontend/src/pages/BacktestChart.tsx @@ -10,6 +10,8 @@ interface BacktestChartProps { }[] } +// Bug #39 Note: This chart currently displays cash balance (balanceAfter), not total equity. +// A true equity curve (cash + position value) would require an equityAfter field in the trade records. const BacktestChart: React.FC = ({ trades }) => { const { t } = useTranslation() const chartRef = useRef(null)