Skip to content

Commit

Permalink
Merge pull request #43 from daithihearn/period-calculation-improvements
Browse files Browse the repository at this point in the history
feat: improving period calculation logic
  • Loading branch information
daithihearn authored Oct 18, 2023
2 parents 24743ab + dad3e4a commit 2e4627b
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 72 deletions.
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.14.0
1.14.1
3 changes: 3 additions & 0 deletions src/main/kotlin/ie/daithi/electricityprices/Constants.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const val RATING_VARIANCE = 0.02

const val VARIANCE_DIVISOR = 2
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ class AlexSkillService(private val priceService: PriceService, private val messa
responses.add(getTodayRating(now, pricesToday, thirtyDayAverage, locale))

// Get next good 3-hour period
val nextCheapPeriod = getNextCheapPeriod(now, pricesToday, locale)
val nextCheapPeriod = getNextCheapPeriod(now, pricesToday, thirtyDayAverage, locale)
responses.add(nextCheapPeriod)

// Get next bad 3-hour period
val nextExpensivePeriod = getNextExpensivePeriod(now, pricesToday, locale)
val nextExpensivePeriod = getNextExpensivePeriod(now, pricesToday, thirtyDayAverage, locale)
responses.add(nextExpensivePeriod)

// Get Tomorrow's price data
Expand Down Expand Up @@ -92,11 +92,11 @@ class AlexSkillService(private val priceService: PriceService, private val messa
val ratingText = getTomorrowRatingText(rating, roundedDailyAverage, locale)

// Get the cheapest periods
val cheapPeriods = getCheapPeriods(pricesTomorrow)
val cheapPeriods = getCheapPeriods(pricesTomorrow, thirtyDayAverage)
val cheapPeriodMessage = getCheapPeriodMessage(cheapPeriods, locale)

// Get the expensive periods
val expensePeriods = getExpensivePeriods(pricesTomorrow)
val expensePeriods = getExpensivePeriods(pricesTomorrow, thirtyDayAverage)
val expensivePeriodMessage = getExpensivePeriodMessage(expensePeriods, locale)

return Pair(
Expand Down Expand Up @@ -217,9 +217,10 @@ class AlexSkillService(private val priceService: PriceService, private val messa
fun getNextCheapPeriod(
dateTime: LocalDateTime = LocalDateTime.now(),
pricesToday: List<Price> = priceService.getPrices(dateTime.toLocalDate()),
thirtyDayAverage: Double = priceService.getThirtyDayAverage(),
locale: Locale
): String {
val cheapPeriods = getCheapPeriods(pricesToday)
val cheapPeriods = getCheapPeriods(pricesToday, thirtyDayAverage)

val nextPeriod = getNextPeriod(cheapPeriods, dateTime) ?: return messageSource.getMessage(
"alexa.next.cheap.period.no_data",
Expand Down Expand Up @@ -255,10 +256,11 @@ class AlexSkillService(private val priceService: PriceService, private val messa
fun getNextExpensivePeriod(
dateTime: LocalDateTime = LocalDateTime.now(),
pricesToday: List<Price> = priceService.getPrices(dateTime.toLocalDate()),
thirtyDayAverage: Double = priceService.getThirtyDayAverage(),
locale: Locale
): String {
val expensivePeriods =
getExpensivePeriods(pricesToday)
getExpensivePeriods(pricesToday, thirtyDayAverage)

if (expensivePeriods.isEmpty()) throw Exception("No expensive periods found")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ class PriceService(
val date = dateStr?.let { LocalDate.parse(it, dateFormatter) } ?: LocalDate.now()
val prices = getPrices(start = dateStr, end = dateStr)
if (prices.isEmpty()) return null
val cheapestPeriods = getCheapPeriods(prices)
val expensivePeriods = getExpensivePeriods(prices)
val thirtyDayAverage: Double = getThirtyDayAverage(date.atStartOfDay())
val cheapestPeriods = getCheapPeriods(prices, thirtyDayAverage)
val expensivePeriods = getExpensivePeriods(prices, thirtyDayAverage)

val dailyAverage = prices.map { it.price }.average()
val thirtyDayAverage: Double = getThirtyDayAverage(date.atStartOfDay())

return DailyPriceInfo(
dayRating = calculateRating(dailyAverage, thirtyDayAverage),
Expand All @@ -60,7 +60,7 @@ class PriceService(

/*
Calls to the API and update the latest prices
We use the REE API as they have the most up to date data
We use the REE API as they have the most up-to-date data
*/
fun updatePriceData(date: LocalDate) {
logger.info("Updating price data from REE for $date")
Expand Down
64 changes: 45 additions & 19 deletions src/main/kotlin/ie/daithi/electricityprices/utils/PriceUtils.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package ie.daithi.electricityprices.utils

import RATING_VARIANCE
import VARIANCE_DIVISOR
import ie.daithi.electricityprices.model.DayRating
import ie.daithi.electricityprices.model.Price
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt

const val RATING_VARIANCE = 0.02

// The maximum variance value
const val MAX_VARIANCE = 0.03

const val VARIANCE_DIVISOR = 2

/**
* Joins prices that are adjacent to each other.
*/
Expand All @@ -30,29 +27,56 @@ fun joinPrices(cheapPrices: List<Price>): List<List<Price>> {
return result
}

/**
* Calculate the combination between the thirty-day average and the daily average.
* A weighted average is used with the daily average being weighted twice as much as the thirty-day average.
*/
private fun calculateCombinedAverage(dailyAverage: Double, thirtyDayAverage: Double): Double {
return (dailyAverage * 2 + thirtyDayAverage) / 3
}

private fun calculateMinVariance(prices: List<Price>): Double {
val cheapestPrice = prices.minByOrNull { it.price }?.price ?: return 0.0
val expensivePrice = prices.maxByOrNull { it.price }?.price ?: return 0.0
return (expensivePrice - cheapestPrice) / 6
}

private fun calculateMaxVariance(prices: List<Price>): Double {
val cheapestPrice = prices.minByOrNull { it.price }?.price ?: return 0.0
val expensivePrice = prices.maxByOrNull { it.price }?.price ?: return 0.0
return (expensivePrice - cheapestPrice) / 3
}

/**
* Calculate the variance for the cheap periods.
* This is calculated as:
* MIN((dailyAverage - cheapestPrice) / VARIANCE_DIVISOR, MAX_VARIANCE)
*/
fun calculateCheapVariance(prices: List<Price>): Double {
fun calculateCheapVariance(prices: List<Price>, thirtyDayAverage: Double): Double {
val dailyAverage = calculateAverage(prices)
val cheapestPrice = prices.minByOrNull { it.price }?.price ?: return MAX_VARIANCE
val variance = (dailyAverage - cheapestPrice) / VARIANCE_DIVISOR
return if (variance > MAX_VARIANCE) MAX_VARIANCE else variance
val combinedAverage = calculateCombinedAverage(dailyAverage, thirtyDayAverage)
val minVariance = calculateMinVariance(prices)
val maxVariance = calculateMaxVariance(prices)
val cheapestPrice = prices.minByOrNull { it.price }?.price ?: return minVariance
val variance = (combinedAverage - cheapestPrice) / VARIANCE_DIVISOR

return max(min(variance, maxVariance), minVariance)
}

/**
* Calculate the variance for the expensive periods.
* This is calculated as:
* MIN((expensivePrice - dailyAverage) / VARIANCE_DIVISOR, MAX_VARIANCE)
*/
fun calculateExpensiveVariance(prices: List<Price>): Double {
fun calculateExpensiveVariance(prices: List<Price>, thirtyDayAverage: Double): Double {
val dailyAverage = calculateAverage(prices)
val expensivePrice = prices.maxByOrNull { it.price }?.price ?: return MAX_VARIANCE
val variance = (expensivePrice - dailyAverage) / VARIANCE_DIVISOR
return if (variance > MAX_VARIANCE) MAX_VARIANCE else variance
val combinedAverage = calculateCombinedAverage(dailyAverage, thirtyDayAverage)
val minVariance = calculateMinVariance(prices)
val maxVariance = calculateMaxVariance(prices)
val expensivePrice = prices.maxByOrNull { it.price }?.price ?: return minVariance
val variance = (expensivePrice - combinedAverage) / VARIANCE_DIVISOR

return max(min(variance, maxVariance), minVariance)
}

/**
Expand All @@ -75,10 +99,11 @@ fun isWithinExpensivePriceVariance(
* Returns the cheap periods
*/
fun getCheapPeriods(
prices: List<Price>
prices: List<Price>,
thirtyDayAverage: Double
): List<List<Price>> {

val variance = calculateCheapVariance(prices)
val variance = calculateCheapVariance(prices, thirtyDayAverage)
val cheapestPrice = prices.minByOrNull { it.price }?.price ?: return emptyList()

val cheapPrices = prices.filter { price ->
Expand All @@ -96,10 +121,11 @@ fun getCheapPeriods(
* Returns the expensive periods
*/
fun getExpensivePeriods(
prices: List<Price>
prices: List<Price>,
thirtyDayAverage: Double
): List<List<Price>> {

val variance = calculateExpensiveVariance(prices)
val variance = calculateExpensiveVariance(prices, thirtyDayAverage)
val expensivePrice = prices.maxByOrNull { it.price }?.price ?: return emptyList()

val expensivePrices = prices.filter { price ->
Expand Down
Loading

0 comments on commit 2e4627b

Please sign in to comment.