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

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;
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 {