fix logging issues

This commit is contained in:
2026-07-02 15:46:57 +02:00
parent 0a7a941da8
commit 3e54f8f84d
7 changed files with 115 additions and 67 deletions

46
e2e.sh
View File

@@ -1,44 +1,46 @@
#!/bin/bash #!/bin/bash
BASE_URL="http://localhost:8080/api" BASE_URL="http://localhost:8080/api"
TODAY=$(date +%Y-%m-%d)
echo "=== E2E Pseudo Test ===" echo "=== E2E Pseudo Test ==="
echo "Using date: $TODAY"
echo "" echo ""
echo "POST /api/orderupdate - Place SELL order" echo "POST /api/orderupdate - Place SELL order"
curl -s -X POST "$BASE_URL/orderupdate" \ curl -s -X POST "$BASE_URL/orderupdate" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d "{
"delivery_start_time": "2024-01-15T10:00:00", \"delivery_start_time\": \"${TODAY}T10:00:00\",
"delivery_end_time": "2024-01-15T10:15:00", \"delivery_end_time\": \"${TODAY}T10:15:00\",
"order_side": "SELL", \"order_side\": \"SELL\",
"quantity": 10, \"quantity\": 10,
"price": 50.00 \"price\": 50.00
}' | jq . }" | jq .
echo "" echo ""
echo "POST /api/orderupdate - Place matching BUY order" echo "POST /api/orderupdate - Place matching BUY order"
curl -s -X POST "$BASE_URL/orderupdate" \ curl -s -X POST "$BASE_URL/orderupdate" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d "{
"delivery_start_time": "2024-01-15T10:00:00", \"delivery_start_time\": \"${TODAY}T10:00:00\",
"delivery_end_time": "2024-01-15T10:15:00", \"delivery_end_time\": \"${TODAY}T10:15:00\",
"order_side": "BUY", \"order_side\": \"BUY\",
"quantity": 5, \"quantity\": 5,
"price": 50.00 \"price\": 50.00
}' | jq . }" | jq .
echo "" echo ""
echo "POST /api/orderupdate - Place another BUY order" echo "POST /api/orderupdate - Place another SELL order"
curl -s -X POST "$BASE_URL/orderupdate" \ curl -s -X POST "$BASE_URL/orderupdate" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d "{
"delivery_start_time": "2024-01-15T10:15:00", \"delivery_start_time\": \"${TODAY}T10:15:00\",
"delivery_end_time": "2024-01-15T10:30:00", \"delivery_end_time\": \"${TODAY}T10:30:00\",
"order_side": "BUY", \"order_side\": \"SELL\",
"quantity": 20, \"quantity\": 20,
"price": 45.00 \"price\": 45.00
}' | jq . }" | jq .
echo "" echo ""
echo "GET /api/market/overview - Get market overview" echo "GET /api/market/overview - Get market overview"

View File

@@ -38,6 +38,10 @@ class SteeringSignalClient(
} }
private fun formatSignal(signal: SteeringSignal): String { 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}"
} }
} }

View File

