상세투자분석 계산로직 수정

This commit is contained in:
Macbook
2026-06-07 15:12:42 +09:00
parent c4365283cf
commit e1cf2ea46d
14 changed files with 366 additions and 58 deletions
@@ -0,0 +1,125 @@
package com.goldenchart.service;
import com.goldenchart.dto.BacktestSettingsDto;
/**
* 표준 주식 프로그램 방식의 포트폴리오 회계.
* <ul>
* <li>MARK_TO_MARKET: 예수금 + 보유주식 시가평가 (평가손익 포함)</li>
* <li>REALIZED_ONLY: 청산 완료 실현손익만 반영 (기말 미청산 평가 제외)</li>
* </ul>
*/
final class PortfolioLedger {
static final String MARK_TO_MARKET = "MARK_TO_MARKET";
static final String REALIZED_ONLY = "REALIZED_ONLY";
final double initialCapital;
final String analysisMethod;
final BacktestSettingsDto cfg;
double cash;
double shares;
/** 현재 보유분 취득원가(수수료 포함) */
double costBasis;
double realizedPnl;
double grossProfit;
double grossLoss;
PortfolioLedger(double initialCapital, BacktestSettingsDto cfg) {
this.initialCapital = initialCapital;
this.cfg = cfg;
this.analysisMethod = normalizeMethod(cfg.getAnalysisMethod());
this.cash = initialCapital;
}
static String normalizeMethod(String raw) {
return REALIZED_ONLY.equalsIgnoreCase(raw) ? REALIZED_ONLY : MARK_TO_MARKET;
}
boolean hasPosition() {
return shares > 1e-12;
}
/** 평가금액 = 예수금 + 보유주식 평가액 */
double portfolioValue(double markPrice) {
return cash + shares * markPrice;
}
double unrealizedPnl(double markPrice) {
if (shares <= 1e-12) return 0.0;
return shares * markPrice - costBasis;
}
double resolveFinalEquity(double lastMarkPrice) {
if (REALIZED_ONLY.equals(analysisMethod)) {
return initialCapital + realizedPnl;
}
return cash + shares * lastMarkPrice;
}
/** LONG 매수 체결 */
void executeBuy(double effEntry, double markPriceForSizing) {
if (effEntry <= 0 || cash <= 0) return;
double commRate = commissionRate();
double orderAmount = computeOrderAmount(markPriceForSizing);
if (orderAmount <= 0) return;
double maxSpend = cash;
orderAmount = Math.min(orderAmount, maxSpend / (1 + commRate));
if (orderAmount <= 0) return;
double sharesToBuy = orderAmount / effEntry;
double totalCost = sharesToBuy * effEntry * (1 + commRate);
if (totalCost > cash + 1e-6) {
sharesToBuy = cash / (effEntry * (1 + commRate));
totalCost = sharesToBuy * effEntry * (1 + commRate);
}
if (sharesToBuy <= 1e-12) return;
cash -= totalCost;
shares += sharesToBuy;
costBasis += totalCost;
}
/** LONG 매도 체결 — sellFraction: 0~1 (전량=1) */
void executeSell(double effExit, double sellFraction) {
if (effExit <= 0 || shares <= 1e-12) return;
double fraction = Math.max(0.0, Math.min(1.0, sellFraction));
double sharesToSell = shares * fraction;
if (sharesToSell <= 1e-12) return;
double commRate = commissionRate();
double proceeds = sharesToSell * effExit * (1 - commRate);
double costPortion = costBasis * (sharesToSell / shares);
double pnl = proceeds - costPortion;
cash += proceeds;
realizedPnl += pnl;
if (pnl >= 0) grossProfit += pnl;
else grossLoss += pnl;
shares -= sharesToSell;
costBasis -= costPortion;
if (shares <= 1e-12) {
shares = 0;
costBasis = 0;
}
}
private double computeOrderAmount(double markPrice) {
double tradeSizePct = cfg.getTradeSizeValue() != null
? cfg.getTradeSizeValue().doubleValue() / 100.0 : 1.0;
if ("FIXED_AMOUNT".equals(cfg.getTradeSizeType())) {
double fixed = cfg.getTradeSizeValue() != null ? cfg.getTradeSizeValue().doubleValue() : 0;
return Math.min(fixed, cash);
}
double equity = portfolioValue(markPrice);
return equity * tradeSizePct;
}
private double commissionRate() {
if ("ZERO".equals(cfg.getCommissionType())) return 0.0;
return cfg.getCommissionRate() != null ? cfg.getCommissionRate().doubleValue() : 0.0015;
}
}