@@ -9,6 +9,11 @@ use super::models::{HourPrice, NpsEntry, NpsResponse, SpotResponse};
99/// consumer price like the price apps do.
1010const VAT : f64 = 1.255 ;
1111
12+ /// Minimum hours before tomorrow counts as "published" — until the day-ahead
13+ /// auction lands (~14:00) Elering returns only an hour or two of tomorrow, and a
14+ /// lone bar isn't worth a chart (the frontend shows a placeholder instead).
15+ const MIN_TOMORROW_HOURS : usize = 20 ;
16+
1217/// Fetch today + tomorrow Finnish prices straight from the TSO-grade open source
1318/// (Elering LIVE, Nord Pool day-ahead). API: <https://dashboard.elering.ee/api>
1419pub async fn fetch (
@@ -42,12 +47,14 @@ pub async fn fetch(
4247 . error_for_status ( ) ?
4348 . json ( )
4449 . await ?;
45- Ok ( aggregate ( resp. data . fi ) )
50+ Ok ( aggregate ( resp. data . fi , now . date_naive ( ) ) )
4651}
4752
48- /// Average the 15-min entries into hourly bars (in Helsinki local time) and split
49- /// into today/tomorrow; convert EUR/MWh ex-VAT → c/kWh incl. VAT.
50- fn aggregate ( entries : Vec < NpsEntry > ) -> SpotResponse {
53+ /// Average the 15-min entries into hourly bars (in Helsinki local time), keyed to
54+ /// the real local dates so today/tomorrow don't drift; convert EUR/MWh ex-VAT →
55+ /// c/kWh incl. VAT. Tomorrow is dropped until it's a near-complete published day,
56+ /// so a lone post-midnight bar doesn't spill into the tomorrow chart.
57+ fn aggregate ( entries : Vec < NpsEntry > , today : NaiveDate ) -> SpotResponse {
5158 // (local date, hour) -> (price sum, count, hour-start datetime)
5259 let mut buckets: BTreeMap < ( NaiveDate , u32 ) , ( f64 , u32 , DateTime < Tz > ) > = BTreeMap :: new ( ) ;
5360 for e in & entries {
@@ -67,35 +74,33 @@ fn aggregate(entries: Vec<NpsEntry>) -> SpotResponse {
6774 slot. 1 += 1 ;
6875 }
6976
70- let mut dates: Vec < NaiveDate > = buckets. keys ( ) . map ( |( d, _) | * d) . collect ( ) ;
71- dates. dedup ( ) ; // BTreeMap keys are sorted, so dups are adjacent
72-
73- let to_hours = |target : Option < NaiveDate > | -> Vec < HourPrice > {
74- let Some ( target) = target else {
75- return Vec :: new ( ) ;
76- } ;
77+ let hours_for = |date : NaiveDate | -> Vec < HourPrice > {
7778 buckets
7879 . iter ( )
79- . filter ( |( ( d, _) , _) | * d == target )
80+ . filter ( |( ( d, _) , _) | * d == date )
8081 . map ( |( _, ( sum, count, hour_start) ) | HourPrice {
8182 hour : hour_start. to_rfc3339 ( ) ,
8283 price : ( sum / * count as f64 ) / 10.0 * VAT , // EUR/MWh ex-VAT -> c/kWh incl VAT
8384 } )
8485 . collect ( )
8586 } ;
8687
87- let today = to_hours ( dates. first ( ) . copied ( ) ) ;
88- let tomorrow = to_hours ( dates. get ( 1 ) . copied ( ) ) ;
89- let today_average = if today. is_empty ( ) {
88+ let today_v = hours_for ( today) ;
89+ let tomorrow_v = today
90+ . succ_opt ( )
91+ . map ( hours_for)
92+ . filter ( |h| h. len ( ) >= MIN_TOMORROW_HOURS )
93+ . unwrap_or_default ( ) ;
94+ let today_average = if today_v. is_empty ( ) {
9095 0.0
9196 } else {
92- today . iter ( ) . map ( |h| h. price ) . sum :: < f64 > ( ) / today . len ( ) as f64
97+ today_v . iter ( ) . map ( |h| h. price ) . sum :: < f64 > ( ) / today_v . len ( ) as f64
9398 } ;
9499
95100 SpotResponse {
96101 unit : "c/kWh" . into ( ) ,
97- today,
98- tomorrow,
102+ today : today_v ,
103+ tomorrow : tomorrow_v ,
99104 today_average,
100105 }
101106}
0 commit comments