diff --git a/src/main/kotlin/com/frankenergie/smartasset/controller/MarketOverviewController.kt b/src/main/kotlin/com/frankenergie/smartasset/controller/MarketOverviewController.kt new file mode 100644 index 0000000..1885de4 --- /dev/null +++ b/src/main/kotlin/com/frankenergie/smartasset/controller/MarketOverviewController.kt @@ -0,0 +1,21 @@ +package com.frankenergie.smartasset.controller + +import com.frankenergie.smartasset.model.MarketOverviewResponse +import com.frankenergie.smartasset.model.OrderUpdateRequest +import com.frankenergie.smartasset.model.OrderUpdateResponse +import com.frankenergie.smartasset.service.OrderBookService +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api") +class MarketOverviewController(private val orderBookService: OrderBookService) { + @GetMapping("/market/overview") + fun getMarketOverview(): ResponseEntity { + val response = orderBookService.getMarketOverview() + return ResponseEntity.ok(response) + } +} diff --git a/src/main/kotlin/com/frankenergie/smartasset/model/QuarterOverview.kt b/src/main/kotlin/com/frankenergie/smartasset/model/QuarterOverview.kt new file mode 100644 index 0000000..a6ffddb --- /dev/null +++ b/src/main/kotlin/com/frankenergie/smartasset/model/QuarterOverview.kt @@ -0,0 +1,12 @@ +package com.frankenergie.smartasset.model + +import com.fasterxml.jackson.annotation.JsonProperty +import java.math.BigDecimal +import java.time.LocalDateTime + +data class QuarterOverview( + @JsonProperty("delivery_start_time") val deliveryStartTime: LocalDateTime, + @JsonProperty("delivery_end_time") val deliveryEndTime: LocalDateTime, + @JsonProperty("highest_buy_price") val highestBuyPrice: BigDecimal?, + @JsonProperty("lowest_sell_price") val lowestSellPrice: BigDecimal? +) \ No newline at end of file diff --git a/src/main/kotlin/com/frankenergie/smartasset/model/QuarterOverviewResponse.kt b/src/main/kotlin/com/frankenergie/smartasset/model/QuarterOverviewResponse.kt new file mode 100644 index 0000000..3cdce78 --- /dev/null +++ b/src/main/kotlin/com/frankenergie/smartasset/model/QuarterOverviewResponse.kt @@ -0,0 +1,9 @@ +package com.frankenergie.smartasset.model + +import com.fasterxml.jackson.annotation.JsonProperty +import java.math.BigDecimal +import java.time.LocalDateTime + +data class MarketOverviewResponse( + @JsonProperty("quarters") val quarters: List +) diff --git a/src/main/kotlin/com/frankenergie/smartasset/service/OrderBookService.kt b/src/main/kotlin/com/frankenergie/smartasset/service/OrderBookService.kt index 6a135eb..6165c80 100644 --- a/src/main/kotlin/com/frankenergie/smartasset/service/OrderBookService.kt +++ b/src/main/kotlin/com/frankenergie/smartasset/service/OrderBookService.kt @@ -2,8 +2,10 @@ package com.frankenergie.smartasset.service import com.frankenergie.smartasset.domain.OrderBook import com.frankenergie.smartasset.model.DeliveryPeriod +import com.frankenergie.smartasset.model.MarketOverviewResponse import com.frankenergie.smartasset.model.OrderUpdateRequest import com.frankenergie.smartasset.model.OrderUpdateResponse +import com.frankenergie.smartasset.model.QuarterOverview import org.springframework.stereotype.Service import java.time.Instant import java.util.UUID @@ -30,5 +32,19 @@ class OrderBookService { return OrderUpdateResponse(orderId = orderId, status = status, timestamp = Instant.now()) } + fun getMarketOverview(): MarketOverviewResponse { + val quarters = orderBook.getAllPeriods() + .sortedBy { it.startTime } + .map { period -> + QuarterOverview( + deliveryStartTime = period.startTime, + deliveryEndTime = period.endTime, + highestBuyPrice = orderBook.getBestBid(period)?.price, + lowestSellPrice = orderBook.getBestAsk(period)?.price + ) + } + return MarketOverviewResponse(quarters) + } + fun getOrderBook(): OrderBook = orderBook } diff --git a/src/test/kotlin/com/frankenergie/smartasset/controller/MarketOverviewControllerTest.kt b/src/test/kotlin/com/frankenergie/smartasset/controller/MarketOverviewControllerTest.kt new file mode 100644 index 0000000..5829115 --- /dev/null +++ b/src/test/kotlin/com/frankenergie/smartasset/controller/MarketOverviewControllerTest.kt @@ -0,0 +1,82 @@ +package com.frankenergie.smartasset.controller + +import com.fasterxml.jackson.databind.ObjectMapper +import com.frankenergie.smartasset.model.OrderSide +import com.frankenergie.smartasset.model.OrderUpdateRequest +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import java.math.BigDecimal +import java.time.LocalDateTime + +@SpringBootTest +@AutoConfigureMockMvc +class MarketOverviewControllerTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var objectMapper: ObjectMapper + + @Test + fun `GET market overview returns empty list when no orders`() { + mockMvc.get("/api/market/overview") + .andExpect { + status { isOk() } + jsonPath("$.quarters") { isArray() } + } + } + + @Test + fun `GET market overview returns quarters with best prices`() { + // Add a buy order + val buyRequest = OrderUpdateRequest( + deliveryStartTime = LocalDateTime.of(2024, 6, 15, 10, 0), + deliveryEndTime = LocalDateTime.of(2024, 6, 15, 10, 15), + orderSide = OrderSide.BUY, + quantity = BigDecimal("10"), + price = BigDecimal("50.00") + ) + mockMvc.post("/api/orderupdate") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(buyRequest) + } + + // Add a higher buy order for same quarter + val higherBuyRequest = buyRequest.copy(price = BigDecimal("52.00")) + mockMvc.post("/api/orderupdate") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(higherBuyRequest) + } + + // Add a sell order + val sellRequest = buyRequest.copy(orderSide = OrderSide.SELL, price = BigDecimal("55.00")) + mockMvc.post("/api/orderupdate") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(sellRequest) + } + + // Add a lower sell order + val lowerSellRequest = buyRequest.copy(orderSide = OrderSide.SELL, price = BigDecimal("53.00")) + mockMvc.post("/api/orderupdate") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(lowerSellRequest) + } + + mockMvc.get("/api/market/overview") + .andExpect { + status { isOk() } + jsonPath("$.quarters.length()") { value(1) } + jsonPath("$.quarters[0].delivery_start_time") { exists() } + jsonPath("$.quarters[0].delivery_end_time") { exists() } + jsonPath("$.quarters[0].highest_buy_price") { value(52.00) } + jsonPath("$.quarters[0].lowest_sell_price") { value(53.00) } + } + } +}