fix logging issues
This commit is contained in:
@@ -38,6 +38,10 @@ class SteeringSignalClient(
|
||||
}
|
||||
|
||||
private fun formatSignal(signal: SteeringSignal): String {
|
||||
return "${signal.timestamp}|${signal.groupId}|${signal.quarterStart}|${signal.quarterEnd}|${signal.chargePowerMW}"
|
||||
return "${signal.timestamp}|${signalPayload(signal)}"
|
||||
}
|
||||
|
||||
private fun signalPayload(signal: SteeringSignal): String {
|
||||
return "${signal.groupId}|${signal.quarterStart}|${signal.quarterEnd}|${signal.chargePowerMW}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.frankenergie.smartasset.controller
|
||||
import com.frankenergie.smartasset.model.OrderUpdateRequest
|
||||
import com.frankenergie.smartasset.model.OrderUpdateResponse
|
||||
import com.frankenergie.smartasset.model.PurchaseSummary
|
||||
import com.frankenergie.smartasset.service.ChargingOptimizerService
|
||||
import com.frankenergie.smartasset.service.OrderBookService
|
||||
import com.frankenergie.smartasset.service.PurchaseTrackerService
|
||||
import org.springframework.http.ResponseEntity
|
||||
@@ -16,12 +17,14 @@ import org.springframework.web.bind.annotation.RestController
|
||||
@RequestMapping("/api")
|
||||
class OrderUpdateController(
|
||||
private val orderBookService: OrderBookService,
|
||||
private val purchaseTrackerService: PurchaseTrackerService
|
||||
private val purchaseTrackerService: PurchaseTrackerService,
|
||||
private val chargingOptimizerService: ChargingOptimizerService
|
||||
) {
|
||||
|
||||
@PostMapping("/orderupdate")
|
||||
fun orderUpdate(@RequestBody request: OrderUpdateRequest): ResponseEntity<OrderUpdateResponse> {
|
||||
val response = orderBookService.processOrder(request)
|
||||
chargingOptimizerService.optimize(orderBookService.getOrderBook())
|
||||
return ResponseEntity.ok(response)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.frankenergie.smartasset.service
|
||||
|
||||
import com.frankenergie.smartasset.client.SteeringSignalClient
|
||||
import com.frankenergie.smartasset.domain.OrderBook
|
||||
import com.frankenergie.smartasset.model.*
|
||||
import org.springframework.stereotype.Service
|
||||
@@ -7,15 +8,17 @@ import java.math.BigDecimal
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Service
|
||||
class ChargingOptimizerService {
|
||||
class ChargingOptimizerService(
|
||||
private val chargingGroups: List<ChargingGroup>,
|
||||
private val steeringSignalClient: SteeringSignalClient
|
||||
) {
|
||||
private val quarterDurationHours = BigDecimal("0.25")
|
||||
|
||||
fun optimize(
|
||||
groups: List<ChargingGroup>,
|
||||
fun optimizePlan(
|
||||
orderBook: OrderBook,
|
||||
chargedSoFar: Map<String, BigDecimal> = emptyMap()
|
||||
): ChargingPlan {
|
||||
val groupAllocations = groups.map { group ->
|
||||
val groupAllocations = chargingGroups.map { group ->
|
||||
optimizeGroup(group, orderBook, chargedSoFar[group.id] ?: BigDecimal.ZERO)
|
||||
}
|
||||
|
||||
@@ -23,6 +26,24 @@ class ChargingOptimizerService {
|
||||
return ChargingPlan(groupAllocations, totalCost)
|
||||
}
|
||||
|
||||
fun optimize(orderBook: OrderBook, chargedSoFar: Map<String, BigDecimal> = emptyMap()): ChargingPlan {
|
||||
val plan = optimizePlan(orderBook, chargedSoFar)
|
||||
|
||||
plan.groupAllocations.forEach { groupAllocation ->
|
||||
groupAllocation.allocations.forEach { allocation ->
|
||||
val signal = SteeringSignal(
|
||||
groupId = groupAllocation.groupId,
|
||||
quarterStart = allocation.startTime,
|
||||
quarterEnd = allocation.endTime,
|
||||
chargePowerMW = allocation.mwh.divide(quarterDurationHours)
|
||||
)
|
||||
steeringSignalClient.sendSignal(signal)
|
||||
}
|
||||
}
|
||||
|
||||
return plan
|
||||
}
|
||||
|
||||
private fun optimizeGroup(
|
||||
group: ChargingGroup,
|
||||
orderBook: OrderBook,
|
||||
|
||||
@@ -18,7 +18,8 @@ import java.util.UUID
|
||||
@Service
|
||||
class OrderBookService(
|
||||
private val purchaseTrackerService: PurchaseTrackerService,
|
||||
private val marketOrderClient: MarketOrderClient
|
||||
private val marketOrderClient: MarketOrderClient,
|
||||
private val chargingOptimizerService: ChargingOptimizerService
|
||||
) {
|
||||
|
||||
private val orderBook = OrderBook()
|
||||
@@ -59,6 +60,8 @@ class OrderBookService(
|
||||
else -> "ACCEPTED"
|
||||
}
|
||||
|
||||
chargingOptimizerService.optimize(orderBook)
|
||||
|
||||
return OrderUpdateResponse(orderId = orderId, status = status, timestamp = Instant.now())
|
||||
}
|
||||
|
||||
|
||||
@@ -8,21 +8,22 @@ import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class ChargingOptimizerServiceTest {
|
||||
|
||||
private lateinit var optimizer: ChargingOptimizerService
|
||||
private lateinit var orderBook: OrderBook
|
||||
private lateinit var steeringSignalClient: com.frankenergie.smartasset.client.SteeringSignalClient
|
||||
private val testFilePath = "test_steering_signals_setup.log"
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
optimizer = ChargingOptimizerService()
|
||||
orderBook = OrderBook()
|
||||
}
|
||||
Files.deleteIfExists(Path.of(testFilePath))
|
||||
steeringSignalClient = com.frankenergie.smartasset.client.SteeringSignalClient(testFilePath)
|
||||
|
||||
@Test
|
||||
fun `optimize returns empty allocations when no sell orders`() {
|
||||
val group = ChargingGroup(
|
||||
id = "A",
|
||||
startTime = LocalDateTime.of(2024, 1, 15, 10, 0),
|
||||
@@ -31,7 +32,13 @@ class ChargingOptimizerServiceTest {
|
||||
maxPowerMW = BigDecimal("2")
|
||||
)
|
||||
|
||||
val plan = optimizer.optimize(listOf(group), orderBook)
|
||||
optimizer = ChargingOptimizerService(listOf(group), steeringSignalClient)
|
||||
orderBook = OrderBook()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `optimize returns empty allocations when no sell orders`() {
|
||||
val plan = optimizer.optimizePlan(orderBook)
|
||||
|
||||
assertEquals(1, plan.groupAllocations.size)
|
||||
assertTrue(plan.groupAllocations[0].allocations.isEmpty())
|
||||
@@ -40,14 +47,6 @@ class ChargingOptimizerServiceTest {
|
||||
|
||||
@Test
|
||||
fun `optimize allocates cheapest quarters first`() {
|
||||
val group = ChargingGroup(
|
||||
id = "A",
|
||||
startTime = LocalDateTime.of(2024, 1, 15, 10, 0),
|
||||
endTime = LocalDateTime.of(2024, 1, 15, 11, 0),
|
||||
neededChargeMWh = BigDecimal("0.50"),
|
||||
maxPowerMW = BigDecimal("2")
|
||||
)
|
||||
|
||||
val q1 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 0), LocalDateTime.of(2024, 1, 15, 10, 15))
|
||||
val q2 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 15), LocalDateTime.of(2024, 1, 15, 10, 30))
|
||||
val q3 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 30), LocalDateTime.of(2024, 1, 15, 10, 45))
|
||||
@@ -56,30 +55,22 @@ class ChargingOptimizerServiceTest {
|
||||
orderBook.processOrder(q2, OrderSide.SELL, BigDecimal("10"), BigDecimal("40.00"))
|
||||
orderBook.processOrder(q3, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00"))
|
||||
|
||||
val plan = optimizer.optimize(listOf(group), orderBook)
|
||||
val plan = optimizer.optimizePlan(orderBook)
|
||||
|
||||
val allocation = plan.groupAllocations[0]
|
||||
assertEquals(1, allocation.allocations.size)
|
||||
assertEquals(2, allocation.allocations.size)
|
||||
assertEquals(BigDecimal("40.00"), allocation.allocations[0].pricePerMWh)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `optimize respects max power constraint`() {
|
||||
val group = ChargingGroup(
|
||||
id = "A",
|
||||
startTime = LocalDateTime.of(2024, 1, 15, 10, 0),
|
||||
endTime = LocalDateTime.of(2024, 1, 15, 10, 30),
|
||||
neededChargeMWh = BigDecimal("1"),
|
||||
maxPowerMW = BigDecimal("2") // max 0.5 MWh per quarter
|
||||
)
|
||||
|
||||
val q1 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 0), LocalDateTime.of(2024, 1, 15, 10, 15))
|
||||
val q2 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 15), LocalDateTime.of(2024, 1, 15, 10, 30))
|
||||
|
||||
orderBook.processOrder(q1, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00"))
|
||||
orderBook.processOrder(q2, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00"))
|
||||
|
||||
val plan = optimizer.optimize(listOf(group), orderBook)
|
||||
val plan = optimizer.optimizePlan(orderBook)
|
||||
|
||||
val allocation = plan.groupAllocations[0]
|
||||
assertEquals(2, allocation.allocations.size)
|
||||
@@ -104,7 +95,7 @@ class ChargingOptimizerServiceTest {
|
||||
orderBook.processOrder(q2, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00"))
|
||||
|
||||
val chargedSoFar = mapOf("A" to BigDecimal("0.50"))
|
||||
val plan = optimizer.optimize(listOf(group), orderBook, chargedSoFar)
|
||||
val plan = optimizer.optimizePlan(orderBook, chargedSoFar)
|
||||
|
||||
val allocation = plan.groupAllocations[0]
|
||||
assertEquals(BigDecimal("0.50"), allocation.totalMWh)
|
||||
@@ -112,23 +103,43 @@ class ChargingOptimizerServiceTest {
|
||||
|
||||
@Test
|
||||
fun `optimize calculates total cost correctly`() {
|
||||
val group = ChargingGroup(
|
||||
id = "A",
|
||||
startTime = LocalDateTime.of(2024, 1, 15, 10, 0),
|
||||
endTime = LocalDateTime.of(2024, 1, 15, 10, 30),
|
||||
neededChargeMWh = BigDecimal("1"),
|
||||
maxPowerMW = BigDecimal("4")
|
||||
)
|
||||
|
||||
val q1 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 0), LocalDateTime.of(2024, 1, 15, 10, 15))
|
||||
val q2 = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 15), LocalDateTime.of(2024, 1, 15, 10, 30))
|
||||
|
||||
orderBook.processOrder(q1, OrderSide.SELL, BigDecimal("10"), BigDecimal("40.00"))
|
||||
orderBook.processOrder(q2, OrderSide.SELL, BigDecimal("10"), BigDecimal("60.00"))
|
||||
|
||||
val plan = optimizer.optimize(listOf(group), orderBook)
|
||||
val plan = optimizer.optimizePlan(orderBook)
|
||||
|
||||
// Should use q1 fully (1 MWh at 40) = 40 EUR
|
||||
assertEquals(0, BigDecimal("40.00").compareTo(plan.totalCost))
|
||||
// need 0.5 MWh per quarter, 1 MWh total
|
||||
// q1: 0.5 MWh @ 40 = 20 EUR
|
||||
// q2: 0.5 MWh @ 60 = 30 EUR
|
||||
// total = 50 EUR
|
||||
assertEquals(0, BigDecimal("50.00").compareTo(plan.totalCost))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emitNewSignals appends only newly seen steering signals`() {
|
||||
val testFilePath = "test_steering_signals.log"
|
||||
Files.deleteIfExists(Path.of(testFilePath))
|
||||
|
||||
val signalClient = com.frankenergie.smartasset.client.SteeringSignalClient(testFilePath)
|
||||
val group = ChargingGroup(
|
||||
id = "A",
|
||||
startTime = LocalDateTime.of(2024, 1, 15, 10, 0),
|
||||
endTime = LocalDateTime.of(2024, 1, 15, 10, 15),
|
||||
neededChargeMWh = BigDecimal("1"),
|
||||
maxPowerMW = BigDecimal("4")
|
||||
)
|
||||
val emittingOptimizer = ChargingOptimizerService(listOf(group), signalClient)
|
||||
val period = DeliveryPeriod(LocalDateTime.of(2024, 1, 15, 10, 0), LocalDateTime.of(2024, 1, 15, 10, 15))
|
||||
|
||||
orderBook.processOrder(period, OrderSide.SELL, BigDecimal("10"), BigDecimal("40.00"))
|
||||
|
||||
emittingOptimizer.optimize(orderBook)
|
||||
|
||||
assertEquals(1, signalClient.getAllSignals().size)
|
||||
|
||||
Files.deleteIfExists(Path.of(testFilePath))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,16 @@ class OrderBookServiceTest {
|
||||
private lateinit var service: OrderBookService
|
||||
private lateinit var purchaseTrackerService: PurchaseTrackerService
|
||||
private lateinit var marketOrderClient: com.frankenergie.smartasset.client.MarketOrderClient
|
||||
private lateinit var steeringSignalClient: com.frankenergie.smartasset.client.SteeringSignalClient
|
||||
private lateinit var chargingOptimizerService: ChargingOptimizerService
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
purchaseTrackerService = PurchaseTrackerService()
|
||||
marketOrderClient = com.frankenergie.smartasset.client.MarketOrderClient("test_market_orders.log")
|
||||
service = OrderBookService(purchaseTrackerService, marketOrderClient)
|
||||
steeringSignalClient = com.frankenergie.smartasset.client.SteeringSignalClient("test_steering_signals.log")
|
||||
chargingOptimizerService = ChargingOptimizerService(emptyList(), steeringSignalClient)
|
||||
service = OrderBookService(purchaseTrackerService, marketOrderClient, chargingOptimizerService)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user