# Python 'interest rate' object

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?

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)

• Thank you for your in-depth response. Jan 16 '19 at 0:00

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)

• Thanks! I was completely oblivious to the existence of '@property'. Jan 16 '19 at 11:42
• Excellent point! Jan 16 '19 at 14:24