From c4365283cf7b749e4cf684f14744c48e71f14002 Mon Sep 17 00:00:00 2001 From: Macbook Date: Sun, 7 Jun 2026 14:55:35 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B6=84=EC=84=9D=EB=A0=88=ED=8F=AC=ED=8A=B8?= =?UTF-8?q?=20=EC=88=98=EC=9D=B5=EB=A5=A0,=20=EC=8A=B9=EB=A5=A0=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/BacktestingService.java | 71 +++++++++++-------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/backend/src/main/java/com/goldenchart/service/BacktestingService.java b/backend/src/main/java/com/goldenchart/service/BacktestingService.java index 47dab30..6936a86 100644 --- a/backend/src/main/java/com/goldenchart/service/BacktestingService.java +++ b/backend/src/main/java/com/goldenchart/service/BacktestingService.java @@ -146,6 +146,8 @@ public class BacktestingService { boolean partialDone = false; double equity = initCap; + final double[] simGrossProfit = {0.0}; + final double[] simGrossLoss = {0.0}; int barCount = series.getBarCount(); @@ -185,7 +187,8 @@ public class BacktestingService { ? (entryPrice - effExit) / entryPrice : (effExit - entryPrice) / entryPrice; double size = partialDone ? (1.0 - partialPct) : 1.0; - equity += equity * tradeSizePct * size * (rawReturn - commission); + equity = applyEquityPnl(equity, tradeSizePct, size, rawReturn - commission, + simGrossProfit, simGrossLoss); Num numExitPrice = series.numFactory().numOf(effExit); Num numShares = record.getCurrentPosition().getEntry().getAmount(); record.exit(i, numExitPrice, numShares); @@ -231,7 +234,8 @@ public class BacktestingService { ? (entryPrice - effExit) / entryPrice : (effExit - entryPrice) / entryPrice; double commission = calcCommissionRate(cfg) * 2; - equity += equity * tradeSizePct * partialPct * (partReturn - commission); + equity = applyEquityPnl(equity, tradeSizePct, partialPct, partReturn - commission, + simGrossProfit, simGrossLoss); signals.add(Signal.builder() .time(time).type("PARTIAL_SELL").price(effExit).barIndex(i).build()); @@ -248,7 +252,8 @@ public class BacktestingService { double netReturn = rawReturn - commission; double size = partialDone ? (1.0 - partialPct) : 1.0; - equity += equity * tradeSizePct * size * netReturn; + equity = applyEquityPnl(equity, tradeSizePct, size, netReturn, + simGrossProfit, simGrossLoss); Num numExitPrice = series.numFactory().numOf(effExit); Num numShares = record.getCurrentPosition().getEntry().getAmount(); @@ -266,7 +271,8 @@ public class BacktestingService { } // ── AnalysisCriterion 전체 계산 ─────────────────────────────────────── - BacktestAnalysisDto analysis = calcAnalysis(series, record, cfg, initCap, equity); + BacktestAnalysisDto analysis = calcAnalysis(series, record, cfg, initCap, equity, + simGrossProfit[0], simGrossLoss[0]); // ── Stats (하위 호환) ───────────────────────────────────────────────── Stats stats = toStats(analysis, signals); @@ -285,16 +291,18 @@ public class BacktestingService { // ── Ta4j AnalysisCriterion 전체 계산 ───────────────────────────────────── private BacktestAnalysisDto calcAnalysis(BarSeries series, TradingRecord record, - BacktestSettingsDto cfg, double initCap, double finalEquity) { + BacktestSettingsDto cfg, double initCap, double finalEquity, + double simGrossProfit, double simGrossLoss) { BacktestAnalysisDto.BacktestAnalysisDtoBuilder b = BacktestAnalysisDto.builder() .initialCapital(initCap) .finalEquity(finalEquity); - double totalReturnPct = safeCalc(() -> calcTotalReturnPct(series, record)); - double grossProfit = safeCalc(() -> calcCriterion("org.ta4j.core.criteria.pnl.GrossProfitCriterion", series, record)); - double grossLoss = safeCalc(() -> calcCriterion("org.ta4j.core.criteria.pnl.GrossLossCriterion", series, record)); - double avgReturnPct = safeCalc(() -> calcCriterion("org.ta4j.core.criteria.pnl.AverageProfitLossCriterion", series, record)); - double profitLossRatio = safeCalc(() -> calcCriterion("org.ta4j.core.criteria.pnl.ProfitLossRatioCriterion", series, record)); + // equity 시뮬레이션 기준 총 수익률 — Ta4j criterion/fallback 은 개별 거래 수익률 합산으로 부정확 + double totalReturnPct = initCap > 0 ? (finalEquity - initCap) / initCap : 0.0; + double grossProfit = simGrossProfit; + double grossLoss = simGrossLoss; + double avgReturnPct = 0.0; + double profitLossRatio = 0.0; int positions = safeCalcInt(() -> calcCriterionInt("org.ta4j.core.criteria.NumberOfPositionsCriterion", series, record)); int winning = safeCalcInt(() -> calcCriterionInt("org.ta4j.core.criteria.NumberOfWinningPositionsCriterion", series, record)); @@ -325,6 +333,19 @@ public class BacktestingService { } if (winRate == 0 && positions > 0) winRate = (double) winning / positions; + if (positions > 0) { + avgReturnPct = totalReturnPct / positions; + } else { + avgReturnPct = safeCalc(() -> calcCriterion( + "org.ta4j.core.criteria.pnl.AverageProfitLossCriterion", series, record)); + } + if (grossLoss != 0) { + profitLossRatio = grossProfit / Math.abs(grossLoss); + } else if (grossProfit > 0) { + profitLossRatio = safeCalc(() -> calcCriterion( + "org.ta4j.core.criteria.pnl.ProfitLossRatioCriterion", series, record)); + } + return b .totalReturnPct(totalReturnPct) .totalProfitLoss(totalPnl) @@ -351,24 +372,18 @@ public class BacktestingService { // ── 개별 Criterion 계산 헬퍼 ───────────────────────────────────────────── - private double calcTotalReturnPct(BarSeries series, TradingRecord record) throws Exception { - // 총 수익률 = (finalEquity - initialCapital) / initialCapital - // TotalProfitLossPercentageCriterion 또는 ReturnCriterion 시도 - try { - Class cls = Class.forName("org.ta4j.core.criteria.pnl.TotalProfitLossPercentageCriterion"); - AnalysisCriterion criterion = (AnalysisCriterion) cls.getDeclaredConstructor().newInstance(); - return criterion.calculate(series, record).doubleValue(); - } catch (Exception ignored) {} - try { - Class cls = Class.forName("org.ta4j.core.criteria.ReturnCriterion"); - AnalysisCriterion criterion = (AnalysisCriterion) cls.getDeclaredConstructor().newInstance(); - return criterion.calculate(series, record).doubleValue() - 1.0; - } catch (Exception ignored) {} - // fallback: 거래 수익 직접 계산 - return record.getPositions().stream() - .filter(Position::isClosed) - .mapToDouble(p -> p.getProfit().doubleValue() / p.getEntry().getNetPrice().doubleValue()) - .sum(); + /** + * equity 시뮬레이션에 체결 손익을 반영하고, 총 이익/총 손실 누적값을 갱신한다. + * @return 갱신된 equity + */ + private double applyEquityPnl(double equity, double tradeSizePct, double size, double netReturn, + double[] grossProfitAcc, double[] grossLossAcc) { + double eqBefore = equity; + double next = eqBefore + eqBefore * tradeSizePct * size * netReturn; + double delta = next - eqBefore; + if (delta >= 0) grossProfitAcc[0] += delta; + else grossLossAcc[0] += delta; + return next; } private double calcMaxDrawdown(BarSeries series, TradingRecord record) throws Exception {