Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ List<UserGroupTransaction> findAllByUserGroupAndCreatedAtBetweenOrderByCreatedAt
// 해당 UserGroup의 첫 거래 하나 (createdAt 오름차순)
Optional<UserGroupTransaction> findFirstByUserGroupOrderByCreatedAtAsc(UserGroup userGroup);

List<UserGroupTransaction> findAllByUserGroupAndCreatedAtBetweenOrderByCreatedAtAsc(
UserGroup userGroup, LocalDateTime start, LocalDateTime end);

List<UserGroupTransaction> findAllByUserGroupId(Long userGroupId);

List<UserGroupTransaction> findAllByUserGroup_Id(Long userGroupId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -42,69 +44,130 @@ public TradeHistoryResponse getHistory(
LocalDateTime start = startDate.atStartOfDay();
LocalDateTime end = endDate.plusDays(1).atStartOfDay();

// 3) 해당 그룹 내 내 거래내역 조회 (최신순)
// 3) 해당 그룹 내 내 거래내역 조회 (오래된 순으로 정렬)
List<UserGroupTransaction> txs =
userGroupTransactionRepository.findAllByUserGroupAndCreatedAtBetweenOrderByCreatedAtDesc(
userGroupTransactionRepository.findAllByUserGroupAndCreatedAtBetweenOrderByCreatedAtAsc(
userGroup, start, end);

BigDecimal totalBuy = BigDecimal.ZERO;
BigDecimal totalSell = BigDecimal.ZERO;
BigDecimal realizedPnl = BigDecimal.ZERO; // 매도 시점 기준 실현손익

// 종목별 포지션 상태 (이동평균법)
Map<Long, BigDecimal> positionQtyMap = new HashMap<>();
Map<Long, BigDecimal> positionCostMap = new HashMap<>();

List<TradeHistoryItemDto> 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<LocalDate, List<TradeHistoryItemDto>> byDate =
items.stream()
.collect(
Collectors.groupingBy(
i -> i.getTradedAt().toLocalDate(), // ✅ tradedAt 기준
LinkedHashMap::new,
Collectors.toList()));
i -> i.getTradedAt().toLocalDate(), LinkedHashMap::new, Collectors.toList()));

List<DailyTradeHistoryDto> days =
byDate.entrySet().stream()
// 날짜 최신순
.sorted(Map.Entry.<LocalDate, List<TradeHistoryItemDto>>comparingByKey().reversed())
.map(e -> DailyTradeHistoryDto.builder().date(e.getKey()).items(e.getValue()).build())
.map(
e -> {
// 하루 안에서는 최신 거래가 위로 오도록 정렬
List<TradeHistoryItemDto> 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) {

Expand Down