@@ -2,11 +2,14 @@ package scheduler
22
33import (
44 "context"
5+ "fmt"
56 "io"
67 "log"
8+ "math"
79 "net/http"
810 "net/http/httptest"
911 "os"
12+ "strings"
1013 "testing"
1114 "time"
1215)
@@ -179,10 +182,13 @@ func TestGetCurrentPrice_InvalidLocation(t *testing.T) {
179182// TestStoreMarketDataXML_RefreshesMPCAndNextDayPrices validates that uploading
180183// market data XML invalidates cached prices, merges next day data, and re-runs MPC.
181184func TestStoreMarketDataXML_RefreshesMPCAndNextDayPrices (t * testing.T ) {
182- xmlData , err := os .ReadFile ("../test_data/Energy_Prices_202609012200-202609022200.xml" )
183- if err != nil {
184- t .Fatalf ("Failed to read test data file: %v" , err )
185- }
185+ // The document must cover the current wall clock: buildMPCForecast walks
186+ // forward from time.Now() and silently drops every slot that has no price,
187+ // so a fixture pinned to a hard-coded date yields an empty forecast (and
188+ // zero MPC decisions) on any other day. Generate the prices relative to
189+ // today instead of reading a static file.
190+ dayStart := time .Now ().UTC ().Truncate (24 * time .Hour )
191+ xmlData := dayAheadPricesXML (dayStart , 48 * time .Hour )
186192
187193 server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
188194 w .Header ().Set ("Content-Type" , "application/xml" )
@@ -205,9 +211,12 @@ func TestStoreMarketDataXML_RefreshesMPCAndNextDayPrices(t *testing.T) {
205211 scheduler := NewMinerScheduler (config , logger )
206212
207213 ctx := context .Background ()
208- dateKey := "2026-09-02"
209214
210- err = scheduler .StoreMarketDataXML (dateKey , xmlData )
215+ // The XML cache is keyed on the current date in the configured location
216+ // (see entsoe.DownloadPublicationMarketData), so the key must track today.
217+ dateKey := dayStart .Format ("2006-01-02" )
218+
219+ err := scheduler .StoreMarketDataXML (dateKey , xmlData )
211220 if err != nil {
212221 t .Fatalf ("StoreMarketDataXML failed: %v" , err )
213222 }
@@ -223,12 +232,80 @@ func TestStoreMarketDataXML_RefreshesMPCAndNextDayPrices(t *testing.T) {
223232 t .Fatal ("Expected non-nil marketData" )
224233 }
225234
235+ // Sanity-check that the generated document really does price the current
236+ // moment; otherwise a zero-decision result below would be ambiguous.
237+ if _ , ok := marketData .LookupPriceByTime (time .Now ()); ! ok {
238+ t .Fatal ("generated market data does not cover the current time" )
239+ }
240+
226241 decisions := scheduler .GetMPCDecisions ()
227242 if len (decisions ) == 0 {
228243 t .Errorf ("Expected MPC decisions to be generated after uploading XML, got 0" )
229244 }
230245}
231246
247+ // dayAheadPricesXML builds a minimal but valid ENTSO-E A44 day-ahead price
248+ // document covering [start, start+duration) at 15-minute resolution.
249+ //
250+ // Prices follow a simple repeating daily shape so the MPC has something with
251+ // enough spread to act on; the exact values are irrelevant to the assertions.
252+ func dayAheadPricesXML (start time.Time , duration time.Duration ) []byte {
253+ const resolution = 15 * time .Minute
254+ start = start .UTC ()
255+ end := start .Add (duration )
256+ points := int (duration / resolution )
257+
258+ var b strings.Builder
259+ const tsFormat = "2006-01-02T15:04Z"
260+
261+ fmt .Fprintf (& b , `<?xml version="1.0" encoding="UTF-8"?>
262+ <Publication_MarketDocument xmlns="urn:iec62325.351:tc57wg16:451-3:publicationdocument:7:3">
263+ <mRID>test-%s</mRID>
264+ <revisionNumber>1</revisionNumber>
265+ <type>A44</type>
266+ <createdDateTime>%s</createdDateTime>
267+ <period.timeInterval>
268+ <start>%s</start>
269+ <end>%s</end>
270+ </period.timeInterval>
271+ <TimeSeries>
272+ <mRID>1</mRID>
273+ <businessType>A62</businessType>
274+ <in_Domain.mRID codingScheme="A01">10YLV-1001A00074</in_Domain.mRID>
275+ <out_Domain.mRID codingScheme="A01">10YLV-1001A00074</out_Domain.mRID>
276+ <currency_Unit.name>EUR</currency_Unit.name>
277+ <price_Measure_Unit.name>MWH</price_Measure_Unit.name>
278+ <curveType>A03</curveType>
279+ <Period>
280+ <timeInterval>
281+ <start>%s</start>
282+ <end>%s</end>
283+ </timeInterval>
284+ <resolution>PT15M</resolution>
285+ ` , start .Format ("20060102" ), start .Format (time .RFC3339 ),
286+ start .Format (tsFormat ), end .Format (tsFormat ),
287+ start .Format (tsFormat ), end .Format (tsFormat ))
288+
289+ for i := range points {
290+ // Cheap overnight, expensive in the evening peak — a spread the
291+ // optimizer can actually exploit.
292+ hour := float64 ((i * int (resolution / time .Minute )/ 60 )% 24 ) + 0.0
293+ price := 100.0 + 80.0 * math .Sin ((hour - 6 )/ 24 * 2 * math .Pi )
294+ fmt .Fprintf (& b , ` <Point>
295+ <position>%d</position>
296+ <price.amount>%.2f</price.amount>
297+ </Point>
298+ ` , i + 1 , price )
299+ }
300+
301+ b .WriteString (` </Period>
302+ </TimeSeries>
303+ </Publication_MarketDocument>
304+ ` )
305+
306+ return []byte (b .String ())
307+ }
308+
232309// TestMarketDataDownloadHandler validates the GET /api/market-data/download endpoint
233310func TestMarketDataDownloadHandler (t * testing.T ) {
234311 xmlData , err := os .ReadFile ("../test_data/Energy_Prices_202609012200-202609022200.xml" )
0 commit comments