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 @@ -16,4 +16,8 @@ List<Transaction> findAllByUserAndCreatedAtBetweenOrderByCreatedAtDesc(

// 해당 유저의 "첫 거래" 하나만 (createdAt 오름차순)
Optional<Transaction> findFirstByUserOrderByCreatedAtAsc(User user);

// 기간 내 거래를 "오래된 순"으로 가져오도록 변경
List<Transaction> findAllByUserAndCreatedAtBetweenOrderByCreatedAtAsc(
User user, LocalDateTime start, LocalDateTime end);
}
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 Down Expand Up @@ -37,44 +39,97 @@ public TradeHistoryResponse getHistory(User user, LocalDate startDate, LocalDate

// [2] DB에서 해당 기간 거래 내역 조회 (최신순)
List<Transaction> 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<Long, BigDecimal> positionQtyMap = new HashMap<>();
Map<Long, BigDecimal> positionCostMap = new HashMap<>();

List<TradeHistoryItemDto> 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<LocalDate, List<TradeHistoryItemDto>> byDate =
items.stream()
.collect(
Expand All @@ -83,13 +138,22 @@ public TradeHistoryResponse getHistory(User user, LocalDate startDate, LocalDate

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)
Expand All @@ -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)
Expand All @@ -115,7 +177,7 @@ public TradeHistoryResponse getDefaultHistory(User user) {
LocalDate firstDate = firstTxOpt.get().getCreatedAt().toLocalDate();
LocalDate today = LocalDate.now();

// 🌟 한 줄 핵심 로직: "첫 거래일 ~ 오늘" 범위로 기존 메서드 재사용
// "첫 거래일 ~ 오늘" 범위로 기존 메서드 재사용
return getHistory(user, firstDate, today);
}
}