diff --git a/src/main/java/com/cleanengine/coin/orderbook/application/service/OrderBookService.java b/src/main/java/com/cleanengine/coin/orderbook/application/service/OrderBookService.java index 51ee275e..acdbe58d 100644 --- a/src/main/java/com/cleanengine/coin/orderbook/application/service/OrderBookService.java +++ b/src/main/java/com/cleanengine/coin/orderbook/application/service/OrderBookService.java @@ -6,11 +6,14 @@ import com.cleanengine.coin.order.domain.spi.ActiveOrders; import com.cleanengine.coin.order.domain.spi.ActiveOrdersManager; import com.cleanengine.coin.orderbook.domain.OrderBookDomainService; +import com.cleanengine.coin.orderbook.dto.ClosingPriceDto; import com.cleanengine.coin.orderbook.dto.OrderBookInfo; import com.cleanengine.coin.orderbook.dto.OrderBookUnitInfo; +import com.cleanengine.coin.orderbook.infra.TradeQueryRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -22,6 +25,7 @@ public class OrderBookService implements UpdateOrderBookUsecase, ReadOrderBookUs private final ActiveOrdersManager activeOrdersManager; private final OrderBookDomainService orderBookDomainService; private final OrderBookUpdatedNotifierPort orderBookUpdatedNotifierPort; + private final TradeQueryService tradeQueryService; @Override public void updateOrderBookOnNewOrder(Order order) { @@ -67,15 +71,33 @@ private void updateOrderBookOnTradeExecuted(String ticker, Long orderId, boolean } private OrderBookInfo extractOrderBookInfo(String ticker){ + ClosingPriceDto finalClosingPriceDto = getYesterdayClosingPrice(ticker); + List buyOrderBookUnitInfos = orderBookDomainService.getBuyOrderBookList(ticker, 10) - .stream().map(OrderBookUnitInfo::new).toList(); + .stream() + .map(orderBookUnit -> new OrderBookUnitInfo(orderBookUnit, finalClosingPriceDto.closingPrice())) + .toList(); List sellOrderBookUnitInfos = orderBookDomainService.getSellOrderBookList(ticker, 10) - .stream().map(OrderBookUnitInfo::new).toList(); + .stream() + .map(orderBookUnit -> new OrderBookUnitInfo(orderBookUnit, finalClosingPriceDto.closingPrice())) + .toList(); + return new OrderBookInfo(ticker, buyOrderBookUnitInfos, sellOrderBookUnitInfos); } + private ClosingPriceDto getYesterdayClosingPrice(String ticker){ + LocalDate yesterday = LocalDate.now().minusDays(1); + ClosingPriceDto closingPriceDto = tradeQueryService.getYesterdayClosingPrice(ticker, yesterday); + + if(closingPriceDto == null) { + closingPriceDto = new ClosingPriceDto(ticker, yesterday, 0.0); + } + + return closingPriceDto; + } + private void sendOrderBookUpdated(String ticker){ orderBookUpdatedNotifierPort.sendOrderBooks(extractOrderBookInfo(ticker)); } diff --git a/src/main/java/com/cleanengine/coin/orderbook/application/service/TradeQueryService.java b/src/main/java/com/cleanengine/coin/orderbook/application/service/TradeQueryService.java new file mode 100644 index 00000000..d3f830ba --- /dev/null +++ b/src/main/java/com/cleanengine/coin/orderbook/application/service/TradeQueryService.java @@ -0,0 +1,22 @@ +package com.cleanengine.coin.orderbook.application.service; + +import com.cleanengine.coin.orderbook.dto.ClosingPriceDto; +import com.cleanengine.coin.orderbook.infra.TradeQueryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class TradeQueryService { + private final TradeQueryRepository tradeQueryRepository; + + @Transactional(isolation = Isolation.READ_COMMITTED) + public ClosingPriceDto getYesterdayClosingPrice(String ticker, LocalDate yesterdayDate) { + return tradeQueryRepository.getYesterdayClosingPrice(ticker, yesterdayDate); + } +} diff --git a/src/main/java/com/cleanengine/coin/orderbook/dto/ClosingPriceDto.java b/src/main/java/com/cleanengine/coin/orderbook/dto/ClosingPriceDto.java new file mode 100644 index 00000000..c05b3c8b --- /dev/null +++ b/src/main/java/com/cleanengine/coin/orderbook/dto/ClosingPriceDto.java @@ -0,0 +1,10 @@ +package com.cleanengine.coin.orderbook.dto; + +import com.querydsl.core.annotations.QueryProjection; + +import java.time.LocalDate; + +public record ClosingPriceDto(String ticker, LocalDate baseDate, Double closingPrice) { + @QueryProjection + public ClosingPriceDto {} +} diff --git a/src/main/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfo.java b/src/main/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfo.java index 1a893d4c..1a4d870d 100644 --- a/src/main/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfo.java +++ b/src/main/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfo.java @@ -4,9 +4,27 @@ public record OrderBookUnitInfo( Double price, - Double size + Double size, + Double priceChangePercent ){ - public OrderBookUnitInfo(OrderBookUnit orderBookUnit) { - this(orderBookUnit.getPrice(), orderBookUnit.getSize()); + public OrderBookUnitInfo{ + if(price == null || size == null || priceChangePercent == null){ + throw new IllegalArgumentException("price, size, priceChangePercent cannot be null."); + } + } + + public OrderBookUnitInfo(OrderBookUnit orderBookUnit, Double yesterdayClosingPrice) { + this(orderBookUnit.getPrice(), + orderBookUnit.getSize(), + calculateChangePercent(orderBookUnit.getPrice(), yesterdayClosingPrice)); + } + + private static Double calculateChangePercent(Double price, Double yesterdayClosingPrice) { + if(price == null || yesterdayClosingPrice == null){ + throw new IllegalArgumentException("price, yesterdayClosingPrice cannot be null."); + } + + return (yesterdayClosingPrice <= 0) ? + 0.0 : (price - yesterdayClosingPrice) / yesterdayClosingPrice * 100; } } diff --git a/src/main/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepository.java b/src/main/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepository.java new file mode 100644 index 00000000..f55c4c75 --- /dev/null +++ b/src/main/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepository.java @@ -0,0 +1,39 @@ +package com.cleanengine.coin.orderbook.infra; + +import com.cleanengine.coin.orderbook.dto.ClosingPriceDto; +import com.cleanengine.coin.orderbook.dto.QClosingPriceDto; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static com.cleanengine.coin.trade.entity.QTrade.trade; + +@Component +public class TradeQueryRepository { + private final JPAQueryFactory queryFactory; + + public TradeQueryRepository(EntityManager entityManager){ + this.queryFactory = new JPAQueryFactory(entityManager); + } + + public ClosingPriceDto getYesterdayClosingPrice(String ticker, LocalDate yesterdayDate) { + LocalDateTime yesterday = yesterdayDate.atStartOfDay(); + + return queryFactory + .select(new QClosingPriceDto( + trade.ticker, + Expressions.asDate(yesterdayDate).as("baseDate"), + trade.price)) + .from(trade) + .where( + trade.ticker.eq(ticker) + .and(trade.tradeTime.goe(yesterday)) + .and(trade.tradeTime.lt(yesterday.plusDays(1)))) + .orderBy(trade.tradeTime.desc(), trade.id.desc()) + .fetchFirst(); + } +} diff --git a/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookInfoSerializationTest.java b/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookInfoSerializationTest.java index 4ddf061e..78acaefe 100644 --- a/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookInfoSerializationTest.java +++ b/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookInfoSerializationTest.java @@ -21,22 +21,22 @@ static void initStatic(){ @Test void eachOrderBookHasOnePrice_serializeIt_resultEqualsAsExpected() throws JsonProcessingException { OrderBookInfo orderBookInfo = new OrderBookInfo("BTC", - List.of(new OrderBookUnitInfo(1.0, 1.0)), - List.of(new OrderBookUnitInfo( 2.0, 2.0))); + List.of(new OrderBookUnitInfo(1.0, 1.0, 0.0)), + List.of(new OrderBookUnitInfo( 2.0, 2.0, 0.0))); String json = objectMapper.writeValueAsString(orderBookInfo); System.out.println(json); - assertEquals("{\"ticker\":\"BTC\",\"buyOrderBookUnits\":[{\"price\":1.0,\"size\":1.0}],\"sellOrderBookUnits\":[{\"price\":2.0,\"size\":2.0}]}", json); + assertEquals("{\"ticker\":\"BTC\",\"buyOrderBookUnits\":[{\"price\":1.0,\"size\":1.0,\"priceChangePercent\":0.0}],\"sellOrderBookUnits\":[{\"price\":2.0,\"size\":2.0,\"priceChangePercent\":0.0}]}", json); } @Test void oneOfOrderBookIsEmpty_serializeIt_resultEqualsAsExpected() throws JsonProcessingException { OrderBookInfo orderBookInfo = new OrderBookInfo("BTC", - List.of(new OrderBookUnitInfo(1.0, 1.0)), + List.of(new OrderBookUnitInfo(1.0, 1.0, 0.0)), List.of()); String json = objectMapper.writeValueAsString(orderBookInfo); System.out.println(json); - assertEquals("{\"ticker\":\"BTC\",\"buyOrderBookUnits\":[{\"price\":1.0,\"size\":1.0}],\"sellOrderBookUnits\":[]}", json); + assertEquals("{\"ticker\":\"BTC\",\"buyOrderBookUnits\":[{\"price\":1.0,\"size\":1.0,\"priceChangePercent\":0.0}],\"sellOrderBookUnits\":[]}", json); } } \ No newline at end of file diff --git a/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfoTest.java b/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfoTest.java new file mode 100644 index 00000000..a814c755 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/orderbook/dto/OrderBookUnitInfoTest.java @@ -0,0 +1,94 @@ +package com.cleanengine.coin.orderbook.dto; + +import com.cleanengine.coin.orderbook.domain.BuyOrderBookUnit; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class OrderBookUnitInfoTest { + + @Nested + @DisplayName("기본 생성 유효성 테스트") + class CreateOrderBookUnitInfoTest { + @DisplayName("price가 null이라면 생성시 IllegalArgumentException을 반환한다.") + @Test + public void createOrderBookUnitInfoWithNullPrice_throwsIllegalArgumentException() { + Double nullPrice = null; + + assertThrows(IllegalArgumentException.class, () -> new OrderBookUnitInfo(nullPrice, 1.0, 1.0)); + } + + @DisplayName("size가 null이라면 생성시 IllegalArgumentException을 반환한다.") + @Test + public void createOrderBookUnitInfoWithNullSize_throwsIllegalArgumentException() { + Double nullSize = null; + + assertThrows(IllegalArgumentException.class, () -> new OrderBookUnitInfo(1.0, nullSize, 1.0)); + } + + @DisplayName("priceChangePercent가 null이라면 생성시 IllegalArgumentException을 반환한다.") + @Test + public void createOrderBookUnitInfoWithNullPriceChangePercent_throwsIllegalArgumentException() { + Double nullPriceChangePercent = null; + + assertThrows(IllegalArgumentException.class, () -> new OrderBookUnitInfo(1.0, 1.0, nullPriceChangePercent)); + } + } + + @Nested + @DisplayName("priceChangePercent 반영 테스트") + class ChangePercentTest { + @DisplayName("closingPrice가 0이하일 경우 percentChange도 0이다.") + @Test + public void createOrderBookUnitInfoWithZeroClosingPrice_percentChangeIsZero() { + Double closingPrice = 0.0; + + OrderBookUnitInfo orderBookUnitInfo = new OrderBookUnitInfo(1.0, 1.0, closingPrice); + + assertEquals(0.0, orderBookUnitInfo.priceChangePercent()); + } + + @DisplayName("closingPrice가 null이라면 IllegalArgumentException을 반환한다.") + @Test + public void createOrderBookUnitInfoWithNullClosingPrice_throwsIllegalArgumentException() { + Double nullClosingPrice = null; + BuyOrderBookUnit buyOrderBookUnit = new BuyOrderBookUnit(1.0, 1.0); + + assertThrows(IllegalArgumentException.class, () -> new OrderBookUnitInfo(buyOrderBookUnit, nullClosingPrice)); + } + + @DisplayName("price가 null이라면 IllegalArgumentException을 반환다.") + @Test + public void createOrderBookUnitInfoWithNullPrice_throwsIllegalArgumentException() { + Double nullPrice = null; + BuyOrderBookUnit buyOrderBookUnit = new BuyOrderBookUnit(nullPrice, 1.0); + + assertThrows(IllegalArgumentException.class, () -> new OrderBookUnitInfo(buyOrderBookUnit, 1.0)); + } + + @DisplayName("closingPrice가 비교대상 price의 절반이라면, percentChange는 +100.0이다.") + @Test + public void createOrderBookUnitInfoWithHalfClosingPrice_percentChangeIs100() { + Double closingPrice = 1.0; + BuyOrderBookUnit buyOrderBookUnit = new BuyOrderBookUnit(closingPrice * 2.0, 1.0); + + OrderBookUnitInfo orderBookUnitInfo = new OrderBookUnitInfo(buyOrderBookUnit, closingPrice); + + assertEquals(100.0, orderBookUnitInfo.priceChangePercent()); + } + + @DisplayName("closingPrice가 비교대상 price의 두배라면, percentChange는 -50.0이다.") + @Test + public void createOrderBookUnitInfoWithDoubleClosingPrice_percentChangeIsMinus50() { + Double closingPrice = 1.0; + BuyOrderBookUnit buyOrderBookUnit = new BuyOrderBookUnit(closingPrice / 2.0, 1.0); + + OrderBookUnitInfo orderBookUnitInfo = new OrderBookUnitInfo(buyOrderBookUnit, closingPrice); + + assertEquals(-50.0, orderBookUnitInfo.priceChangePercent()); + } + } +} diff --git a/src/test/java/com/cleanengine/coin/orderbook/infra/OrderBookUpdatedNotifierAdapterTest.java b/src/test/java/com/cleanengine/coin/orderbook/infra/OrderBookUpdatedNotifierAdapterTest.java index 8059137a..c8f0a468 100644 --- a/src/test/java/com/cleanengine/coin/orderbook/infra/OrderBookUpdatedNotifierAdapterTest.java +++ b/src/test/java/com/cleanengine/coin/orderbook/infra/OrderBookUpdatedNotifierAdapterTest.java @@ -22,8 +22,8 @@ public class OrderBookUpdatedNotifierAdapterTest extends WebSocketTest { @Test public void getOrderBooks() throws Exception { OrderBookInfo orderBookInfo = new OrderBookInfo("BTC", - List.of(new OrderBookUnitInfo(1.0, 1.0)), - List.of(new OrderBookUnitInfo( 2.0, 2.0))); + List.of(new OrderBookUnitInfo(1.0, 1.0, 0.0)), + List.of(new OrderBookUnitInfo( 2.0, 2.0, 0.0))); session.subscribe("/topic/orderbook/BTC", new GenericStompFrameHandler<>(OrderBookInfo.class, responseQueue)); diff --git a/src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryH2Test.java b/src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryH2Test.java new file mode 100644 index 00000000..82f597d9 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryH2Test.java @@ -0,0 +1,92 @@ +package com.cleanengine.coin.orderbook.infra; + +import com.cleanengine.coin.orderbook.dto.ClosingPriceDto; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; + +@ActiveProfiles("dev, it, h2-mem") +@DataJpaTest +@Import({TradeQueryRepository.class}) +public class TradeQueryRepositoryH2Test { + @Autowired + private TradeQueryRepository tradeQueryRepository; + + @DisplayName("어제 trade가 있었을 경우, 정상적으로 yesterdayClosingPrice를 조회한다.") + @Test + @Transactional + @SqlGroup({ + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertYesterdayTrade.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/clearTrade.sql", + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + }) + public void queryYesterdayClosingPriceWithTradeExecutedYesterday_shouldReturnSuccessfully() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNotNull(closingPriceDto); + assertEquals("BTC", closingPriceDto.ticker()); + assertEquals(yesterdayDate, closingPriceDto.baseDate()); + assertEquals(400.0, closingPriceDto.closingPrice()); + } + + @DisplayName("어제 trade가 없었을 경우, null을 조회한다.") + @Test + @Transactional + public void queryYesterdayClosingPriceWithoutYesterdayTrade_shouldReturnNull() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNull(closingPriceDto); + } + + @DisplayName("어제 trade가 없고, 오늘 00시 00분 00초의 trade가 있었을 때, null을 조회한다.") + @Test + @Transactional + @SqlGroup({ + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertStartOfTodayTrade.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/clearTrade.sql", + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + }) + public void queryYesterdayClosingPriceWithTradeExecutedToday_shouldReturnNull() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNull(closingPriceDto); + } + + @DisplayName("어제 같은 시간에 여러건의 trade가 있었을 때, id가 가장 큰 ClosingPrice를 조회한다.") + @Test + @Transactional + @SqlGroup({ + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertDuplicateTimeYesterdayTrade.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/clearTrade.sql", + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + }) + public void queryYesterdayClosingPriceWithDuplicateTimeTrades_shouldReturnBiggestIdDto() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNotNull(closingPriceDto); + assertEquals("BTC", closingPriceDto.ticker()); + assertEquals(yesterdayDate, closingPriceDto.baseDate()); + assertEquals(600.0, closingPriceDto.closingPrice()); + } +} diff --git a/src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryMariadbTest.java b/src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryMariadbTest.java new file mode 100644 index 00000000..7893c2a3 --- /dev/null +++ b/src/test/java/com/cleanengine/coin/orderbook/infra/TradeQueryRepositoryMariadbTest.java @@ -0,0 +1,78 @@ +package com.cleanengine.coin.orderbook.infra; + +import com.cleanengine.coin.base.MariaDBAdapterTest; +import com.cleanengine.coin.orderbook.dto.ClosingPriceDto; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; + +@Import({ + TradeQueryRepository.class +}) +public class TradeQueryRepositoryMariadbTest extends MariaDBAdapterTest { + @Autowired + private TradeQueryRepository tradeQueryRepository; + + @DisplayName("어제 trade가 있었을 경우, 정상적으로 yesterdayClosingPrice를 조회한다.") + @Test + @Transactional + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertYesterdayTrade.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + public void queryYesterdayClosingPriceWithTradeExecutedYesterday_shouldReturnSuccessfully() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNotNull(closingPriceDto); + assertEquals("BTC", closingPriceDto.ticker()); + assertEquals(yesterdayDate, closingPriceDto.baseDate()); + assertEquals(400.0, closingPriceDto.closingPrice()); + } + + @DisplayName("어제 trade가 없었을 경우, null을 조회한다.") + @Test + @Transactional + public void queryYesterdayClosingPriceWithoutYesterdayTrade_shouldReturnNull() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNull(closingPriceDto); + } + + @DisplayName("어제 trade가 없고, 오늘 00시 00분 00초의 trade가 있었을 때, null을 조회한다.") + @Test + @Transactional + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertStartOfTodayTrade.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + public void queryYesterdayClosingPriceWithTradeExecutedToday_shouldReturnNull() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNull(closingPriceDto); + } + + @DisplayName("어제 같은 시간에 여러건의 trade가 있었을 때, id가 가장 큰 ClosingPrice를 조회한다.") + @Test + @Transactional + @Sql(scripts = "classpath:com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertDuplicateTimeYesterdayTrade.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + public void queryYesterdayClosingPriceWithDuplicateTimeTrades_shouldReturnBiggestIdDto() { + LocalDate yesterdayDate = LocalDate.of(2025, 7, 1); + + ClosingPriceDto closingPriceDto = tradeQueryRepository.getYesterdayClosingPrice("BTC", yesterdayDate); + + assertNotNull(closingPriceDto); + assertEquals("BTC", closingPriceDto.ticker()); + assertEquals(yesterdayDate, closingPriceDto.baseDate()); + assertEquals(600.0, closingPriceDto.closingPrice()); + } +} diff --git a/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java b/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java index a89b282e..1c6d650a 100644 --- a/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java +++ b/src/test/java/com/cleanengine/coin/tool/TokenGenerator.java @@ -2,6 +2,7 @@ import com.cleanengine.coin.user.login.application.JWTUtil; import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; @@ -12,6 +13,7 @@ import java.util.ArrayList; import java.util.List; +@Disabled @SpringBootTest @Profile("dev, it") public class TokenGenerator { diff --git a/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/clearTrade.sql b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/clearTrade.sql new file mode 100644 index 00000000..0d04bda7 --- /dev/null +++ b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/clearTrade.sql @@ -0,0 +1 @@ +DELETE FROM trade; \ No newline at end of file diff --git a/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertDuplicateTimeYesterdayTrade.sql b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertDuplicateTimeYesterdayTrade.sql new file mode 100644 index 00000000..086aa653 --- /dev/null +++ b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertDuplicateTimeYesterdayTrade.sql @@ -0,0 +1,9 @@ +DELETE FROM trade; + +INSERT INTO trade + (trade_id, ticker, trade_time, buy_user_id, sell_user_id, price, size) +VALUES + (1, 'BTC', '2025-07-01 20:00:00.000000',1, 2, 300, 30), + (2, 'BTC', '2025-07-01 20:00:00.000000',1, 2, 400, 30), + (4, 'BTC', '2025-07-01 20:00:00.000000',1, 2, 600, 30), + (3, 'BTC', '2025-07-01 20:00:00.000000',1, 2, 500, 30); diff --git a/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertStartOfTodayTrade.sql b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertStartOfTodayTrade.sql new file mode 100644 index 00000000..d772a767 --- /dev/null +++ b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertStartOfTodayTrade.sql @@ -0,0 +1,4 @@ +DELETE FROM trade; + +INSERT INTO trade (trade_id, ticker, trade_time, buy_user_id, sell_user_id, price, size) VALUES + (1, 'BTC', '2025-07-02 00:00:00.000000',1, 2, 300, 30); \ No newline at end of file diff --git a/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertYesterdayTrade.sql b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertYesterdayTrade.sql new file mode 100644 index 00000000..670d9667 --- /dev/null +++ b/src/test/resources/com/cleanengine/coin/orderbook/infra/TradeQueryRepository/insertYesterdayTrade.sql @@ -0,0 +1,9 @@ +DELETE FROM trade; + +INSERT INTO trade + (trade_id, ticker, trade_time, buy_user_id, sell_user_id, price, size) +VALUES + (1, 'BTC', '2025-07-01 19:00:00.000000',1, 2, 300, 30), + (2, 'BTC', '2025-07-01 20:00:00.000000',1, 2, 400, 30), + (3, 'BTC', '2025-07-01 17:00:00.000000',1, 2, 500, 30), + (4, 'BTC', '2025-07-01 18:00:00.000000',1, 2, 600, 30); \ No newline at end of file