package com.goldenchart.service; import com.goldenchart.dto.BacktestSettingsDto; /** * 표준 주식 프로그램 방식의 포트폴리오 회계. * */ 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; } }