@@ -3,6 +3,7 @@ package com.frankenergie.smartasset.controller
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 com.frankenergie.smartasset.model.PurchaseSummary import com.frankenergie.smartasset.model.PurchaseSummary
import com.frankenergie.smartasset.service.ChargingOptimizerService
import com.frankenergie.smartasset.service.OrderBookService import com.frankenergie.smartasset.service.OrderBookService
import com.frankenergie.smartasset.service.PurchaseTrackerService import com.frankenergie.smartasset.service.PurchaseTrackerService
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
@@ -16,12 +17,14 @@ import org.springframework.web.bind.annotation.RestController
@RequestMapping("/api") @RequestMapping("/api")
class OrderUpdateController( class OrderUpdateController(
private val orderBookService: OrderBookService, private val orderBookService: OrderBookService,
private val purchaseTrackerService: PurchaseTrackerService private val purchaseTrackerService: PurchaseTrackerService,
private val chargingOptimizerService: ChargingOptimizerService
) { ) {
@PostMapping("/orderupdate") @PostMapping("/orderupdate")
fun orderUpdate(@RequestBody request: OrderUpdateRequest): ResponseEntity<OrderUpdateResponse> { fun orderUpdate(@RequestBody request: OrderUpdateRequest): ResponseEntity<OrderUpdateResponse> {
val response = orderBookService.processOrder(request) val response = orderBookService.processOrder(request)
chargingOptimizerService.optimize(orderBookService.getOrderBook())
return ResponseEntity.ok(response) return ResponseEntity.ok(response)
} }

View File

@@ -1,5 +1,6 @@
package com.frankenergie.smartasset.service package com.frankenergie.smartasset.service
import com.frankenergie.smartasset.client.SteeringSignalClient
import com.frankenergie.smartasset.domain.OrderBook import com.frankenergie.smartasset.domain.OrderBook
import com.frankenergie.smartasset.model.* import com.frankenergie.smartasset.model.*
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -7,15 +8,17 @@ import java.math.BigDecimal
import java.time.LocalDateTime import java.time.LocalDateTime
@Service @Service
class ChargingOptimizerService { class ChargingOptimizerService(
private val chargingGroups: List<ChargingGroup>,
private val steeringSignalClient: SteeringSignalClient
) {
private val quarterDurationHours = BigDecimal("0.25") private val quarterDurationHours = BigDecimal("0.25")
fun optimize( fun optimizePlan(
groups: List<ChargingGroup>,
orderBook: OrderBook, orderBook: OrderBook,
chargedSoFar: Map<String, BigDecimal> = emptyMap() chargedSoFar: Map<String, BigDecimal> = emptyMap()
): ChargingPlan { ): ChargingPlan {
val groupAllocations = groups.map { group -> val groupAllocations = chargingGroups.map { group ->
optimizeGroup(group, orderBook, chargedSoFar[group.id] ?: BigDecimal.ZERO) optimizeGroup(group, orderBook, chargedSoFar[group.id] ?: BigDecimal.ZERO)
} }
@@ -23,6 +26,24 @@ class ChargingOptimizerService {
return ChargingPlan(groupAllocations, totalCost) 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( private fun optimizeGroup(
group: ChargingGroup, group: ChargingGroup,
orderBook: OrderBook, orderBook: OrderBook,

View File

@@ -18,7 +18,8 @@ import java.util.UUID
@Service @Service
class OrderBookService( class OrderBookService(
private val purchaseTrackerService: PurchaseTrackerService, private val purchaseTrackerService: PurchaseTrackerService,
private val marketOrderClient: MarketOrderClient private val marketOrderClient: MarketOrderClient,
private val chargingOptimizerService: ChargingOptimizerService
) { ) {
private val orderBook = OrderBook() private val orderBook = OrderBook()
@@ -59,6 +60,8 @@ class OrderBookService(
else -> "ACCEPTED" else -> "ACCEPTED"
} }
chargingOptimizerService.optimize(orderBook)
return OrderUpdateResponse(orderId = orderId, status = status, timestamp = Instant.now()) return OrderUpdateResponse(orderId = orderId, status = status, timestamp = Instant.now())
} }

View File

@@ -8,21 +8,22 @@ import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.math.BigDecimal import java.math.BigDecimal
import java.nio.file.Files
import java.nio.file.Path
import java.time.LocalDateTime import java.time.LocalDateTime
class ChargingOptimizerServiceTest { class ChargingOptimizerServiceTest {
private lateinit var optimizer: ChargingOptimizerService private lateinit var optimizer: ChargingOptimizerService
private lateinit var orderBook: OrderBook private lateinit var orderBook: OrderBook
private lateinit var steeringSignalClient: com.frankenergie.smartasset.client.SteeringSignalClient
private val testFilePath = "test_steering_signals_setup.log"
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
optimizer = ChargingOptimizerService() Files.deleteIfExists(Path.of(testFilePath))
orderBook = OrderBook() steeringSignalClient = com.frankenergie.smartasset.client.SteeringSignalClient(testFilePath)
}
@Test
fun `optimize returns empty allocations when no sell orders`() {
val group = ChargingGroup( val group = ChargingGroup(
id = "A", id = "A",
startTime = LocalDateTime.of(2024, 1, 15, 10, 0), startTime = LocalDateTime.of(2024, 1, 15, 10, 0),
@@ -31,7 +32,13 @@ class ChargingOptimizerServiceTest {
maxPowerMW = BigDecimal("2") 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) assertEquals(1, plan.groupAllocations.size)
assertTrue(plan.groupAllocations[0].allocations.isEmpty()) assertTrue(plan.groupAllocations[0].allocations.isEmpty())
@@ -40,14 +47,6 @@ class ChargingOptimizerServiceTest {
@Test @Test
fun `optimize allocates cheapest quarters first`() { 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 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 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)) 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(q2, OrderSide.SELL, BigDecimal("10"), BigDecimal("40.00"))
orderBook.processOrder(q3, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.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] val allocation = plan.groupAllocations[0]
assertEquals(1, allocation.allocations.size) assertEquals(2, allocation.allocations.size)
assertEquals(BigDecimal("40.00"), allocation.allocations[0].pricePerMWh) assertEquals(BigDecimal("40.00"), allocation.allocations[0].pricePerMWh)
} }
@Test @Test
fun `optimize respects max power constraint`() { 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 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 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(q1, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00"))
orderBook.processOrder(q2, 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] val allocation = plan.groupAllocations[0]
assertEquals(2, allocation.allocations.size) assertEquals(2, allocation.allocations.size)
@@ -104,7 +95,7 @@ class ChargingOptimizerServiceTest {
orderBook.processOrder(q2, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00")) orderBook.processOrder(q2, OrderSide.SELL, BigDecimal("10"), BigDecimal("50.00"))
val chargedSoFar = mapOf("A" to BigDecimal("0.50")) 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] val allocation = plan.groupAllocations[0]
assertEquals(BigDecimal("0.50"), allocation.totalMWh) assertEquals(BigDecimal("0.50"), allocation.totalMWh)
@@ -112,23 +103,43 @@ class ChargingOptimizerServiceTest {
@Test @Test
fun `optimize calculates total cost correctly`() { 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 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 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(q1, OrderSide.SELL, BigDecimal("10"), BigDecimal("40.00"))
orderBook.processOrder(q2, OrderSide.SELL, BigDecimal("10"), BigDecimal("60.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 // need 0.5 MWh per quarter, 1 MWh total
assertEquals(0, BigDecimal("40.00").compareTo(plan.totalCost)) // 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))
} }
} }

View File

@@ -13,12 +13,16 @@ class OrderBookServiceTest {
private lateinit var service: OrderBookService private lateinit var service: OrderBookService
private lateinit var purchaseTrackerService: PurchaseTrackerService private lateinit var purchaseTrackerService: PurchaseTrackerService
private lateinit var marketOrderClient: com.frankenergie.smartasset.client.MarketOrderClient private lateinit var marketOrderClient: com.frankenergie.smartasset.client.MarketOrderClient
private lateinit var steeringSignalClient: com.frankenergie.smartasset.client.SteeringSignalClient
private lateinit var chargingOptimizerService: ChargingOptimizerService
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
purchaseTrackerService = PurchaseTrackerService() purchaseTrackerService = PurchaseTrackerService()
marketOrderClient = com.frankenergie.smartasset.client.MarketOrderClient("test_market_orders.log") 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 @Test