0
\$\begingroup\$

I have only recently started learning python and the below is my attempt to create an 'interest rate' object which, given one of several types of interest/discount rate, gives the user access to all of the other corresponding rates that are equivalent to this. This looks as follows:

import math

class InterestRate:
    def __init__(self, effectiveInterestRate=0, nominalInterestRate=0, effectiveDiscountRate=0, nominalDiscountRate=0, discountFactor=0, forceOfInterest=0, paymentsPerPeriod=1):

        self.effectiveInterestRate = effectiveInterestRate
        self.nominalInterestRate = nominalInterestRate
        self.effectiveDiscountRate = effectiveDiscountRate
        self.nominalDiscountRate = nominalDiscountRate
        self.discountFactor = discountFactor
        self.forceOfInterest = forceOfInterest
        self.paymentsPerPeriod = paymentsPerPeriod

        numberOfRatesProvided = 0
        for attr, value in vars(self).items():
            if value != 0 and attr != 'paymentsPerPeriod':
                numberOfRatesProvided += 1
                rateGiven = attr
        if numberOfRatesProvided != 1:
            raise Exception("Incorrect number of inputs passed to InterestRate object")

        if rateGiven == 'nominalInterestRate': self.effectiveInterestRate = (nominalInterestRate/paymentsPerPeriod + 1)**paymentsPerPeriod - 1
        elif rateGiven == 'effectiveDiscountRate': self.effectiveInterestRate = effectiveDiscountRate / (1 - effectiveDiscountRate)
        elif rateGiven == 'nominalDiscountRate': self.effectiveInterestRate = (1 - nominalDiscountRate/paymentsPerPeriod)**-paymentsPerPeriod - 1
        elif rateGiven == 'discountFactor': self.effectiveInterestRate = 1/discountFactor - 1
        elif rateGiven == 'forceOfInterest': self.effectiveInterestRate = math.exp(forceOfInterest) - 1

        self.nominalInterestRate = self.paymentsPerPeriod*((1+self.effectiveInterestRate)**(1/self.paymentsPerPeriod) - 1)
        self.effectiveDiscountRate = self.effectiveInterestRate/(1+self.effectiveInterestRate)
        self.nominalDiscountRate = self.paymentsPerPeriod*(1 - (1 + self.effectiveInterestRate)**(-1/self.paymentsPerPeriod))
        self.discountFactor = 1 / (1 + self.effectiveInterestRate)
        self.forceOfInterest = math.log(1 + self.effectiveInterestRate)

    def outputInterestRateData(self):
        print("\n  ----------------------------------------------------")
        print("                   Interest Rate Data                 ")
        print("  ----------------------------------------------------")
        print("   Payments per Period:        ", self.paymentsPerPeriod)
        print("   Effective Interest Rate:    ", self.effectiveInterestRate)
        print("   Effective Discount Rate:    ", self.effectiveDiscountRate)
        print("   Nominal Interest Rate:      ", self.nominalInterestRate)
        print("   Nominal Discount Rate:      ", self.nominalDiscountRate)
        print("   Discount Factor:            ", self.discountFactor)
        print("   Force Of Interest:          ", self.forceOfInterest)
        print("  ----------------------------------------------------")

    def shiftedPaymentPV(paymentAmmount, periodsShifted):
        return paymentAmmount*(1 + self.effectiveInterestRate)**periodsShifted

Assuming that it is clear enough what I have attempted to do here, can anyone offer some constructive criticism on my first attempt at a python class module?

\$\endgroup\$
3
\$\begingroup\$

Trying to use the code

The class defines something which feels a bit weird to use. We'll see a few ways to improve this.

The constructor provides default value for all parameters which leads one to think he can just do r = InterestRate() and enjoy. It is most definitly not the case because one (and only one) of the inputs must be provided.

This suggests that maybe we should not have a single constructor doing many things but many small constructors (using alternative constructors for example).

Then, trying to use the shiftedPaymentPV method, I stumbled on a different error: the self is not part of the signature which makes it unusable as far as I can tell.

Simplifying the code

In the __init__, we set the paymentsPerPeriod attribute only to excluse it in next operation when handling all attributes. We may as well set it afterward to makes things more simple.

This also shows how subtle and fragile that piece of logic is. One could easily break the whole thing by moving an attribute which doesn't seem to be used because we use introspection magic (which is pretty cool in itself but probably better to avoid here).

Let's see how it can be improved anyway.

Instead of counting elements as we go, we could put them in a list and count them afterwards. This helps if we want to show the list to the user in the exception message.

    ratesGiven = [attr for attr, value in vars(self).items() if value != 0 and attr != 'paymentsPerPeriod']
    if len(ratesGiven) != 1:
        raise Exception("Incorrect number of inputs passed to InterestRate object")
    rateGiven = ratesGiven[0]

This could give use the following code:

import math

