OrderBookService implemented and tested
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
package com.frankenergie.smartasset.domain
|
||||||
|
|
||||||
|
import com.frankenergie.smartasset.model.DeliveryPeriod
|
||||||
|
import com.frankenergie.smartasset.model.OrderSide
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.util.TreeMap
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
data class PriceLevel(
|
||||||
|
val price: BigDecimal,
|
||||||
|
val quantity: BigDecimal
|
||||||
|
)
|
||||||
|
|
||||||
|
class OrderBook {
|
||||||
|
|
||||||
|
private val bidsPerPeriod: ConcurrentHashMap<DeliveryPeriod, TreeMap<BigDecimal, BigDecimal>> = ConcurrentHashMap()
|
||||||
|
private val asksPerPeriod: ConcurrentHashMap<DeliveryPeriod, TreeMap<BigDecimal, BigDecimal>> = ConcurrentHashMap()
|
||||||
|
|
||||||
|
fun processOrder(period: DeliveryPeriod, side: OrderSide, quantity: BigDecimal, price: BigDecimal): BigDecimal {
|
||||||
|
val bids = bidsPerPeriod.computeIfAbsent(period) { TreeMap(Comparator.reverseOrder()) }
|
||||||
|
val asks = asksPerPeriod.computeIfAbsent(period) { TreeMap() }
|
||||||
|
|
||||||
|
return when (side) {
|
||||||
|
OrderSide.BUY -> processBuyOrder(bids, asks, quantity, price)
|
||||||
|
OrderSide.SELL -> processSellOrder(bids, asks, quantity, price)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processBuyOrder(
|
||||||
|
bids: TreeMap<BigDecimal, BigDecimal>,
|
||||||
|
asks: TreeMap<BigDecimal, BigDecimal>,
|
||||||
|
quantity: BigDecimal,
|
||||||
|
price: BigDecimal
|
||||||
|
): BigDecimal {
|
||||||
|
var remaining = quantity
|
||||||
|
|
||||||
|
val iterator = asks.entries.iterator()
|
||||||
|
while (iterator.hasNext() && remaining > BigDecimal.ZERO) {
|
||||||
|
val (askPrice, askQty) = iterator.next()
|
||||||
|
if (askPrice > price) break
|
||||||
|
|
||||||
|
val matched = remaining.min(askQty)
|
||||||
|
remaining -= matched
|
||||||
|
val newQty = askQty - matched
|
||||||
|
if (newQty <= BigDecimal.ZERO) iterator.remove() else asks[askPrice] = newQty
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining > BigDecimal.ZERO) {
|
||||||
|
bids.merge(price, remaining) { old, new -> old + new }
|
||||||
|
}
|
||||||
|
|
||||||
|
return quantity - remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processSellOrder(
|
||||||
|
bids: TreeMap<BigDecimal, BigDecimal>,
|
||||||
|
asks: TreeMap<BigDecimal, BigDecimal>,
|
||||||
|
quantity: BigDecimal,
|
||||||
|
price: BigDecimal
|
||||||
|
): BigDecimal {
|
||||||
|
var remaining = quantity
|
||||||
|
|
||||||
|
val iterator = bids.entries.iterator()
|
||||||
|
while (iterator.hasNext() && remaining > BigDecimal.ZERO) {
|
||||||
|
val (bidPrice, bidQty) = iterator.next()
|
||||||
|
if (bidPrice < price) break
|
||||||
|
|
||||||
|
val matched = remaining.min(bidQty)
|
||||||
|
remaining -= matched
|
||||||
|
val newQty = bidQty - matched
|
||||||
|
if (newQty <= BigDecimal.ZERO) iterator.remove() else bids[bidPrice] = newQty
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining > BigDecimal.ZERO) {
|
||||||
|
asks.merge(price, remaining) { old, new -> old + new }
|
||||||
|
}
|
||||||
|
|
||||||
|
return quantity - remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBids(period: DeliveryPeriod): Map<BigDecimal, BigDecimal> = bidsPerPeriod[period]?.toMap() ?: emptyMap()
|
||||||
|
|
||||||
|
fun getAsks(period: DeliveryPeriod): Map<BigDecimal, BigDecimal> = asksPerPeriod[period]?.toMap() ?: emptyMap()
|
||||||
|
|
||||||
|
fun getBestBid(period: DeliveryPeriod): PriceLevel? =
|
||||||
|
bidsPerPeriod[period]?.firstEntry()?.let { PriceLevel(it.key, it.value) }
|
||||||
|
|
||||||
|
fun getBestAsk(period: DeliveryPeriod): PriceLevel? =
|
||||||
|
asksPerPeriod[period]?.firstEntry()?.let { PriceLevel(it.key, it.value) }
|
||||||
|
|
||||||
|
fun getAllPeriods(): Set<DeliveryPeriod> = bidsPerPeriod.keys + asksPerPeriod.keys
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.frankenergie.smartasset.model
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class DeliveryPeriod(
|
||||||
|
val startTime: LocalDateTime,
|
||||||
|
val endTime: LocalDateTime
|
||||||
|
)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.frankenergie.smartasset.model
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
|
data class DeliveryQuarter(
|
||||||
|
val startTime: LocalDateTime
|
||||||
|
) {
|
||||||
|
val endTime: LocalDateTime = startTime.plusMinutes(15)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(dateTime: LocalDateTime): DeliveryQuarter {
|
||||||
|
val minute = (dateTime.minute / 15) * 15
|
||||||
|
val truncated = dateTime.truncatedTo(ChronoUnit.HOURS).plusMinutes(minute.toLong())
|
||||||
|
return DeliveryQuarter(truncated)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromRange(start: LocalDateTime, end: LocalDateTime): List<DeliveryQuarter> {
|
||||||
|
val quarters = mutableListOf<DeliveryQuarter>()
|
||||||
|
var current = from(start)
|
||||||
|
while (current.startTime < end) {
|
||||||
|
quarters.add(current)
|
||||||
|
current = DeliveryQuarter(current.endTime)
|
||||||
|
}
|
||||||
|
return quarters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/main/kotlin/com/frankenergie/smartasset/model/Order.kt
Normal file
19
src/main/kotlin/com/frankenergie/smartasset/model/Order.kt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package com.frankenergie.smartasset.model
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class Order(
|
||||||
|
val orderId: String,
|
||||||
|
val deliveryStartTime: LocalDateTime,
|
||||||
|
val deliveryEndTime: LocalDateTime,
|
||||||
|
val orderSide: OrderSide,
|
||||||
|
val originalQuantity: BigDecimal,
|
||||||
|
var remainingQuantity: BigDecimal,
|
||||||
|
val price: BigDecimal,
|
||||||
|
val createdAt: Instant
|
||||||
|
) {
|
||||||
|
val isFilled: Boolean
|
||||||
|
get() = remainingQuantity.compareTo(BigDecimal.ZERO) == 0
|
||||||
|
}
|
||||||
14
src/main/kotlin/com/frankenergie/smartasset/model/Trade.kt
Normal file
14
src/main/kotlin/com/frankenergie/smartasset/model/Trade.kt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.frankenergie.smartasset.model
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class Trade(
|
||||||
|
val tradeId: String,
|
||||||
|
val buyOrderId: String,
|
||||||
|
val sellOrderId: String,
|
||||||
|
val price: BigDecimal,
|
||||||
|
val quantity: BigDecimal,
|
||||||
|
val executedAt: Instant
|
||||||
|
)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.frankenergie.smartasset.service
|
package com.frankenergie.smartasset.service
|
||||||
|
|
||||||
|
import com.frankenergie.smartasset.domain.OrderBook
|
||||||
|
import com.frankenergie.smartasset.model.DeliveryPeriod
|
||||||
import com.frankenergie.smartasset.model.OrderUpdateRequest
|
import com.frankenergie.smartasset.model.OrderUpdateRequest
|
||||||
import com.frankenergie.smartasset.model.OrderUpdateResponse
|
import com.frankenergie.smartasset.model.OrderUpdateResponse
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
@@ -9,15 +11,24 @@ import java.util.UUID
|
|||||||
@Service
|
@Service
|
||||||
class OrderBookService {
|
class OrderBookService {
|
||||||
|
|
||||||
|
private val orderBook = OrderBook()
|
||||||
|
|
||||||
fun processOrder(request: OrderUpdateRequest): OrderUpdateResponse {
|
fun processOrder(request: OrderUpdateRequest): OrderUpdateResponse {
|
||||||
val orderId = UUID.randomUUID().toString()
|
val orderId = UUID.randomUUID().toString()
|
||||||
|
|
||||||
// TODO: implement order book logic
|
val period = DeliveryPeriod(request.deliveryStartTime, request.deliveryEndTime)
|
||||||
|
val matchedQuantity = orderBook.processOrder(period, request.orderSide, request.quantity, request.price)
|
||||||
|
|
||||||
return OrderUpdateResponse(
|
val status = when {
|
||||||
orderId = orderId,
|
matchedQuantity == request.quantity -> "FILLED"
|
||||||
status = "ACCEPTED",
|
matchedQuantity > java.math.BigDecimal.ZERO -> "PARTIALLY_FILLED"
|
||||||
timestamp = Instant.now()
|
else -> "ACCEPTED"
|
||||||
)
|
}
|
||||||
|
|
||||||
|
// TODO: write this to a file
|
||||||
|
|
||||||
|
return OrderUpdateResponse(orderId = orderId, status = status, timestamp = Instant.now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getOrderBook(): OrderBook = orderBook
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package com.frankenergie.smartasset.domain
|
||||||
|
|
||||||
|
import com.frankenergie.smartasset.model.DeliveryPeriod
|
||||||
|
import com.frankenergie.smartasset.model.OrderSide
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
class OrderBookTest {
|
||||||
|
|
||||||
|
private lateinit var orderBook: OrderBook
|
||||||
|
private lateinit var period: DeliveryPeriod
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
orderBook = OrderBook()
|
||||||
|
period = DeliveryPeriod(
|
||||||
|
LocalDateTime.of(2024, 1, 15, 10, 0),
|
||||||
|
LocalDateTime.of(2024, 1, 15, 10, 15)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buy order added to empty book`() {
|
||||||
|
val matched = orderBook.processOrder(period, OrderSide.BUY, BigDecimal("10"), BigDecimal("50.00"))
|
||||||
|
|
||||||
|
assertEquals(BigDecimal.ZERO, matched)
|
||||||
|
assertEquals(BigDecimal("10"), orderBook.getBids(period)[BigDecimal("50.00")])
|
||||||
|
assertTrue(orderBook.getAsks(period).isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `sell order added to empty book`() {
|
||||||
|
val matched = orderBook.processOrder(period, OrderSide.SELL, BigDecimal("10"), BigDecimal("55.00"))
|
||||||
|
|
||||||
|
assertEquals(BigDecimal.ZERO, matched)
|
||||||
|
assertEquals(BigDecimal("10"), orderBook.getAsks(period)[BigDecimal("55.00")])
|
||||||
|
assertTrue(orderBook.getBids(period).isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buy order does not match higher priced sell order`() {
|
||||||
|
orderBook.processOrder(period, OrderSide.SELL, BigDecimal("10"), BigDecimal("55.00"))
|
||||||
|
val matched = orderBook.processOrder(period, OrderSide.BUY, BigDecimal("5"), BigDecimal("50.00"))
|
||||||
|
|
||||||
|
assertEquals(BigDecimal.ZERO, matched)
|
||||||
|
assertEquals(BigDecimal("10"), orderBook.getAsks(period)[BigDecimal("55.00")])
|
||||||
|
assertEquals(BigDecimal("5"), orderBook.getBids(period)[BigDecimal("50.00")])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buy order fully matches equal priced sell order`() {
|
||||||
|
orderBook.processOrder(period, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00"))
|
||||||
|
val matched = orderBook.processOrder(period, OrderSide.BUY, BigDecimal("10"), BigDecimal("50.00"))
|
||||||
|
|
||||||
|
assertEquals(BigDecimal("10"), matched)
|
||||||
|
assertTrue(orderBook.getAsks(period).isEmpty())
|
||||||
|
assertTrue(orderBook.getBids(period).isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buy order fully matches lower priced sell order`() {
|
||||||
|
orderBook.processOrder(period, OrderSide.SELL, BigDecimal("10"), BigDecimal("48.00"))
|
||||||
|
val matched = orderBook.processOrder(period, OrderSide.BUY, BigDecimal("10"), BigDecimal("50.00"))
|
||||||
|
|
||||||
|
assertEquals(BigDecimal("10"), matched)
|
||||||
|
assertTrue(orderBook.getAsks(period).isEmpty())
|
||||||
|
assertTrue(orderBook.getBids(period).isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buy order partially matches sell order - remaining added to bids`() {
|
||||||
|
orderBook.processOrder(period, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00"))
|
||||||
|
val matched = orderBook.processOrder(period, OrderSide.BUY, BigDecimal("15"), BigDecimal("50.00"))
|
||||||
|
|
||||||
|
assertEquals(BigDecimal("10"), matched)
|
||||||
|
assertTrue(orderBook.getAsks(period).isEmpty())
|
||||||
|
assertEquals(BigDecimal("5"), orderBook.getBids(period)[BigDecimal("50.00")])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buy order partially fills sell order - remaining stays in asks`() {
|
||||||
|
orderBook.processOrder(period, OrderSide.SELL, BigDecimal("15"), BigDecimal("50.00"))
|
||||||
|
val matched = orderBook.processOrder(period, OrderSide.BUY, BigDecimal("10"), BigDecimal("50.00"))
|
||||||
|
|
||||||
|
assertEquals(BigDecimal("10"), matched)
|
||||||
|
assertEquals(BigDecimal("5"), orderBook.getAsks(period)[BigDecimal("50.00")])
|
||||||
|
assertTrue(orderBook.getBids(period).isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `sell order fully matches higher priced buy order`() {
|
||||||
|
orderBook.processOrder(period, OrderSide.BUY, BigDecimal("10"), BigDecimal("55.00"))
|
||||||
|
val matched = orderBook.processOrder(period, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00"))
|
||||||
|
|
||||||
|
assertEquals(BigDecimal("10"), matched)
|
||||||
|
assertTrue(orderBook.getBids(period).isEmpty())
|
||||||
|
assertTrue(orderBook.getAsks(period).isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `sell order does not match lower priced buy order`() {
|
||||||
|
orderBook.processOrder(period, OrderSide.BUY, BigDecimal("10"), BigDecimal("50.00"))
|
||||||
|
val matched = orderBook.processOrder(period, OrderSide.SELL, BigDecimal("5"), BigDecimal("55.00"))
|
||||||
|
|
||||||
|
assertEquals(BigDecimal.ZERO, matched)
|
||||||
|
assertEquals(BigDecimal("10"), orderBook.getBids(period)[BigDecimal("50.00")])
|
||||||
|
assertEquals(BigDecimal("5"), orderBook.getAsks(period)[BigDecimal("55.00")])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buy order matches multiple price levels`() {
|
||||||
|
orderBook.processOrder(period, OrderSide.SELL, BigDecimal("5"), BigDecimal("48.00"))
|
||||||
|
orderBook.processOrder(period, OrderSide.SELL, BigDecimal("5"), BigDecimal("49.00"))
|
||||||
|
orderBook.processOrder(period, OrderSide.SELL, BigDecimal("5"), BigDecimal("52.00"))
|
||||||
|
|
||||||
|
val matched = orderBook.processOrder(period, OrderSide.BUY, BigDecimal("12"), BigDecimal("50.00"))
|
||||||
|
|
||||||
|
assertEquals(BigDecimal("10"), matched)
|
||||||
|
assertEquals(BigDecimal("2"), orderBook.getBids(period)[BigDecimal("50.00")])
|
||||||
|
assertEquals(BigDecimal("5"), orderBook.getAsks(period)[BigDecimal("52.00")])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `sell order matches multiple price levels`() {
|
||||||
|
orderBook.processOrder(period, OrderSide.BUY, BigDecimal("5"), BigDecimal("52.00"))
|
||||||
|
orderBook.processOrder(period, OrderSide.BUY, BigDecimal("5"), BigDecimal("51.00"))
|
||||||
|
orderBook.processOrder(period, OrderSide.BUY, BigDecimal("5"), BigDecimal("48.00"))
|
||||||
|
|
||||||
|
val matched = orderBook.processOrder(period, OrderSide.SELL, BigDecimal("12"), BigDecimal("50.00"))
|
||||||
|
|
||||||
|
assertEquals(BigDecimal("10"), matched)
|
||||||
|
assertEquals(BigDecimal("2"), orderBook.getAsks(period)[BigDecimal("50.00")])
|
||||||
|
assertEquals(BigDecimal("5"), orderBook.getBids(period)[BigDecimal("48.00")])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `orders aggregate at same price level`() {
|
||||||
|
orderBook.processOrder(period, OrderSide.BUY, BigDecimal("5"), BigDecimal("50.00"))
|
||||||
|
orderBook.processOrder(period, OrderSide.BUY, BigDecimal("3"), BigDecimal("50.00"))
|
||||||
|
|
||||||
|
assertEquals(BigDecimal("8"), orderBook.getBids(period)[BigDecimal("50.00")])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `separate order books per delivery period`() {
|
||||||
|
val period1 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 0), LocalDateTime.of(2024, 1, 15, 10, 15))
|
||||||
|
val period2 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 15), LocalDateTime.of(2024, 1, 15, 10, 30))
|
||||||
|
|
||||||
|
orderBook.processOrder(period1, OrderSide.BUY, BigDecimal("10"), BigDecimal("50.00"))
|
||||||
|
orderBook.processOrder(period2, OrderSide.BUY, BigDecimal("20"), BigDecimal("55.00"))
|
||||||
|
|
||||||
|
assertEquals(BigDecimal("10"), orderBook.getBids(period1)[BigDecimal("50.00")])
|
||||||
|
assertEquals(BigDecimal("20"), orderBook.getBids(period2)[BigDecimal("55.00")])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getBestBid returns highest bid`() {
|
||||||
|
orderBook.processOrder(period, OrderSide.BUY, BigDecimal("5"), BigDecimal("50.00"))
|
||||||
|
orderBook.processOrder(period, OrderSide.BUY, BigDecimal("3"), BigDecimal("52.00"))
|
||||||
|
|
||||||
|
val best = orderBook.getBestBid(period)!!
|
||||||
|
assertEquals(BigDecimal("52.00"), best.price)
|
||||||
|
assertEquals(BigDecimal("3"), best.quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getBestAsk returns lowest ask`() {
|
||||||
|
orderBook.processOrder(period, OrderSide.SELL, BigDecimal("5"), BigDecimal("55.00"))
|
||||||
|
orderBook.processOrder(period, OrderSide.SELL, BigDecimal("3"), BigDecimal("53.00"))
|
||||||
|
|
||||||
|
val best = orderBook.getBestAsk(period)!!
|
||||||
|
assertEquals(BigDecimal("53.00"), best.price)
|
||||||
|
assertEquals(BigDecimal("3"), best.quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getBestBid returns null for empty book`() {
|
||||||
|
assertNull(orderBook.getBestBid(period))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getBestAsk returns null for empty book`() {
|
||||||
|
assertNull(orderBook.getBestAsk(period))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.frankenergie.smartasset.service
|
||||||
|
|
||||||
|
import com.frankenergie.smartasset.model.OrderSide
|
||||||
|
import com.frankenergie.smartasset.model.OrderUpdateRequest
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
class OrderBookServiceTest {
|
||||||
|
|
||||||
|
private lateinit var service: OrderBookService
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
service = OrderBookService()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `processOrder returns ACCEPTED when no match`() {
|
||||||
|
val request = OrderUpdateRequest(
|
||||||
|
deliveryStartTime = LocalDateTime.of(2024, 1, 15, 10, 0),
|
||||||
|
deliveryEndTime = LocalDateTime.of(2024, 1, 15, 10, 15),
|
||||||
|
orderSide = OrderSide.BUY,
|
||||||
|
quantity = BigDecimal("10"),
|
||||||
|
price = BigDecimal("50.00")
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = service.processOrder(request)
|
||||||
|
|
||||||
|
assertNotNull(response.orderId)
|
||||||
|
assertEquals("ACCEPTED", response.status)
|
||||||
|
assertNotNull(response.timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `processOrder returns FILLED when fully matched`() {
|
||||||
|
val sellRequest = OrderUpdateRequest(
|
||||||
|
deliveryStartTime = LocalDateTime.of(2024, 1, 15, 10, 0),
|
||||||
|
deliveryEndTime = LocalDateTime.of(2024, 1, 15, 10, 15),
|
||||||
|
orderSide = OrderSide.SELL,
|
||||||
|
quantity = BigDecimal("10"),
|
||||||
|
price = BigDecimal("50.00")
|
||||||
|
)
|
||||||
|
service.processOrder(sellRequest)
|
||||||
|
|
||||||
|
val buyRequest = sellRequest.copy(orderSide = OrderSide.BUY)
|
||||||
|
val response = service.processOrder(buyRequest)
|
||||||
|
|
||||||
|
assertEquals("FILLED", response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `processOrder returns PARTIALLY_FILLED when partially matched`() {
|
||||||
|
val sellRequest = OrderUpdateRequest(
|
||||||
|
deliveryStartTime = LocalDateTime.of(2024, 1, 15, 10, 0),
|
||||||
|
deliveryEndTime = LocalDateTime.of(2024, 1, 15, 10, 15),
|
||||||
|
orderSide = OrderSide.SELL,
|
||||||
|
quantity = BigDecimal("5"),
|
||||||
|
price = BigDecimal("50.00")
|
||||||
|
)
|
||||||
|
service.processOrder(sellRequest)
|
||||||
|
|
||||||
|
val buyRequest = sellRequest.copy(orderSide = OrderSide.BUY, quantity = BigDecimal("10"))
|
||||||
|
val response = service.processOrder(buyRequest)
|
||||||
|
|
||||||
|
assertEquals("PARTIALLY_FILLED", response.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user