diff --git a/src/main/java/com/hackathon/tomolow/domain/transaction/repository/TransactionRepository.java b/src/main/java/com/hackathon/tomolow/domain/transaction/repository/TransactionRepository.java index d6e58fa..63960ec 100644 --- a/src/main/java/com/hackathon/tomolow/domain/transaction/repository/TransactionRepository.java +++ b/src/main/java/com/hackathon/tomolow/domain/transaction/repository/TransactionRepository.java @@ -16,4 +16,8 @@ List findAllByUserAndCreatedAtBetweenOrderByCreatedAtDesc( // 해당 유저의 "첫 거래" 하나만 (createdAt 오름차순) Optional findFirstByUserOrderByCreatedAtAsc(User user); + + // 기간 내 거래를 "오래된 순"으로 가져오도록 변경 + List findAllByUserAndCreatedAtBetweenOrderByCreatedAtAsc( + User user, LocalDateTime start, LocalDateTime end); } diff --git a/src/main/java/com/hackathon/tomolow/domain/transaction/service/TradeHistoryService.java b/src/main/java/com/hackathon/tomolow/domain/transaction/service/TradeHistoryService.java index 38330d2..7002bd6 100644 --- a/src/main/java/com/hackathon/tomolow/domain/transaction/service/TradeHistoryService.java +++ b/src/main/java/com/hackathon/tomolow/domain/transaction/service/TradeHistoryService.java @@ -5,6 +5,8 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -37,44 +39,97 @@ public TradeHistoryResponse getHistory(User user, LocalDate startDate, LocalDate // [2] DB에서 해당 기간 거래 내역 조회 (최신순) List txs = - transactionRepository.findAllByUserAndCreatedAtBetweenOrderByCreatedAtDesc( - user, start, end); + transactionRepository.findAllByUserAndCreatedAtBetweenOrderByCreatedAtAsc(user, start, end); // [3] 요약 값 계산 BigDecimal totalBuy = BigDecimal.ZERO; BigDecimal totalSell = BigDecimal.ZERO; + BigDecimal realizedPnl = BigDecimal.ZERO; // 실제 손익 + + // 종목별 포지션 상태 (이동평균법) + // key: marketId + Map positionQtyMap = new HashMap<>(); + Map positionCostMap = new HashMap<>(); List items = new ArrayList<>(); for (Transaction tx : txs) { - BigDecimal amount = tx.getPrice().multiply(BigDecimal.valueOf(tx.getQuantity())); // 가격 * 수량 - - if (tx.getTradeType() == TradeType.BUY) { - totalBuy = totalBuy.add(amount); - } else { - totalSell = totalSell.add(amount); - } + Long marketId = tx.getMarket().getId(); + BigDecimal price = tx.getPrice(); + BigDecimal qty = BigDecimal.valueOf(tx.getQuantity()); + BigDecimal amount = price.multiply(qty); // 가격 * 수량 + // 거래 내역 DTO는 기존과 동일하게 쌓아둔다 (UI용) items.add( TradeHistoryItemDto.builder() .tradedAt(tx.getCreatedAt()) .name(tx.getMarket().getName()) .symbol(tx.getMarket().getSymbol()) - .price(tx.getPrice()) + .price(price) .quantity(tx.getQuantity()) .tradeType(tx.getTradeType()) .amount(amount) .build()); + + BigDecimal currentQty = positionQtyMap.getOrDefault(marketId, BigDecimal.ZERO); + BigDecimal currentCost = positionCostMap.getOrDefault(marketId, BigDecimal.ZERO); + + if (tx.getTradeType() == TradeType.BUY) { + // 매수: 총 매수금액 + 포지션 반영 + totalBuy = totalBuy.add(amount); + + BigDecimal newQty = currentQty.add(qty); + BigDecimal newCost = currentCost.add(amount); + + positionQtyMap.put(marketId, newQty); + positionCostMap.put(marketId, newCost); + + } else if (tx.getTradeType() == TradeType.SELL) { + // 매도: 총 매도금액 + totalSell = totalSell.add(amount); + + // 보유 물량이 있을 때만 손익 계산 + if (currentQty.compareTo(BigDecimal.ZERO) > 0) { + // 평단가 = 현재까지의 총 원가 / 보유 수량 + BigDecimal avgCost = currentCost.divide(currentQty, 8, RoundingMode.HALF_UP); // 소수점 넉넉히 + + BigDecimal sellQty = qty; + + // 만약 매도 수량 > 보유 수량이면, 보유 수량까지만 손익 계산 + if (sellQty.compareTo(currentQty) > 0) { + sellQty = currentQty; + } + + BigDecimal costForSell = avgCost.multiply(sellQty); + BigDecimal sellAmountForPnl = price.multiply(sellQty); + + BigDecimal pnl = sellAmountForPnl.subtract(costForSell); + realizedPnl = realizedPnl.add(pnl); + + // 포지션 업데이트 + BigDecimal newQty = currentQty.subtract(sellQty); + BigDecimal newCost = currentCost.subtract(costForSell); + + if (newQty.compareTo(BigDecimal.ZERO) <= 0) { + newQty = BigDecimal.ZERO; + newCost = BigDecimal.ZERO; + } + + positionQtyMap.put(marketId, newQty); + positionCostMap.put(marketId, newCost); + } + // 보유 수량이 0인데 매도한 경우는, 원가를 알 수 없으니 손익 0으로 처리 (무시) + } } - BigDecimal periodPnl = totalSell.subtract(totalBuy); // 매도 - 매수 + BigDecimal periodPnl = realizedPnl; // 이제는 "실제 실현 손익" BigDecimal pnlRate = (totalBuy.signum() == 0) ? BigDecimal.ZERO : periodPnl.divide(totalBuy, 4, RoundingMode.HALF_UP); // 예: -0.0245 - // [4] 날짜별 그룹핑 (최신 날짜 순) + // [4] 날짜별 그룹핑 (최신 날짜 순, 각 날짜 내부는 거래 최신순) Map> byDate = items.stream() .collect( @@ -83,13 +138,22 @@ public TradeHistoryResponse getHistory(User user, LocalDate startDate, LocalDate List days = byDate.entrySet().stream() + // 날짜 기준 최신 날짜 순 .sorted(Map.Entry.>comparingByKey().reversed()) - .map(e -> DailyTradeHistoryDto.builder().date(e.getKey()).items(e.getValue()).build()) + .map( + e -> { + // 하루 안에서도 최신 거래가 위로 오도록 정렬 + List sortedItems = + e.getValue().stream() + .sorted(Comparator.comparing(TradeHistoryItemDto::getTradedAt).reversed()) + .toList(); + return DailyTradeHistoryDto.builder().date(e.getKey()).items(sortedItems).build(); + }) .toList(); return TradeHistoryResponse.builder() - .periodPnlAmount(periodPnl) - .periodPnlRate(pnlRate) + .periodPnlAmount(periodPnl) // 매도 기준 실현 손익 합계 + .periodPnlRate(pnlRate) // 실현 손익 / 기간 내 총 매수금액 .totalBuyAmount(totalBuy) .totalSellAmount(totalSell) .days(days) @@ -98,10 +162,8 @@ public TradeHistoryResponse getHistory(User user, LocalDate startDate, LocalDate @Transactional(readOnly = true) public TradeHistoryResponse getDefaultHistory(User user) { - // 1) 해당 유저의 첫 거래 찾기 var firstTxOpt = transactionRepository.findFirstByUserOrderByCreatedAtAsc(user); - // 2) 거래가 아예 없으면 빈 응답 반환 if (firstTxOpt.isEmpty()) { return TradeHistoryResponse.builder() .periodPnlAmount(BigDecimal.ZERO) @@ -115,7 +177,7 @@ public TradeHistoryResponse getDefaultHistory(User user) { LocalDate firstDate = firstTxOpt.get().getCreatedAt().toLocalDate(); LocalDate today = LocalDate.now(); - // 🌟 한 줄 핵심 로직: "첫 거래일 ~ 오늘" 범위로 기존 메서드 재사용 + // "첫 거래일 ~ 오늘" 범위로 기존 메서드 재사용 return getHistory(user, firstDate, today); } }