class InterestRate:

    @classmethod
    def from_nominal(cls, nominalInterestRate, paymentsPerPeriod=1):
        effectiveInterestRate = (nominalInterestRate/paymentsPerPeriod + 1) ** paymentsPerPeriod - 1
        return cls(effectiveInterestRate=effectiveInterestRate, paymentsPerPeriod=paymentsPerPeriod)

    @classmethod
    def from_effective_discount_rate(cls, effectiveDiscountRate, paymentsPerPeriod=1):
        effectiveInterestRate = effectiveDiscountRate / (1 - effectiveDiscountRate)
        return cls(effectiveInterestRate=effectiveInterestRate, paymentsPerPeriod=paymentsPerPeriod)

    @classmethod
    def from_nominal_discount_rate(cls, nominalDiscountRate, paymentsPerPeriod=1):
        effectiveInterestRate = (1 - nominalDiscountRate/paymentsPerPeriod) ** -paymentsPerPeriod - 1
        return cls(effectiveInterestRate=effectiveInterestRate, paymentsPerPeriod=paymentsPerPeriod)

    @classmethod
    def from_discount_factor(cls, discountFactor, paymentsPerPeriod=1):
        effectiveInterestRate = 1 / discountFactor - 1
        return cls(effectiveInterestRate=effectiveInterestRate, paymentsPerPeriod=paymentsPerPeriod)

    @classmethod
    def from_force_of_interest(cls, forceOfInterest, paymentsPerPeriod=1):
        effectiveInterestRate = math.exp(forceOfInterest) - 1
        return cls(effectiveInterestRate=effectiveInterestRate, paymentsPerPeriod=paymentsPerPeriod)

    @classmethod
    def from_effective_interest_rate(cls, effectiveInterestRate, paymentsPerPeriod=1):
        return cls(effectiveInterestRate=effectiveInterestRate, paymentsPerPeriod=paymentsPerPeriod)

    def __init__(self, effectiveInterestRate, paymentsPerPeriod):
        self.effectiveInterestRate = effectiveInterestRate
        self.paymentsPerPeriod = paymentsPerPeriod

        self.nominalInterestRate = paymentsPerPeriod * ((1 + effectiveInterestRate) ** (1 / paymentsPerPeriod) - 1)
        self.effectiveDiscountRate = effectiveInterestRate / (1 + effectiveInterestRate)
        self.nominalDiscountRate = paymentsPerPeriod * (1 - (1 + effectiveInterestRate) ** (-1 / paymentsPerPeriod))
        self.discountFactor = 1 / (1 + effectiveInterestRate)
        self.forceOfInterest = math.log(1 + effectiveInterestRate)

    def outputInterestRateData(self):
        print("\n  ----------------------------------------------------")
        print("                   Interest Rate Data                 ")
        print("  ----------------------------------------------------")
        print("   Payments per Period:        ", self.paymentsPerPeriod)
        print("   Effective Interest Rate:    ", self.effectiveInterestRate)
        print("   Effective Discount Rate:    ", self.effectiveDiscountRate)
        print("   Nominal Interest Rate:      ", self.nominalInterestRate)
        print("   Nominal Discount Rate:      ", self.nominalDiscountRate)
        print("   Discount Factor:            ", self.discountFactor)
        print("   Force Of Interest:          ", self.forceOfInterest)
        print("  ----------------------------------------------------")

    def shiftedPaymentPV(paymentAmmount, periodsShifted):
        assert False, "this can't work"
        return paymentAmmount*(1 + self.effectiveInterestRate)**periodsShifted


r = InterestRate.from_nominal(nominalInterestRate=-0.9375, paymentsPerPeriod=2)
print(r.outputInterestRateData())

Style

Python has a Style Guide called PEP 8. It is definitly worth reading it and trying to apply it.

In your case, that would mean adding whitespaces around operators and using the snake_case naming convention for variables and method names.

String conversions

Instead of a outputInterestRateData, maybe you could define the __str__ method.

Also, you could take this chance to use one of the various ways to format strings in Python.

You'd get something like:

    def __str__(self):
        return """
  ----------------------------------------------------
                   Interest Rate Data                 
  ----------------------------------------------------
   Payments per Period:     {}   
   Effective Interest Rate: {}   
   Effective Discount Rate: {}   
   Nominal Interest Rate:   {}   
   Nominal Discount Rate:   {}   
   Discount Factor:         {}   
   Force Of Interest:       {}   
  ----------------------------------------------------
        """.format(self.paymentsPerPeriod, self.effectiveInterestRate,
                self.effectiveDiscountRate, self.nominalInterestRate,
                self.nominalDiscountRate, self.discountFactor,
                self.forceOfInterest)
\$\endgroup\$
1
  • \$\begingroup\$ Thank you for your in-depth response. \$\endgroup\$
    – M Smith
    Jan 16 '19 at 0:00
3
\$\begingroup\$

Starting from the excellent progress @Josay made, I'd only have one major suggestion: Turn the set-once properties into read-only, computed properties. This will:

  1. prevent them from being inadvertently overwritten by other code and
  2. allow the effectiInterestRate and/or paymentsPerPeriod to be updated, without the derived computations then becoming invalid.

The main change is to make the constructor be:

    def __init__(self, effectiveInterestRate, paymentsPerPeriod):
        self.effectiveInterestRate = effectiveInterestRate
        self.paymentsPerPeriod = paymentsPerPeriod

and then make some computed properties:

    @property
    def nominalInterestRate(self):
        return self.paymentsPerPeriod * ((1 + self.effectiveInterestRate) ** (1 / self.paymentsPerPeriod) - 1)

    @property
    def effectiveDiscountRate(self):
        return self.effectiveInterestRate / (1 + self.effectiveInterestRate)

    @property
    def nominalDiscountRate(self):
        return self.paymentsPerPeriod * (1 - (1 + self.effectiveInterestRate) ** (-1 / self.paymentsPerPeriod))

    @property
    def discountFactor(self):
        return 1 / (1 + self.effectiveInterestRate)

    @property
    def forceOfInterest(self):
        return math.log(1 + effectiveInterestRate)
\$\endgroup\$
2
  • \$\begingroup\$ Thanks! I was completely oblivious to the existence of '@property'. \$\endgroup\$
    – M Smith
    Jan 16 '19 at 11:42
  • \$\begingroup\$ Excellent point! \$\endgroup\$
    – SylvainD
    Jan 16 '19 at 14:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.