분석레포트 수익률, 승률 계산오류 수정
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user