분석레포트 수익률, 승률 계산오류 수정

This commit is contained in:
Macbook
2026-06-07 14:55:35 +09:00
parent a3024ccdf9
commit c4365283cf
@@ -146,6 +146,8 @@ public class BacktestingService {
boolean partialDone = false; boolean partialDone = false;
double equity = initCap; double equity = initCap;
final double[] simGrossProfit = {0.0};
final double[] simGrossLoss = {0.0};
int barCount = series.getBarCount(); int barCount = series.getBarCount();
@@ -185,7 +187,8 @@ public class BacktestingService {
? (entryPrice - effExit) / entryPrice ? (entryPrice - effExit) / entryPrice
: (effExit - entryPrice) / entryPrice; : (effExit - entryPrice) / entryPrice;
double size = partialDone ? (1.0 - partialPct) : 1.0; 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 numExitPrice = series.numFactory().numOf(effExit);
Num numShares = record.getCurrentPosition().getEntry().getAmount(); Num numShares = record.getCurrentPosition().getEntry().getAmount();
record.exit(i, numExitPrice, numShares); record.exit(i, numExitPrice, numShares);
@@ -231,7 +234,8 @@ public class BacktestingService {
? (entryPrice - effExit) / entryPrice ? (entryPrice - effExit) / entryPrice
: (effExit - entryPrice) / entryPrice; : (effExit - entryPrice) / entryPrice;
double commission = calcCommissionRate(cfg) * 2; double commission = calcCommissionRate(cfg) * 2;
equity += equity * tradeSizePct * partialPct * (partReturn - commission); equity = applyEquityPnl(equity, tradeSizePct, partialPct, partReturn - commission,
simGrossProfit, simGrossLoss);
signals.add(Signal.builder() signals.add(Signal.builder()
.time(time).type("PARTIAL_SELL").price(effExit).barIndex(i).build()); .time(time).type("PARTIAL_SELL").price(effExit).barIndex(i).build());
@@ -248,7 +252,8 @@ public class BacktestingService {
double netReturn = rawReturn - commission; double netReturn = rawReturn - commission;
double size = partialDone ? (1.0 - partialPct) : 1.0; 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 numExitPrice = series.numFactory().numOf(effExit);
Num numShares = record.getCurrentPosition().getEntry().getAmount(); Num numShares = record.getCurrentPosition().getEntry().getAmount();
@@ -266,7 +271,8 @@ public class BacktestingService {
} }
// ── AnalysisCriterion 전체 계산 ─────────────────────────────────────── // ── AnalysisCriterion 전체 계산 ───────────────────────────────────────
BacktestAnalysisDto analysis = calcAnalysis(series, record, cfg, initCap, equity); BacktestAnalysisDto analysis = calcAnalysis(series, record, cfg, initCap, equity,
simGrossProfit[0], simGrossLoss[0]);
// ── Stats (하위 호환) ───────────────────────────────────────────────── // ── Stats (하위 호환) ─────────────────────────────────────────────────
Stats stats = toStats(analysis, signals); Stats stats = toStats(analysis, signals);
@@ -285,16 +291,18 @@ public class BacktestingService {
// ── Ta4j AnalysisCriterion 전체 계산 ───────────────────────────────────── // ── Ta4j AnalysisCriterion 전체 계산 ─────────────────────────────────────
private BacktestAnalysisDto calcAnalysis(BarSeries series, TradingRecord record, 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() BacktestAnalysisDto.BacktestAnalysisDtoBuilder b = BacktestAnalysisDto.builder()
.initialCapital(initCap) .initialCapital(initCap)
.finalEquity(finalEquity); .finalEquity(finalEquity);
double totalReturnPct = safeCalc(() -> calcTotalReturnPct(series, record)); // equity 시뮬레이션 기준 총 수익률 — Ta4j criterion/fallback 은 개별 거래 수익률 합산으로 부정확
double grossProfit = safeCalc(() -> calcCriterion("org.ta4j.core.criteria.pnl.GrossProfitCriterion", series, record)); double totalReturnPct = initCap > 0 ? (finalEquity - initCap) / initCap : 0.0;
double grossLoss = safeCalc(() -> calcCriterion("org.ta4j.core.criteria.pnl.GrossLossCriterion", series, record)); double grossProfit = simGrossProfit;
double avgReturnPct = safeCalc(() -> calcCriterion("org.ta4j.core.criteria.pnl.AverageProfitLossCriterion", series, record)); double grossLoss = simGrossLoss;
double profitLossRatio = safeCalc(() -> calcCriterion("org.ta4j.core.criteria.pnl.ProfitLossRatioCriterion", series, record)); double avgReturnPct = 0.0;
double profitLossRatio = 0.0;
int positions = safeCalcInt(() -> calcCriterionInt("org.ta4j.core.criteria.NumberOfPositionsCriterion", series, record)); int positions = safeCalcInt(() -> calcCriterionInt("org.ta4j.core.criteria.NumberOfPositionsCriterion", series, record));
int winning = safeCalcInt(() -> calcCriterionInt("org.ta4j.core.criteria.NumberOfWinningPositionsCriterion", 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 (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 return b
.totalReturnPct(totalReturnPct) .totalReturnPct(totalReturnPct)
.totalProfitLoss(totalPnl) .totalProfitLoss(totalPnl)
@@ -351,24 +372,18 @@ public class BacktestingService {
// ── 개별 Criterion 계산 헬퍼 ───────────────────────────────────────────── // ── 개별 Criterion 계산 헬퍼 ─────────────────────────────────────────────
private double calcTotalReturnPct(BarSeries series, TradingRecord record) throws Exception { /**
// 총 수익률 = (finalEquity - initialCapital) / initialCapital * equity 시뮬레이션에 체결 손익을 반영하고, 총 이익/총 손실 누적값을 갱신한다.
// TotalProfitLossPercentageCriterion 또는 ReturnCriterion 시도 * @return 갱신된 equity
try { */
Class<?> cls = Class.forName("org.ta4j.core.criteria.pnl.TotalProfitLossPercentageCriterion"); private double applyEquityPnl(double equity, double tradeSizePct, double size, double netReturn,
AnalysisCriterion criterion = (AnalysisCriterion) cls.getDeclaredConstructor().newInstance(); double[] grossProfitAcc, double[] grossLossAcc) {
return criterion.calculate(series, record).doubleValue(); double eqBefore = equity;
} catch (Exception ignored) {} double next = eqBefore + eqBefore * tradeSizePct * size * netReturn;
try { double delta = next - eqBefore;
Class<?> cls = Class.forName("org.ta4j.core.criteria.ReturnCriterion"); if (delta >= 0) grossProfitAcc[0] += delta;
AnalysisCriterion criterion = (AnalysisCriterion) cls.getDeclaredConstructor().newInstance(); else grossLossAcc[0] += delta;
return criterion.calculate(series, record).doubleValue() - 1.0; return next;
} catch (Exception ignored) {}
// fallback: 거래 수익 직접 계산
return record.getPositions().stream()
.filter(Position::isClosed)
.mapToDouble(p -> p.getProfit().doubleValue() / p.getEntry().getNetPrice().doubleValue())
.sum();
} }
private double calcMaxDrawdown(BarSeries series, TradingRecord record) throws Exception { private double calcMaxDrawdown(BarSeries series, TradingRecord record) throws Exception {