diff --git a/src/main/java/com/hackathon/tomolow/domain/userGroupTransaction/repository/UserGroupTransactionRepository.java b/src/main/java/com/hackathon/tomolow/domain/userGroupTransaction/repository/UserGroupTransactionRepository.java index 42ad79a..3ad2a18 100644 --- a/src/main/java/com/hackathon/tomolow/domain/userGroupTransaction/repository/UserGroupTransactionRepository.java +++ b/src/main/java/com/hackathon/tomolow/domain/userGroupTransaction/repository/UserGroupTransactionRepository.java @@ -19,6 +19,9 @@ List findAllByUserGroupAndCreatedAtBetweenOrderByCreatedAt // 해당 UserGroup의 첫 거래 하나 (createdAt 오름차순) Optional findFirstByUserGroupOrderByCreatedAtAsc(UserGroup userGroup); + List findAllByUserGroupAndCreatedAtBetweenOrderByCreatedAtAsc( + UserGroup userGroup, LocalDateTime start, LocalDateTime end); + List findAllByUserGroupId(Long userGroupId); List findAllByUserGroup_Id(Long userGroupId); diff --git a/src/main/java/com/hackathon/tomolow/domain/userGroupTransaction/service/GroupTradeHistoryService.java b/src/main/java/com/hackathon/tomolow/domain/userGroupTransaction/service/GroupTradeHistoryService.java index fcc2095..010598d 100644 --- a/src/main/java/com/hackathon/tomolow/domain/userGroupTransaction/service/GroupTradeHistoryService.java +++ b/src/main/java/com/hackathon/tomolow/domain/userGroupTransaction/service/GroupTradeHistoryService.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; @@ -30,7 +32,7 @@ public class GroupTradeHistoryService { private final UserGroupTransactionRepository userGroupTransactionRepository; private final GroupOrderInfoService groupOrderInfoService; - /** 🔹 특정 그룹에서의 내 거래내역 (기간 지정) */ + /** 특정 그룹에서의 내 거래내역 (기간 지정) - 실현손익 기준 */ @Transactional(readOnly = true) public TradeHistoryResponse getHistory( Long userId, Long groupId, LocalDate startDate, LocalDate endDate) { @@ -42,69 +44,130 @@ public TradeHistoryResponse getHistory( LocalDateTime start = startDate.atStartOfDay(); LocalDateTime end = endDate.plusDays(1).atStartOfDay(); - // 3) 해당 그룹 내 내 거래내역 조회 (최신순) + // 3) 해당 그룹 내 내 거래내역 조회 (오래된 순으로 정렬) List txs = - userGroupTransactionRepository.findAllByUserGroupAndCreatedAtBetweenOrderByCreatedAtDesc( + userGroupTransactionRepository.findAllByUserGroupAndCreatedAtBetweenOrderByCreatedAtAsc( userGroup, start, end); BigDecimal totalBuy = BigDecimal.ZERO; BigDecimal totalSell = BigDecimal.ZERO; + BigDecimal realizedPnl = BigDecimal.ZERO; // 매도 시점 기준 실현손익 + + // 종목별 포지션 상태 (이동평균법) + Map positionQtyMap = new HashMap<>(); + Map positionCostMap = new HashMap<>(); List items = new ArrayList<>(); for (UserGroupTransaction 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); + // UI에 뿌릴 개별 거래내역은 기존과 동일하게 쌓기 items.add( TradeHistoryItemDto.builder() - .tradedAt(tx.getCreatedAt()) // ✅ 개인과 동일: tradedAt + .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); - // 4) 날짜별 그룹핑 (최신 날짜 순) + // 4) 날짜별 그룹핑 (날짜는 최신 날짜 순, 하루 안에서는 최신 거래 먼저) Map> byDate = items.stream() .collect( Collectors.groupingBy( - i -> i.getTradedAt().toLocalDate(), // ✅ tradedAt 기준 - LinkedHashMap::new, - Collectors.toList())); + i -> i.getTradedAt().toLocalDate(), LinkedHashMap::new, Collectors.toList())); 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) .build(); } - /** 🔹 그룹 내 기본 범위(첫 거래일 ~ 오늘) */ + /** 그룹 내 기본 범위(첫 거래일 ~ 오늘) */ @Transactional(readOnly = true) public TradeHistoryResponse getDefaultHistory(Long userId, Long groupId) {