분석레포트 승률 로직 수정
This commit is contained in:
@@ -277,9 +277,19 @@ public class BacktestingService {
|
||||
entryPrice, lastMarkPrice, tradeSizePct, partialDone, partialPct, direction,
|
||||
simGrossProfit, simGrossLoss);
|
||||
|
||||
// 체결(ledger) 기준 거래 통계 — Ta4j TradingRecord와 분리
|
||||
PortfolioLedger.TradeStats executedStats = ledger != null ? ledger.tradeStats() : null;
|
||||
if (executedStats != null && executedStats.closedCount > 0) {
|
||||
int ta4jWins = countTa4jWinningPositions(record);
|
||||
if (ta4jWins != executedStats.winning) {
|
||||
log.debug("[Backtest] 승률: ledger 순손익 {}승 (Ta4j 가격기준 {}승) — ledger 기준 적용",
|
||||
executedStats.winning, ta4jWins);
|
||||
}
|
||||
}
|
||||
|
||||
// ── AnalysisCriterion 전체 계산 ───────────────────────────────────────
|
||||
BacktestAnalysisDto analysis = calcAnalysis(series, record, cfg, initCap, finalEquity,
|
||||
ledger, lastMarkPrice, simGrossProfit[0], simGrossLoss[0]);
|
||||
ledger, executedStats, lastMarkPrice, simGrossProfit[0], simGrossLoss[0]);
|
||||
|
||||
// ── Stats (하위 호환) ─────────────────────────────────────────────────
|
||||
Stats stats = toStats(analysis, signals);
|
||||
@@ -299,7 +309,8 @@ public class BacktestingService {
|
||||
|
||||
private BacktestAnalysisDto calcAnalysis(BarSeries series, TradingRecord record,
|
||||
BacktestSettingsDto cfg, double initCap, double finalEquity,
|
||||
PortfolioLedger ledger, double lastMarkPrice,
|
||||
PortfolioLedger ledger, PortfolioLedger.TradeStats executedStats,
|
||||
double lastMarkPrice,
|
||||
double simGrossProfit, double simGrossLoss) {
|
||||
String analysisMethod = ledger != null
|
||||
? ledger.analysisMethod
|
||||
@@ -335,26 +346,38 @@ public class BacktestingService {
|
||||
double sortino;
|
||||
double calmar;
|
||||
|
||||
if (ledger != null) {
|
||||
PortfolioLedger.TradeStats ts = ledger.tradeStats();
|
||||
positions = ts.closedCount;
|
||||
winning = ts.winning;
|
||||
losing = ts.losing;
|
||||
breakEven = ts.breakEven;
|
||||
winRate = ts.winRate;
|
||||
avgReturnPct = ts.avgReturnPct;
|
||||
maxDrawdown = ledger.maxDrawdownPct();
|
||||
if (executedStats != null && executedStats.closedCount > 0) {
|
||||
// LONG PortfolioLedger: 수수료·슬리피지 반영 실현손익 기준 (실매매와 동일)
|
||||
positions = executedStats.closedCount;
|
||||
winning = executedStats.winning;
|
||||
losing = executedStats.losing;
|
||||
breakEven = executedStats.breakEven;
|
||||
winRate = executedStats.winRate;
|
||||
avgReturnPct = executedStats.avgReturnPct;
|
||||
maxDrawdown = normalizeDrawdownPct(ledger.maxDrawdownPct());
|
||||
sharpe = ledger.sharpeFromEquityCurve();
|
||||
sortino = safeCalc(() -> calcCriterion(
|
||||
"org.ta4j.core.criteria.SortinoRatioCriterion", series, record));
|
||||
calmar = (maxDrawdown != 0) ? totalReturnPct / Math.abs(maxDrawdown) : 0.0;
|
||||
} else if (ledger != null) {
|
||||
// ledger 있으나 청산 0건 — Ta4j 승률 사용하지 않음
|
||||
positions = 0;
|
||||
winning = 0;
|
||||
losing = 0;
|
||||
breakEven = 0;
|
||||
winRate = 0.0;
|
||||
avgReturnPct = 0.0;
|
||||
maxDrawdown = normalizeDrawdownPct(ledger.maxDrawdownPct());
|
||||
sharpe = ledger.sharpeFromEquityCurve();
|
||||
sortino = 0.0;
|
||||
calmar = 0.0;
|
||||
} else {
|
||||
positions = safeCalcInt(() -> calcCriterionInt("org.ta4j.core.criteria.NumberOfPositionsCriterion", series, record));
|
||||
winning = safeCalcInt(() -> calcCriterionInt("org.ta4j.core.criteria.NumberOfWinningPositionsCriterion", series, record));
|
||||
losing = safeCalcInt(() -> calcCriterionInt("org.ta4j.core.criteria.NumberOfLosingPositionsCriterion", series, record));
|
||||
breakEven = safeCalcInt(() -> calcCriterionInt("org.ta4j.core.criteria.NumberOfBreakEvenPositionsCriterion", series, record));
|
||||
winRate = safeCalc(() -> calcCriterion("org.ta4j.core.criteria.WinningPositionsRatioCriterion", series, record));
|
||||
maxDrawdown = safeCalc(() -> calcMaxDrawdown(series, record));
|
||||
maxDrawdown = normalizeDrawdownPct(safeCalc(() -> calcMaxDrawdown(series, record)));
|
||||
sharpe = safeCalc(() -> calcSharpeRatio(series, record));
|
||||
sortino = safeCalc(() -> calcCriterion("org.ta4j.core.criteria.SortinoRatioCriterion", series, record));
|
||||
calmar = (maxDrawdown != 0) ? totalReturnPct / Math.abs(maxDrawdown) : 0.0;
|
||||
@@ -415,6 +438,20 @@ public class BacktestingService {
|
||||
.build();
|
||||
}
|
||||
|
||||
/** MDD는 음수(낙폭)로 통일 */
|
||||
private static double normalizeDrawdownPct(double dd) {
|
||||
if (dd == 0.0) return 0.0;
|
||||
return dd > 0 ? -dd : dd;
|
||||
}
|
||||
|
||||
/** Ta4j 가격 기준 승수 (디버그·비교용, 레포트에는 미사용) */
|
||||
private static int countTa4jWinningPositions(TradingRecord record) {
|
||||
if (record == null) return 0;
|
||||
return (int) record.getPositions().stream()
|
||||
.filter(p -> p.isClosed() && p.getProfit().isPositive())
|
||||
.count();
|
||||
}
|
||||
|
||||
private static Signal buildFillSignal(long time, String type, double price, int barIndex, double quantity) {
|
||||
return Signal.builder()
|
||||
.time(time)
|
||||
|
||||
Reference in New Issue
Block a user