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
|
||||
|
||||
import com.frankenergie.smartasset.domain.OrderBook
|
||||
import com.frankenergie.smartasset.model.DeliveryPeriod
|
||||
import com.frankenergie.smartasset.model.OrderUpdateRequest
|
||||
import com.frankenergie.smartasset.model.OrderUpdateResponse
|
||||
import org.springframework.stereotype.Service
|
||||
@@ -9,15 +11,24 @@ import java.util.UUID
|
||||
@Service
|
||||
class OrderBookService {
|
||||
|
||||
private val orderBook = OrderBook()
|
||||
|
||||
fun processOrder(request: OrderUpdateRequest): OrderUpdateResponse {
|
||||
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(
|
||||
orderId = orderId,
|
||||
status = "ACCEPTED",
|
||||
timestamp = Instant.now()
|
||||
)
|
||||
val status = when {
|
||||
matchedQuantity == request.quantity -> "FILLED"
|
||||
matchedQuantity > java.math.BigDecimal.ZERO -> "PARTIALLY_FILLED"
|
||||
else -> "ACCEPTED"
|
||||
}
|
||||
|
||||
// TODO: write this to a file
|
||||
|
||||
return OrderUpdateResponse(orderId = orderId, status = status, timestamp = Instant.now())
|
||||
}
|
||||
|
||||
fun getOrderBook(): OrderBook = orderBook
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user