Skip to content

Commit c3b6165

Browse files
authored
Merge pull request #179 from CleanEngine/feat/trade-core
Feat/trade core
2 parents 8bf7b2e + 9242164 commit c3b6165

15 files changed

+321
-172
lines changed

build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ dependencies {
7777
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
7878

7979
testImplementation 'org.junit.platform:junit-platform-suite:1.10.0'
80+
81+
// QueryDSL
82+
implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
83+
annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
84+
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
85+
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
86+
8087
// Spring Security + OAuth2
8188
implementation 'org.springframework.boot:spring-boot-starter-security'
8289
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotificationHandler.java

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.cleanengine.coin.trade.application;
22

3-
import com.cleanengine.coin.trade.entity.Trade;
3+
import com.cleanengine.coin.order.domain.Order;
44
import lombok.extern.slf4j.Slf4j;
55
import org.springframework.messaging.simp.SimpMessagingTemplate;
66
import org.springframework.stereotype.Component;
@@ -23,30 +23,23 @@ public TradeExecutedNotificationHandler(SimpMessagingTemplate messagingTemplate)
2323
}
2424

2525
@TransactionalEventListener
26-
public void notifyAfterTradeExecuted(TradeExecutedEvent tradeExecutedEvent) {
27-
Trade trade = tradeExecutedEvent.getTrade();
28-
if (trade == null) {
29-
log.error("체결 알림 실패! trade == null");
26+
public void notifyAfterTradeExecuted(TradeOrderCompletedEvent tradeOrderCompletedEvent) {
27+
// TODO : 평균단가는 별도 계산해야 함
28+
Order order = tradeOrderCompletedEvent.getOrder();
29+
if (order == null) {
30+
log.error("체결 알림 실패! order == null");
3031
return ;
3132
}
3233

33-
Integer sellUserId = trade.getSellUserId();
34-
Integer buyUserId = trade.getBuyUserId();
35-
if (sellUserId == null || buyUserId == null) {
36-
log.error("체결 알림 실패! sellUserId: {}, buyUserId: {}", sellUserId, buyUserId);
34+
Integer userId = order.getUserId();
35+
if (userId == null) {
36+
log.error("체결 알림 실패! userId: {}", userId);
3737
return ;
3838
}
3939

40-
if (sellUserId != SELL_ORDER_BOT_ID) {
41-
TradeExecutedNotifyDto soldDto = TradeExecutedNotifyDto.of(trade, ASK);
42-
messagingTemplate.convertAndSend("/topic/tradeNotification/" + sellUserId, soldDto);
43-
}
44-
if (buyUserId != BUY_ORDER_BOT_ID) {
45-
TradeExecutedNotifyDto boughtDto = TradeExecutedNotifyDto.of(trade, BID);
46-
messagingTemplate.convertAndSend("/topic/tradeNotification/" + buyUserId, boughtDto);
47-
}
48-
if (sellUserId != SELL_ORDER_BOT_ID || buyUserId != BUY_ORDER_BOT_ID) {
49-
log.debug("{} 체결 이벤트 구독 : {}원에 {}개, 매수인: {}, 매도인: {}", trade.getTicker(), trade.getPrice(), trade.getSize(), buyUserId, sellUserId );
40+
if (userId != SELL_ORDER_BOT_ID && userId != BUY_ORDER_BOT_ID) {
41+
TradeOrderCompletedNotifyDto notifyDto = TradeOrderCompletedNotifyDto.of(order);
42+
messagingTemplate.convertAndSend("/topic/tradeNotification/" + userId, notifyDto);
5043
}
5144
}
5245

src/main/java/com/cleanengine/coin/trade/application/TradeExecutedNotifyDto.java

Lines changed: 0 additions & 43 deletions
This file was deleted.

src/main/java/com/cleanengine/coin/trade/application/TradeExecutor.java

Lines changed: 26 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
package com.cleanengine.coin.trade.application;
22

3-
import com.cleanengine.coin.common.error.BusinessException;
4-
import com.cleanengine.coin.common.response.ErrorStatus;
53
import com.cleanengine.coin.order.domain.BuyOrder;
64
import com.cleanengine.coin.order.domain.Order;
75
import com.cleanengine.coin.order.domain.OrderStatus;
86
import com.cleanengine.coin.order.domain.SellOrder;
97
import com.cleanengine.coin.order.domain.spi.WaitingOrders;
108
import com.cleanengine.coin.trade.entity.Trade;
11-
import com.cleanengine.coin.user.domain.Account;
12-
import com.cleanengine.coin.user.domain.Wallet;
139
import com.cleanengine.coin.user.info.application.AccountService;
1410
import com.cleanengine.coin.user.info.application.WalletService;
1511
import lombok.Getter;
@@ -33,10 +29,11 @@ public class TradeExecutor {
3329
private final AccountService accountService;
3430
@Getter
3531
private final TradeExecutedEventPublisher tradeExecutedEventPublisher;
32+
private final TradeOrderCompletedEventPublisher tradeOrderCompletedEventPublisher;
3633
private final TradeService tradeService;
3734

3835
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
39-
public void executeTrade(WaitingOrders waitingOrders, TradePair<Order, Order> tradePair, String ticker) {
36+
public Trade executeTrade(WaitingOrders waitingOrders, TradePair<Order, Order> tradePair, String ticker) {
4037
BuyOrder buyOrder = tradePair.getBuyOrder();
4138
SellOrder sellOrder = tradePair.getSellOrder();
4239
log.trace("{} - 체결 시작: 매수[{} {}원 {}개] / 매도[{} {}원 {}개]", ticker, buyOrder.getId(), buyOrder.getPrice(), buyOrder.getRemainingSize(),
@@ -64,8 +61,7 @@ public void executeTrade(WaitingOrders waitingOrders, TradePair<Order, Order> tr
6461
sellOrder.decreaseRemainingSize(tradedSize);
6562

6663
// 주문 완전체결 처리(잔여금액 or 잔여수량이 0)
67-
removeCompletedBuyOrder(waitingOrders, buyOrder);
68-
removeCompletedSellOrder(waitingOrders, sellOrder);
64+
removeCompletedOrders(waitingOrders, buyOrder, sellOrder);
6965

7066
tradeService.updateOrder(buyOrder);
7167
tradeService.updateOrder(sellOrder);
@@ -82,14 +78,14 @@ public void executeTrade(WaitingOrders waitingOrders, TradePair<Order, Order> tr
8278
}
8379

8480
// 지갑 누적계산
85-
this.updateWalletAfterTrade(buyOrder, ticker, tradedSize, totalTradedPrice);
86-
this.updateWalletAfterTrade(sellOrder, ticker, tradedSize, totalTradedPrice);
81+
walletService.updateWalletAfterTrade(buyOrder, ticker, tradedSize, totalTradedPrice);
8782

8883
// 체결내역 저장
89-
Trade trade = this.insertNewTrade(ticker, buyOrder, sellOrder, tradedSize, tradedPrice);
84+
Trade trade = Trade.of(ticker, LocalDateTime.now(), buyOrder.getUserId(), sellOrder.getUserId(), tradedPrice, tradedSize);
9085

9186
TradeExecutedEvent tradeExecutedEvent = TradeExecutedEvent.of(trade, buyOrder.getId(), sellOrder.getId());
9287
tradeExecutedEventPublisher.publish(tradeExecutedEvent);
88+
return trade;
9389
}
9490

9591
private static void checkZeroOrderAndThrowException(BuyOrder buyOrder, SellOrder sellOrder) {
@@ -105,35 +101,13 @@ else if (approxEquals(sellOrder.getRemainingSize(), 0.0))
105101
}
106102

107103
private void increaseAccountCash(Order order, Double amount) {
108-
Account account = accountService.findAccountByUserId(order.getUserId()).orElseThrow();
109-
accountService.save(account.increaseCash(amount));
110-
}
104+
int updatedRows = accountService.increaseAccountCash(order.getUserId(), amount);
111105

112-
private void updateWalletAfterTrade(Order order, String ticker, double tradedSize, double totalTradedPrice) {
113-
if (order instanceof BuyOrder) {
114-
Wallet buyerWallet = walletService.findWalletByUserIdAndTicker(order.getUserId(), ticker);
115-
double updatedBuySize = buyerWallet.getSize() + tradedSize;
116-
double currentBuyPrice = buyerWallet.getBuyPrice() == null ? 0.0 : buyerWallet.getBuyPrice();
117-
double updatedBuyPrice = ((currentBuyPrice * buyerWallet.getSize()) + totalTradedPrice) / updatedBuySize;
118-
buyerWallet.setSize(updatedBuySize);
119-
buyerWallet.setBuyPrice(updatedBuyPrice);
120-
// TODO : ROI 계산
121-
walletService.save(buyerWallet);
122-
} else if (order instanceof SellOrder) {
123-
// 매도 시에는 평단가 변동 없음
124-
Wallet sellerWallet = walletService.findWalletByUserIdAndTicker(order.getUserId(), ticker);
125-
walletService.save(sellerWallet);
126-
} else {
127-
throw new BusinessException("Unsupported order type: " + order.getClass().getName(), ErrorStatus.INTERNAL_SERVER_ERROR);
106+
if (updatedRows == 0) {
107+
throw new RuntimeException("account updatedRows == 0");
128108
}
129109
}
130110

131-
private Trade insertNewTrade(String ticker, BuyOrder buyOrder, SellOrder sellOrder, double tradeSize, Double tradePrice) {
132-
Trade newTrade = Trade.of(ticker, LocalDateTime.now(), buyOrder.getUserId(), sellOrder.getUserId(), tradePrice, tradeSize);
133-
134-
return tradeService.save(newTrade);
135-
}
136-
137111
private static TradeUnitPriceAndSize getTradeUnitPriceAndSize(BuyOrder buyOrder, SellOrder sellOrder) {
138112
double tradedPrice;
139113
double tradedSize;
@@ -180,25 +154,32 @@ private static void writeTradingLog(BuyOrder buyOrder, SellOrder sellOrder) {
180154
sellOrder.getRemainingSize());
181155
}
182156

183-
private static void removeCompletedBuyOrder(WaitingOrders waitingOrders, BuyOrder order) {
184-
boolean isOrderCompleted = (isMarketOrder(order) && approxEquals(order.getRemainingDeposit(), 0.0)) ||
185-
(isLimitOrder(order) && approxEquals(order.getRemainingSize(), 0.0));
186-
187-
if (isOrderCompleted) {
188-
waitingOrders.removeOrder(order);
189-
updateCompletedOrderStatus(order);
190-
}
157+
private void removeCompletedOrders(WaitingOrders waitingOrders, BuyOrder buyOrder, SellOrder sellOrder) {
158+
removeCompletedOrder(waitingOrders, buyOrder);
159+
removeCompletedOrder(waitingOrders, sellOrder);
191160
}
192161

193-
private static void removeCompletedSellOrder(WaitingOrders waitingOrders, SellOrder order) {
194-
boolean isOrderCompleted = approxEquals(order.getRemainingSize(), 0.0);
162+
private void removeCompletedOrder(WaitingOrders waitingOrders, Order order) {
163+
boolean isOrderCompleted = false;
164+
165+
if (order instanceof BuyOrder buyOrder) {
166+
isOrderCompleted = (isMarketOrder(buyOrder) && approxEquals(buyOrder.getRemainingDeposit(), 0.0)) ||
167+
(isLimitOrder(buyOrder) && approxEquals(buyOrder.getRemainingSize(), 0.0));
168+
} else if (order instanceof SellOrder sellOrder) {
169+
isOrderCompleted = approxEquals(sellOrder.getRemainingSize(), 0.0);
170+
}
195171

196172
if (isOrderCompleted) {
197173
waitingOrders.removeOrder(order);
198174
updateCompletedOrderStatus(order);
175+
publishOrderCompletionEvent(order);
199176
}
200177
}
201178

179+
private void publishOrderCompletionEvent(Order order) {
180+
tradeOrderCompletedEventPublisher.publish(TradeOrderCompletedEventImpl.of(order));
181+
}
182+
202183
private static void updateCompletedOrderStatus(Order order) {
203184
order.setState(OrderStatus.DONE);
204185
}

src/main/java/com/cleanengine/coin/trade/application/TradeFlowService.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@
44
import com.cleanengine.coin.order.domain.Order;
55
import com.cleanengine.coin.order.domain.spi.WaitingOrders;
66
import com.cleanengine.coin.order.domain.spi.WaitingOrdersManager;
7+
import com.cleanengine.coin.trade.entity.Trade;
8+
import com.cleanengine.coin.trade.repository.TradeRepository;
79
import lombok.RequiredArgsConstructor;
810
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.context.annotation.Profile;
912
import org.springframework.stereotype.Component;
1013

14+
import java.util.ArrayList;
15+
import java.util.List;
1116
import java.util.Optional;
17+
import java.util.concurrent.CountDownLatch;
1218

1319
@Slf4j
1420
@RequiredArgsConstructor
@@ -18,16 +24,31 @@ public class TradeFlowService {
1824
private final TradeMatcher tradeMatcher;
1925
private final TradeExecutor tradeExecutor;
2026
private final WaitingOrdersManager waitingOrdersManager;
27+
private final TradeRepository tradeRepository;
28+
29+
private CountDownLatch testLatch; // 테스트용 후크
30+
31+
@Profile("trade-load-test")
32+
public void setTestLatch(CountDownLatch latch) {
33+
this.testLatch = latch;
34+
}
2135

2236
public void execMatchAndTrade(String ticker) {
2337
WaitingOrders waitingOrders = waitingOrdersManager.getWaitingOrders(ticker);
2438
// TODO : peek() 해온 Order 객체들을 lock -> 체결 도중 취소 방지
2539
Optional<TradePair<Order, Order>> tradePair = tradeMatcher.matchOrders(waitingOrders);
2640
boolean continueProcessing = tradePair.isPresent();
41+
List<Trade> tradesToSave = new ArrayList<>();
2742

2843
while (continueProcessing) {
2944
try {
30-
tradeExecutor.executeTrade(waitingOrders, tradePair.get(), ticker);
45+
Trade trade = tradeExecutor.executeTrade(waitingOrders, tradePair.get(), ticker);
46+
tradesToSave.add(trade);
47+
if (tradesToSave.size() > 10000) {
48+
tradeRepository.saveAll(tradesToSave);
49+
tradesToSave.clear();
50+
}
51+
3152
tradePair = tradeMatcher.matchOrders(waitingOrders);
3253
continueProcessing = tradePair.isPresent();
3354
} catch (TradeZeroOrderException e) {
@@ -41,6 +62,15 @@ public void execMatchAndTrade(String ticker) {
4162
continueProcessing = false;
4263
}
4364
}
65+
66+
if (!tradesToSave.isEmpty()) {
67+
tradeRepository.saveAll(tradesToSave);
68+
tradesToSave.clear();
69+
}
70+
71+
if (testLatch != null) {
72+
testLatch.countDown();
73+
}
4474
}
4575

4676
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.cleanengine.coin.trade.application;
2+
3+
import com.cleanengine.coin.order.domain.Order;
4+
5+
public interface TradeOrderCompletedEvent {
6+
7+
Order getOrder();
8+
9+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.cleanengine.coin.trade.application;
2+
3+
import com.cleanengine.coin.order.domain.Order;
4+
import com.cleanengine.coin.trade.entity.Trade;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
@Getter
9+
@Builder
10+
public class TradeOrderCompletedEventImpl implements TradeOrderCompletedEvent {
11+
12+
Order order;
13+
14+
private TradeOrderCompletedEventImpl(Order order) {
15+
this.order = order;
16+
}
17+
18+
public static TradeOrderCompletedEventImpl of(Order order) {
19+
return new TradeOrderCompletedEventImpl(order);
20+
}
21+
22+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.cleanengine.coin.trade.application;
2+
3+
import org.springframework.context.ApplicationEventPublisher;
4+
import org.springframework.stereotype.Service;
5+
6+
@Service
7+
public class TradeOrderCompletedEventPublisher {
8+
9+
private final ApplicationEventPublisher publisher;
10+
11+
public TradeOrderCompletedEventPublisher(ApplicationEventPublisher publisher) {
12+
this.publisher = publisher;
13+
}
14+
15+
public void publish(TradeOrderCompletedEvent event) {
16+
publisher.publishEvent(event);
17+
}
18+
19+
}

0 commit comments

Comments
 (0)