[MSA 프로젝트] 부분체결이 문제로다

greenlemonT·2025년 2월 27일

프로젝트

목록 보기
5/15
post-thumbnail

Kafka 기반 트레이딩 시스템 - 체결 및 부분 체결 처리

트레이딩 시스템에서 주문 매칭(Matching) 은 체결(Execution) 단계 이전에 수행되는 핵심 프로세스다.
매수와 매도 주문이 체결되려면 가격과 수량이 맞아야 하고,
완전히 체결되지 않으면 부분 체결(pending) 또는 미체결(Unmatched) 상태로 유지되어야 한다.


1. 매칭 서비스의 핵심 역할

매칭 서비스는 주문을 접수하고, 체결 가능한 주문을 찾아 처리하는 역할을 한다.

  • 핵심 기능
    주문을 우선순위 큐에 저장하여 시간순 정렬
    Redis에서 최신 20개의 체결 데이터를 가져와 최신 체결가 반영
    주문과 체결 데이터를 비교하여 완전 체결, 부분 체결, 미체결 상태 관리
    체결된 주문을 Kafka를 통해 Execution 모듈로 전송
    10초 내에 체결되지 않으면 Order 모듈로 전송하여 미체결 처리

2. 주문 접수 및 매칭 시작

public void processOrder(OrderCreateReqEvent order) {
    log.info("주문 접수: {}", order);

    orders.offer(order); // 주문을 큐에 추가
    processMatching(); // 매칭 프로세스 실행
}

1) 사용자의 매수 또는 매도 주문을 orders 큐에 저장
2) 저장된 주문을 처리하기 위해 processMatching() 실행

3. 주문 매칭 로직 (processMatching)

@Transactional
protected void processMatching() {
    log.info("매칭 시작");
    long startTime = System.currentTimeMillis();

    while (!orders.isEmpty()) {
        if (System.currentTimeMillis() - startTime > MAX_WAIT_TIME) {
            log.warn("5분 초과 - 미체결 주문을 Order 모듈로 전송");
            moveUnmatchedOrdersToQueue();
            sendUnmatchedOrdersToOrderService();
            return;
        }

        OrderCreateReqEvent order = orders.poll(); // 주문을 하나 가져옴
        List<Map<String, Object>> recentTrades = getRecentTradesFromRedis(order.getStockTicker());

        if (!recentTrades.isEmpty()) {
            // 가격 기준 정렬
            List<Map<String, Object>> sortedTrades = recentTrades.stream()
                    .sorted(Comparator.comparing(trade -> (long) trade.get("current_price")))
                    .toList();

            long maxTradePrice = (long) sortedTrades.get(sortedTrades.size() - 1).get("current_price");
            long minTradePrice = (long) sortedTrades.get(0).get("current_price");

            if (order.getOfferType() == OrderType.BUY) {
                handleBuyOrder(order, sortedTrades, minTradePrice);
            } else {
                handleSellOrder(order, sortedTrades, maxTradePrice);
            }
        }

        if (System.currentTimeMillis() - startTime > MAX_WAIT_TIME) {
            log.warn("5분 초과 - 미체결 주문을 Order 모듈로 전송");
            moveUnmatchedOrdersToQueue();
            sendUnmatchedOrdersToOrderService();
        }
    }
}

매칭 프로세스를 실행하면 주문을 하나씩 꺼내서 처리
Redis에서 최신 체결 데이터를 가져와 최근 가격을 확인
매수 주문이면 최저가(minTradePrice), 매도 주문이면 최고가(maxTradePrice)를 기준으로 매칭
10초 내에 매칭되지 않으면 미체결 주문으로 Order 모듈에 전달

4. 매수 주문 처리 (handleBuyOrder)

private void handleBuyOrder(OrderCreateReqEvent order, List<Map<String, Object>> sortedTrades, long minTradePrice) {
   long currentPrice = minTradePrice;

   if (order.getOfferPrice() > minTradePrice) {
       handleTradeExecution(order, order.getOfferQuantity(), 0L, currentPrice, true);
       long refundAmount = (order.getOfferPrice() - currentPrice) * order.getOfferQuantity();
       updateAvailableBalance(order.getUserId(), refundAmount);
   } else {
       int matchedIndex = linearSearch(sortedTrades, order.getOfferPrice());

       if (matchedIndex != -1) {
           Map<String, Object> matchedTrade = sortedTrades.get(matchedIndex);
           long tradePrice = Math.round(((Number) matchedTrade.get("current_price")).doubleValue());
           long tradeVolume = ((Number) matchedTrade.get("volume")).longValue();

           long orderQuantity = order.getOfferQuantity();
           long matchedQuantity = Math.min(orderQuantity, tradeVolume);
           long unfilledQuantity = orderQuantity - matchedQuantity;

           handleTradeExecution(order, matchedQuantity, unfilledQuantity, tradePrice, true);
           order.setOfferQuantity(unfilledQuantity);

           if (unfilledQuantity > 0) {
               orders.offer(order);
           }
       } else {
           handleTradeExecution(order, 0, order.getOfferQuantity(), order.getOfferPrice(), true);
       }
   }
}

매수 주문이 들어오면 최저 체결가(minTradePrice)와 비교
주문 가격이 체결 가능 가격보다 높으면 바로 체결 후 남은 금액을 환불
체결이 가능한 주문이 있으면 매칭 후 일부 체결(Partial Fill) 가능
체결되지 않은 주문은 다시 큐에 넣어 미체결 상태 유지

5. 체결 완료 및 부분 체결 처리 (handleTradeExecution)

@Transactional
protected void handleTradeExecution(OrderCreateReqEvent order, long matchedQuantity, long unfilledQuantity, long matchedPrice, boolean isBuy) {
    if (isBuy) {
        BuyTradeMatchEvent event = new BuyTradeMatchEvent(
                generateRandomId(),
                order.getOfferNumber(),
                order.getUserId(),
                order.getStockTicker(),
                matchedQuantity,
                unfilledQuantity,
                matchedPrice,
                LocalDateTime.now(),
                order.getOfferQuantity(),
                getExchangeRateFromRedis(order.getStockTicker()),
                order.getOfferPrice()
        );
        sendBuyTradeToExecution(event);
    } else {
        SellTradeMatchEvent event = new SellTradeMatchEvent(
                generateRandomId(),
                order.getOfferNumber(),
                order.getUserId(),
                order.getStockTicker(),
                matchedQuantity,
                unfilledQuantity,
                matchedPrice,
                LocalDateTime.now(),
                order.getOfferQuantity(),
                getExchangeRateFromRedis(order.getStockTicker()),
                order.getOfferPrice()
        );
        sendSellTradeToExecution(event);
    }
}

매수/매도 주문이 체결되면 BuyTradeMatchEvent,SellTradeMatchEvent 생성
체결 수량과 체결되지 않은 수량을 포함하여 Kafka로 Execution 모듈에 전송
부분 체결이 발생하면 남은 주문 수량을 유지하여 다시 매칭 가능

6. 미체결 주문 처리 (sendUnmatchedOrdersToOrderService)

private void sendUnmatchedOrdersToOrderService() {
   while (!unmatchedOrders.isEmpty()) {
       OrderCreateReqEvent unmatchedOrder = unmatchedOrders.poll();
       matchingProducer.sendUnmatchedOrderToOrderService(unmatchedOrder);
       log.info("미체결 주문 Order 모듈 전송: {}", unmatchedOrder);
   }
}

10초 내에 체결되지 않은 주문은 Order 모듈로 전송하여 미체결 주문으로 처리
사용자가 미체결 주문을 확인하고 취소할 수 있도록 관리

0개의 댓글