feat: 투자 소수점 매매 + 카드 할부(installment_months)
- invest_trade.quantity BIGINT→DECIMAL(18,6): 평단·원가·평가/실현손익 계산을 BigDecimal로 전환 (금액은 원단위 정수 유지) - account_entry.installment_months 추가: 카드 지출 2~24개월 할부 기록(일시불 null), 응답에 포함 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f5e9b78a14
commit
bd0a0f7776
@@ -15,6 +15,8 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -86,9 +88,9 @@ public class InvestService {
|
||||
String type = "SELL".equals(req.getTradeType()) ? "SELL" : "BUY";
|
||||
if ("SELL".equals(type)) {
|
||||
Calc c = calc(investMapper.findTradesByHolding(holdingId, memberId));
|
||||
if (req.getQuantity() > c.quantity()) {
|
||||
if (req.getQuantity().compareTo(c.quantity()) > 0) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST,
|
||||
"매도 수량이 보유수량(" + c.quantity() + ")을 초과합니다.");
|
||||
"매도 수량이 보유수량(" + c.quantity().stripTrailingZeros().toPlainString() + ")을 초과합니다.");
|
||||
}
|
||||
}
|
||||
InvestTrade t = InvestTrade.builder()
|
||||
@@ -124,7 +126,7 @@ public class InvestService {
|
||||
long evalPrice = h.getCurrentPrice() != null ? h.getCurrentPrice() : c.avgPrice();
|
||||
WalletInvest wi = result.computeIfAbsent(h.getWalletId(), k -> new WalletInvest());
|
||||
wi.cashDelta += cashDelta(trades);
|
||||
wi.stockEval += c.quantity() * evalPrice;
|
||||
wi.stockEval += amountOf(c.quantity(), evalPrice);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -145,25 +147,28 @@ public class InvestService {
|
||||
return map;
|
||||
}
|
||||
|
||||
/** 매매이력(시간순)으로 보유수량·원가·실현손익 산출 */
|
||||
/** 매매이력(시간순)으로 보유수량·원가·실현손익 산출. 수량은 소수점, 금액은 원 단위 정수. */
|
||||
private Calc calc(List<InvestTrade> trades) {
|
||||
long qty = 0, costBasis = 0, realized = 0;
|
||||
BigDecimal qty = BigDecimal.ZERO;
|
||||
long costBasis = 0, realized = 0;
|
||||
for (InvestTrade t : trades) {
|
||||
long amount = t.getQuantity() * t.getPrice();
|
||||
long amount = amountOf(t.getQuantity(), t.getPrice());
|
||||
long fee = t.getFee() != null ? t.getFee() : 0L;
|
||||
if ("BUY".equals(t.getTradeType())) {
|
||||
qty += t.getQuantity();
|
||||
qty = qty.add(t.getQuantity());
|
||||
costBasis += amount + fee;
|
||||
} else { // SELL
|
||||
long costRemoved = (qty > 0 && t.getQuantity() < qty)
|
||||
? Math.round(costBasis * (double) t.getQuantity() / qty)
|
||||
: costBasis; // 전량 매도 시 잔여 원가 전부 제거(반올림 잔차 방지)
|
||||
// 부분 매도 시 보유원가를 수량 비율로 제거, 전량 매도 시 잔여 원가 전부 제거(반올림 잔차 방지)
|
||||
boolean partial = qty.signum() > 0 && t.getQuantity().compareTo(qty) < 0;
|
||||
long costRemoved = partial
|
||||
? Math.round(costBasis * t.getQuantity().doubleValue() / qty.doubleValue())
|
||||
: costBasis;
|
||||
long proceeds = amount - fee;
|
||||
realized += proceeds - costRemoved;
|
||||
costBasis -= costRemoved;
|
||||
qty -= t.getQuantity();
|
||||
if (qty <= 0) {
|
||||
qty = 0;
|
||||
qty = qty.subtract(t.getQuantity());
|
||||
if (qty.signum() <= 0) {
|
||||
qty = BigDecimal.ZERO;
|
||||
costBasis = 0;
|
||||
}
|
||||
}
|
||||
@@ -171,17 +176,24 @@ public class InvestService {
|
||||
return new Calc(qty, costBasis, realized);
|
||||
}
|
||||
|
||||
private record Calc(long quantity, long costBasis, long realized) {
|
||||
private record Calc(BigDecimal quantity, long costBasis, long realized) {
|
||||
long avgPrice() {
|
||||
return quantity > 0 ? Math.round(costBasis / (double) quantity) : 0;
|
||||
return quantity.signum() > 0 ? Math.round(costBasis / quantity.doubleValue()) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** 거래금액(원) = 수량 × 단가, 원 단위 반올림 */
|
||||
private static long amountOf(BigDecimal quantity, long price) {
|
||||
return quantity.multiply(BigDecimal.valueOf(price))
|
||||
.setScale(0, RoundingMode.HALF_UP)
|
||||
.longValue();
|
||||
}
|
||||
|
||||
/** 매매로 인한 예수금 증감 = −Σ매수(금액+수수료) + Σ매도(금액−수수료) */
|
||||
private long cashDelta(List<InvestTrade> trades) {
|
||||
long cashDelta = 0;
|
||||
for (InvestTrade t : trades) {
|
||||
long amount = t.getQuantity() * t.getPrice();
|
||||
long amount = amountOf(t.getQuantity(), t.getPrice());
|
||||
long fee = t.getFee() != null ? t.getFee() : 0L;
|
||||
cashDelta += "BUY".equals(t.getTradeType()) ? -(amount + fee) : (amount - fee);
|
||||
}
|
||||
@@ -192,7 +204,7 @@ public class InvestService {
|
||||
Calc c = calc(trades);
|
||||
long avgPrice = c.avgPrice();
|
||||
long evalPrice = h.getCurrentPrice() != null ? h.getCurrentPrice() : avgPrice;
|
||||
long evalValue = c.quantity() * evalPrice;
|
||||
long evalValue = amountOf(c.quantity(), evalPrice);
|
||||
long evalGain = evalValue - c.costBasis();
|
||||
Double returnPct = c.costBasis() > 0 ? Math.round(evalGain * 1000.0 / c.costBasis()) / 10.0 : null;
|
||||
return HoldingResponse.builder()
|
||||
|
||||
Reference in New Issue
Block a user