[Django]-Calculate average exchange rate for time period

3👍

1. months_between():

create function months_of(interval)
 returns int strict immutable language sql as $$
  select extract(years from $1)::int * 12 + extract(month from $1)::int
$$;

create function months_between(date, date)
 returns int strict immutable language sql as $$
   select months_of(age($1, $2))
$$;

2. average_weight():

create function average_weight(numeric, date, date, date, date)
 returns numeric(9,2) strict immutable language sql as $$
   select abs(months_between(GREATEST($2, $4), LEAST($3, $5))/months_between($4, $5))*$1
$$;

3. AverageWeight:

from django.db.models.aggregates import Func
from django.db.models.fields import FloatField

class AverageWeight(Func):
    function = 'average_weight'

    def __init__(self, *expressions):
        super(AverageWeight, self).__init__(*expressions, output_field=FloatField())

In your view:

ExchangeRate.objects.all().filter(
        (
            Q(start_date__lt=start_date) & 
            Q(end_date__gt=start_date)
        ) | (
            Q(start_date__gte=start_date) & 
            Q(start_date__lt=end_date) & 
            Q(end_date__gt=start_date) 
        )
).annotate(
    currency_from_name = 'currency_from__name', 
    currency_to_name = 'currency_to__name',
    weight_exchange = AverageWeight(
        F('exchange_rate'),
        start_date,
        end_date,
        F('start_date'),
        F('end_date'),
    )
).values(  # GROUP BY
    'currency_from_name',
    'currency_to_name'
).aggregate(
    F('currency_from_name'), 
    F('currency_to_name'), 
    Avg('weight_exchange')
)
👤WBAR

2👍

The problem with your application is the way you choose to store the exchange rates. So, to answer your question: yes, you are over complicating this.

“The Math” is telling you that the average exchange rate is 4.5 because

(3 + 6) /2 == 4.5 

Regardless of what start date or end date you choose, the system will get you the same value.

In order to address the root cause, let’s try a different approach.
(for simplicity sake I’ll leave the foreign keys and other details not relevant to getting the average inside the specific date range out, you can add them back later)

with this model:

class ExchangeRate(models.Model):
    currency1 = models.CharField(max_length=3)
    currency2 = models.CharField(max_length=3)
    start_date = models.DateField()
    exchange_rate = models.DecimalField(max_digits=12, decimal_places=4)

and this data:

INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-03-01', 3);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-04-01', 3);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-05-01', 3);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-06-01', 3);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-07-01', 3);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-08-01', 6);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-09-01', 6);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-10-01', 6);
INSERT INTO exchange_rate_exchangerate(currency1, currency2, start_date, exchange_rate) VALUES ('PLN', 'USD', '2014-11-01', 6);

we can perform this query:

from django.db.models import Avg
from datetime import date

first_date = date(2014, 6, 1)
last_date = date(2014, 9, 1)
er.models.ExchangeRate.objects.filter(
    start_date__gte = first_date,
    start_date__lt = last_date

).aggregate(Avg('exchange_rate'))

To get this output:

{'exchange_rate__avg': 4.0}

0👍

You should think of this as a weighted average so what you want to do is to calculate the weight of each line and then sum it all together.

I don’t know enough Django to help you there but in SQL this would be (I am not able to test this now but I think it gives the right idea):

SELECT SUM((LEAST(end_date, @end_date) - GREATEST(start_date, @start_date)) * exchange_rate) / (@end_date - @start_date) AS weighted_avg
FROM 
  ExchangeRate
WHERE
  (start_date, end_date) OVERLAPS (@start_date, @end_date)

This uses the OVERLAPS operator to see if the periods overlap. I’m not sure if there is an of by 1 mistake in the weight calculation but think that this should be handled in the definition of the input variable (@end_date = @end_date – 1)

👤NuLo

Leave a comment