From 3e54f8f84d3b0774cd02c05e89ecf7871e4415c4 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 2 Jul 2026 15:46:57 +0200 Subject: [PATCH] fix logging issues --- e2e.sh | 46 +++++----- .../smartasset/client/SteeringSignalClient.kt | 6 +- .../controller/OrderUpdateController.kt | 5 +- .../service/ChargingOptimizedService.kt | 29 ++++++- .../smartasset/service/OrderBookService.kt | 5 +- .../service/ChargingOptimizerServiceTest.kt | 85 +++++++++++-------- .../service/OrderBookServiceTest.kt | 6 +- 7 files changed, 115 insertions(+), 67 deletions(-) diff --git a/e2e.sh b/e2e.sh index ded9646..df764f5 100755 --- a/e2e.sh +++ b/e2e.sh @@ -1,44 +1,46 @@ #!/bin/bash BASE_URL="http://localhost:8080/api" +TODAY=$(date +%Y-%m-%d) echo "=== E2E Pseudo Test ===" +echo "Using date: $TODAY" echo "" echo "POST /api/orderupdate - Place SELL order" curl -s -X POST "$BASE_URL/orderupdate" \ -H "Content-Type: application/json" \ - -d '{ - "delivery_start_time": "2024-01-15T10:00:00", - "delivery_end_time": "2024-01-15T10:15:00", - "order_side": "SELL", - "quantity": 10, - "price": 50.00 - }' | jq . + -d "{ + \"delivery_start_time\": \"${TODAY}T10:00:00\", + \"delivery_end_time\": \"${TODAY}T10:15:00\", + \"order_side\": \"SELL\", + \"quantity\": 10, + \"price\": 50.00 + }" | jq . echo "" echo "POST /api/orderupdate - Place matching BUY order" curl -s -X POST "$BASE_URL/orderupdate" \ -H "Content-Type: application/json" \ - -d '{ - "delivery_start_time": "2024-01-15T10:00:00", - "delivery_end_time": "2024-01-15T10:15:00", - "order_side": "BUY", - "quantity": 5, - "price": 50.00 - }' | jq . + -d "{ + \"delivery_start_time\": \"${TODAY}T10:00:00\", + \"delivery_end_time\": \"${TODAY}T10:15:00\", + \"order_side\": \"BUY\", + \"quantity\": 5, + \"price\": 50.00 + }" | jq . echo "" -echo "POST /api/orderupdate - Place another BUY order" +echo "POST /api/orderupdate - Place another SELL order" curl -s -X POST "$BASE_URL/orderupdate" \ -H "Content-Type: application/json" \ - -d '{ - "delivery_start_time": "2024-01-15T10:15:00", - "delivery_end_time": "2024-01-15T10:30:00", - "order_side": "BUY", - "quantity": 20, - "price": 45.00 - }' | jq . + -d "{ + \"delivery_start_time\": \"${TODAY}T10:15:00\", + \"delivery_end_time\": \"${TODAY}T10:30:00\", + \"order_side\": \"SELL\", + \"quantity\": 20, + \"price\": 45.00 + }" | jq . echo "" echo "GET /api/market/overview - Get market overview" diff --git a/src/main/kotlin/com/frankenergie/smartasset/client/SteeringSignalClient.kt b/src/main/kotlin/com/frankenergie/smartasset/client/SteeringSignalClient.kt index aa7a621..9c6b496 100644 --- a/src/main/kotlin/com/frankenergie/smartasset/client/SteeringSignalClient.kt +++ b/src/main/kotlin/com/frankenergie/smartasset/client/SteeringSignalClient.kt @@ -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}" } } diff --git a/src/main/kotlin/com/frankenergie/smartasset/controller/OrderUpdateController.kt b/src/main/kotlin/com/frankenergie/smartasset/controller/OrderUpdateController.kt index 9f9ca72..1914e55 100644 --- a/src/main/kotlin/com/frankenergie/smartasset/controller/OrderUpdateController.kt +++ b/src/main/kotlin/com/frankenergie/smartasset/controller/OrderUpdateController.kt @@ -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 { val response = orderBookService.processOrder(request) + chargingOptimizerService.optimize(orderBookService.getOrderBook()) return ResponseEntity.ok(response) } diff --git a/src/main/kotlin/com/frankenergie/smartasset/service/ChargingOptimizedService.kt b/src/main/kotlin/com/frankenergie/smartasset/service/ChargingOptimizedService.kt index 88be3c5..0cb144c 100644 --- a/src/main/kotlin/com/frankenergie/smartasset/service/ChargingOptimizedService.kt +++ b/src/main/kotlin/com/frankenergie/smartasset/service/ChargingOptimizedService.kt @@ -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, + private val steeringSignalClient: SteeringSignalClient +) { private val quarterDurationHours = BigDecimal("0.25") - fun optimize( - groups: List, + fun optimizePlan( orderBook: OrderBook, chargedSoFar: Map = 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 = 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, diff --git a/src/main/kotlin/com/frankenergie/smartasset/service/OrderBookService.kt b/src/main/kotlin/com/frankenergie/smartasset/service/OrderBookService.kt index b10e3a8..a6e86dc 100644 --- a/src/main/kotlin/com/frankenergie/smartasset/service/OrderBookService.kt +++ b/src/main/kotlin/com/frankenergie/smartasset/service/OrderBookService.kt @@ -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()) } diff --git a/src/test/kotlin/com/frankenergie/smartasset/service/ChargingOptimizerServiceTest.kt b/src/test/kotlin/com/frankenergie/smartasset/service/ChargingOptimizerServiceTest.kt index fbf7433..02e67ca 100644 --- a/src/test/kotlin/com/frankenergie/smartasset/service/ChargingOptimizerServiceTest.kt +++ b/src/test/kotlin/com/frankenergie/smartasset/service/ChargingOptimizerServiceTest.kt @@ -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)) } } diff --git a/src/test/kotlin/com/frankenergie/smartasset/service/OrderBookServiceTest.kt b/src/test/kotlin/com/frankenergie/smartasset/service/OrderBookServiceTest.kt index 258eeec..a185104 100644 --- a/src/test/kotlin/com/frankenergie/smartasset/service/OrderBookServiceTest.kt +++ b/src/test/kotlin/com/frankenergie/smartasset/service/OrderBookServiceTest.kt @@ -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