상세투자분석 계산로직 수정
This commit is contained in:
@@ -145,6 +145,8 @@ public class BacktestingService {
|
||||
double partialPct = cfg.getPartialExitPct() != null ? cfg.getPartialExitPct().doubleValue() / 100.0 : 0.5;
|
||||
boolean partialDone = false;
|
||||
|
||||
boolean useLedger = "LONG".equals(direction);
|
||||
PortfolioLedger ledger = useLedger ? new PortfolioLedger(initCap, cfg) : null;
|
||||
double equity = initCap;
|
||||
final double[] simGrossProfit = {0.0};
|
||||
final double[] simGrossLoss = {0.0};
|
||||
@@ -168,12 +170,15 @@ public class BacktestingService {
|
||||
.time(time).type(sigType).price(effEntry).barIndex(i).build());
|
||||
// 실제 포지션 추적은 LONG_ONLY 모드와 동일하게 유지 (수익 계산용)
|
||||
if (!inPosition) {
|
||||
double shares = (equity * tradeSizePct) / effEntry;
|
||||
record.enter(i, series.numFactory().numOf(effEntry), series.numFactory().numOf(shares));
|
||||
entryPrice = effEntry;
|
||||
entryBarIdx = i;
|
||||
inPosition = true;
|
||||
partialDone = false;
|
||||
double shares = enterPosition(ledger, useLedger, equity, tradeSizePct,
|
||||
effEntry, closePrice, record, i, series);
|
||||
if (shares > 0) {
|
||||
entryPrice = effEntry;
|
||||
entryBarIdx = i;
|
||||
inPosition = true;
|
||||
partialDone = false;
|
||||
if (useLedger) equity = ledger.portfolioValue(closePrice);
|
||||
}
|
||||
}
|
||||
} else if (exitOk) {
|
||||
double effExit = applySlippage(exitPrice, cfg, false);
|
||||
@@ -182,13 +187,9 @@ public class BacktestingService {
|
||||
.time(time).type(sigType).price(effExit).barIndex(i).build());
|
||||
// 수익 계산: 실제 포지션이 있을 때만
|
||||
if (inPosition) {
|
||||
double commission = calcCommissionRate(cfg) * 2;
|
||||
double rawReturn = "SHORT".equals(direction)
|
||||
? (entryPrice - effExit) / entryPrice
|
||||
: (effExit - entryPrice) / entryPrice;
|
||||
double size = partialDone ? (1.0 - partialPct) : 1.0;
|
||||
equity = applyEquityPnl(equity, tradeSizePct, size, rawReturn - commission,
|
||||
simGrossProfit, simGrossLoss);
|
||||
equity = applyExit(ledger, useLedger, equity, tradeSizePct, entryPrice, effExit,
|
||||
size, direction, cfg, simGrossProfit, simGrossLoss);
|
||||
Num numExitPrice = series.numFactory().numOf(effExit);
|
||||
Num numShares = record.getCurrentPosition().getEntry().getAmount();
|
||||
record.exit(i, numExitPrice, numShares);
|
||||
@@ -210,33 +211,25 @@ public class BacktestingService {
|
||||
|
||||
if (doEnter) {
|
||||
double effEntry = applySlippage(closePrice, cfg, true);
|
||||
double shares = (equity * tradeSizePct) / effEntry;
|
||||
|
||||
Num numPrice = series.numFactory().numOf(effEntry);
|
||||
Num numShares = series.numFactory().numOf(shares);
|
||||
record.enter(i, numPrice, numShares);
|
||||
|
||||
String sigType = "SHORT".equals(direction) ? "SHORT_ENTRY" : "BUY";
|
||||
signals.add(Signal.builder()
|
||||
.time(time).type(sigType).price(effEntry).barIndex(i).build());
|
||||
|
||||
entryPrice = effEntry;
|
||||
entryBarIdx = i;
|
||||
inPosition = true;
|
||||
partialDone = false;
|
||||
double shares = enterPosition(ledger, useLedger, equity, tradeSizePct,
|
||||
effEntry, closePrice, record, i, series);
|
||||
if (shares > 0) {
|
||||
String sigType = "SHORT".equals(direction) ? "SHORT_ENTRY" : "BUY";
|
||||
signals.add(Signal.builder()
|
||||
.time(time).type(sigType).price(effEntry).barIndex(i).build());
|
||||
entryPrice = effEntry;
|
||||
entryBarIdx = i;
|
||||
inPosition = true;
|
||||
partialDone = false;
|
||||
if (useLedger) equity = ledger.portfolioValue(closePrice);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 분할 청산: exit 조건 처음 충족 시 일부만 청산
|
||||
if (partialExit && !partialDone && exitRule.isSatisfied(i, record)) {
|
||||
double effExit = applySlippage(exitPrice, cfg, false);
|
||||
double partShares = record.getCurrentPosition().getEntry().getAmount().doubleValue() * partialPct;
|
||||
double partReturn = "SHORT".equals(direction)
|
||||
? (entryPrice - effExit) / entryPrice
|
||||
: (effExit - entryPrice) / entryPrice;
|
||||
double commission = calcCommissionRate(cfg) * 2;
|
||||
equity = applyEquityPnl(equity, tradeSizePct, partialPct, partReturn - commission,
|
||||
simGrossProfit, simGrossLoss);
|
||||
|
||||
double effExit = applySlippage(exitPrice, cfg, false);
|
||||
equity = applyExit(ledger, useLedger, equity, tradeSizePct, entryPrice, effExit,
|
||||
partialPct, direction, cfg, simGrossProfit, simGrossLoss);
|
||||
signals.add(Signal.builder()
|
||||
.time(time).type("PARTIAL_SELL").price(effExit).barIndex(i).build());
|
||||
partialDone = true;
|
||||
@@ -244,16 +237,10 @@ public class BacktestingService {
|
||||
}
|
||||
|
||||
if (exitRule.isSatisfied(i, record)) {
|
||||
double effExit = applySlippage(exitPrice, cfg, false);
|
||||
double commission = calcCommissionRate(cfg) * 2;
|
||||
double rawReturn = "SHORT".equals(direction)
|
||||
? (entryPrice - effExit) / entryPrice
|
||||
: (effExit - entryPrice) / entryPrice;
|
||||
double netReturn = rawReturn - commission;
|
||||
|
||||
double effExit = applySlippage(exitPrice, cfg, false);
|
||||
double size = partialDone ? (1.0 - partialPct) : 1.0;
|
||||
equity = applyEquityPnl(equity, tradeSizePct, size, netReturn,
|
||||
simGrossProfit, simGrossLoss);
|
||||
equity = applyExit(ledger, useLedger, equity, tradeSizePct, entryPrice, effExit,
|
||||
size, direction, cfg, simGrossProfit, simGrossLoss);
|
||||
|
||||
Num numExitPrice = series.numFactory().numOf(effExit);
|
||||
Num numShares = record.getCurrentPosition().getEntry().getAmount();
|
||||
@@ -270,9 +257,15 @@ public class BacktestingService {
|
||||
}
|
||||
}
|
||||
|
||||
double lastMarkPrice = barCount > 0
|
||||
? series.getBar(barCount - 1).getClosePrice().doubleValue() : 0.0;
|
||||
double finalEquity = resolveFinalEquity(ledger, useLedger, cfg, initCap, equity, inPosition,
|
||||
entryPrice, lastMarkPrice, tradeSizePct, partialDone, partialPct, direction,
|
||||
simGrossProfit, simGrossLoss);
|
||||
|
||||
// ── AnalysisCriterion 전체 계산 ───────────────────────────────────────
|
||||
BacktestAnalysisDto analysis = calcAnalysis(series, record, cfg, initCap, equity,
|
||||
simGrossProfit[0], simGrossLoss[0]);
|
||||
BacktestAnalysisDto analysis = calcAnalysis(series, record, cfg, initCap, finalEquity,
|
||||
ledger, lastMarkPrice, simGrossProfit[0], simGrossLoss[0]);
|
||||
|
||||
// ── Stats (하위 호환) ─────────────────────────────────────────────────
|
||||
Stats stats = toStats(analysis, signals);
|
||||
@@ -292,15 +285,28 @@ public class BacktestingService {
|
||||
|
||||
private BacktestAnalysisDto calcAnalysis(BarSeries series, TradingRecord record,
|
||||
BacktestSettingsDto cfg, double initCap, double finalEquity,
|
||||
PortfolioLedger ledger, double lastMarkPrice,
|
||||
double simGrossProfit, double simGrossLoss) {
|
||||
String analysisMethod = ledger != null
|
||||
? ledger.analysisMethod
|
||||
: PortfolioLedger.normalizeMethod(cfg.getAnalysisMethod());
|
||||
double cashBalance = ledger != null ? ledger.cash : 0.0;
|
||||
double holdingsValue = ledger != null ? ledger.shares * lastMarkPrice : 0.0;
|
||||
double realizedPnl = ledger != null ? ledger.realizedPnl : (simGrossProfit + simGrossLoss);
|
||||
double unrealizedPnl = ledger != null ? ledger.unrealizedPnl(lastMarkPrice) : 0.0;
|
||||
double grossProfit = ledger != null ? ledger.grossProfit : simGrossProfit;
|
||||
double grossLoss = ledger != null ? ledger.grossLoss : simGrossLoss;
|
||||
|
||||
BacktestAnalysisDto.BacktestAnalysisDtoBuilder b = BacktestAnalysisDto.builder()
|
||||
.initialCapital(initCap)
|
||||
.finalEquity(finalEquity);
|
||||
.finalEquity(finalEquity)
|
||||
.analysisMethod(analysisMethod)
|
||||
.cashBalance(cashBalance)
|
||||
.holdingsValue(holdingsValue)
|
||||
.realizedPnl(realizedPnl)
|
||||
.unrealizedPnl(unrealizedPnl);
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -372,10 +378,66 @@ public class BacktestingService {
|
||||
|
||||
// ── 개별 Criterion 계산 헬퍼 ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* equity 시뮬레이션에 체결 손익을 반영하고, 총 이익/총 손실 누적값을 갱신한다.
|
||||
* @return 갱신된 equity
|
||||
*/
|
||||
private double enterPosition(PortfolioLedger ledger, boolean useLedger,
|
||||
double equity, double tradeSizePct, double effEntry, double markPrice,
|
||||
BaseTradingRecord record, int barIndex, BarSeries series) {
|
||||
if (useLedger) {
|
||||
double before = ledger.shares;
|
||||
ledger.executeBuy(effEntry, markPrice);
|
||||
double bought = ledger.shares - before;
|
||||
if (bought <= 1e-12) return 0.0;
|
||||
record.enter(barIndex, series.numFactory().numOf(effEntry),
|
||||
series.numFactory().numOf(bought));
|
||||
return bought;
|
||||
}
|
||||
double shares = (equity * tradeSizePct) / effEntry;
|
||||
if (shares <= 1e-12) return 0.0;
|
||||
record.enter(barIndex, series.numFactory().numOf(effEntry),
|
||||
series.numFactory().numOf(shares));
|
||||
return shares;
|
||||
}
|
||||
|
||||
private double applyExit(PortfolioLedger ledger, boolean useLedger, double equity, double tradeSizePct,
|
||||
double entryPrice, double effExit, double sellFraction, String direction,
|
||||
BacktestSettingsDto cfg, double[] grossProfitAcc, double[] grossLossAcc) {
|
||||
if (useLedger) {
|
||||
ledger.executeSell(effExit, sellFraction);
|
||||
return ledger.portfolioValue(effExit);
|
||||
}
|
||||
double commission = calcCommissionRate(cfg) * 2;
|
||||
double rawReturn = "SHORT".equals(direction)
|
||||
? (entryPrice - effExit) / entryPrice
|
||||
: (effExit - entryPrice) / entryPrice;
|
||||
return applyEquityPnl(equity, tradeSizePct, sellFraction, rawReturn - commission,
|
||||
grossProfitAcc, grossLossAcc);
|
||||
}
|
||||
|
||||
private double resolveFinalEquity(PortfolioLedger ledger, boolean useLedger, BacktestSettingsDto cfg,
|
||||
double initCap, double equity, boolean inPosition, double entryPrice,
|
||||
double lastMarkPrice, double tradeSizePct, boolean partialDone,
|
||||
double partialPct, String direction,
|
||||
double[] grossProfitAcc, double[] grossLossAcc) {
|
||||
if (useLedger && ledger != null) {
|
||||
return ledger.resolveFinalEquity(lastMarkPrice);
|
||||
}
|
||||
String method = PortfolioLedger.normalizeMethod(cfg.getAnalysisMethod());
|
||||
if (inPosition && lastMarkPrice > 0 && entryPrice > 0
|
||||
&& PortfolioLedger.MARK_TO_MARKET.equals(method)) {
|
||||
double commission = calcCommissionRate(cfg) * 2;
|
||||
double rawReturn = "SHORT".equals(direction)
|
||||
? (entryPrice - lastMarkPrice) / entryPrice
|
||||
: (lastMarkPrice - entryPrice) / entryPrice;
|
||||
double size = partialDone ? (1.0 - partialPct) : 1.0;
|
||||
return applyEquityPnl(equity, tradeSizePct, size, rawReturn - commission,
|
||||
grossProfitAcc, grossLossAcc);
|
||||
}
|
||||
if (PortfolioLedger.REALIZED_ONLY.equals(method)) {
|
||||
return initCap + grossProfitAcc[0] + grossLossAcc[0];
|
||||
}
|
||||
return equity;
|
||||
}
|
||||
|
||||
/** SHORT/레거시: 비율 기반 equity 갱신 */
|
||||
private double applyEquityPnl(double equity, double tradeSizePct, double size, double netReturn,
|
||||
double[] grossProfitAcc, double[] grossLossAcc) {
|
||||
double eqBefore = equity;
|
||||
|
||||
Reference in New Issue
Block a user