Functions and Classes in Python

In this chapter, we will explore two other important concepts in Python programming: functions and classes. Functions allow us to group reusable code blocks, while classes enable us to create objects with specific attributes and methods. Both these concepts are critical for structuring and organizing your code more efficiently, especially when tackling complex financial problems.

Functions

Functions are reusable code blocks that perform a specific task. They help break down a program into smaller, more understandable parts. To create a function in Python, we use the keyword def followed by the function name and a pair of parentheses containing the function's parameters. The block of code to be executed is indented under the function definition.

Here is a simple function example for calculating compound interest:

def compound_interest(principal, rate, time):
    amount = principal * (1 + rate) ** time
    interest = amount - principal
    return interest

In this example, we have defined a function called compound_interest that takes three arguments: the initial amount (principal), the interest rate (rate), and the duration in years (time). The function calculates compound interest using the standard formula and returns the result. The return statement allows us to retrieve a value, for example by doing result = my_function(). It is not necessary to return a value if no particular result needs to be retrieved (for instance, if we just want to display something).

To call this function, simply use it with the appropriate arguments:

initial_investment = 1000
interest_rate = 0.04
years = 5

interest = compound_interest(initial_investment, interest_rate, years)
print(f"The compound interest after {years} years is {round(interest,2)} €")

This example calculates compound interest for an initial investment of 1000 €, an interest rate of 4%, and a duration of 5 years.

In this example, you also discover a new way of assigning a value to a string, thanks to the use of f before "" and the use of {} inside the "".

name = "Thomas"
age = 41.75254
print(f"Hello {name}, you are {round(age, 2)} years old")
# Displays "Hello Thomas, you are 41.75 years old"

Here we use the round() function to round the age value to 2 decimal places

Functions are particularly useful for avoiding code duplication and simplifying repetitive tasks. For example, you can reuse the compound_interest function to calculate compound interest for different investments and interest rates.

Optional arguments and default values

It is possible to define default values for certain arguments of a function, making them optional when calling the function. For instance, we can set a default value for the annual interest rate:

def compound_interest(principal, time, rate=0.05):
    amount = principal * (1 + rate) ** time
    interest = amount - principal
    return interest

# Using the function without specifying the interest rate
investment = 1000
years = 5

earned_interest = compound_interest(investment, years)
print(f"The compound interest over {years} years for an investment of {investment} € is {round(earned_interest, 2)} €.")

Here, rate=0.05 sets a default value of 0.05 for the rate parameter. This means that rate is now an optional argument; if we don't provide a value for rate when calling the function, Python will use the default value of 0.05.

Let us see this in action. Suppose we have an investment of 1000 euros, and we want to calculate the compound interest over 5 years. We do not specify the interest rate, so Python will use the default rate of 0.05:

investment = 1000
years = 5

earned_interest = compound_interest(investment, years)
print(f"The compound interest over {years} years for an investment of {investment} € is {round(earned_interest, 2)} €.")

But what if we want to calculate the interest with a different rate, say 7% (or 0.07)? We can simply provide a value for rate when calling the function like this:

earned_interest = compound_interest(investment, years, 0.07)
print(f"The compound interest over {years} years for an investment of {investment} € at a rate of 7% is {round(earned_interest, 2)} €.")

Alternatively, this syntax would work as well:

earned_interest = compound_interest(investment, years, rate=0.07)
print(f"The compound interest over {years} years for an investment of {investment} € at a rate of 7% is {round(earned_interest, 2)} €.")

In a more general way, when defining a function in Python, you can also specify arguments by their parameter name, using the syntax parameter=value. This is especially useful when a function has many parameters, or when you want to only specify some of the optional parameters. This corresponds to what we did with the rate argument in the previous example, except that here we do not provide a default value. In other words, value of parameter=value will necessarily have to be provided when the function is called.

Let us use our compound_interest function as an example:

investment = 1000
years = 5

earned_interest = compound_interest(principal=investment, time=years, rate=0.07)
print(f"The compound interest over {years} years for an investment of {investment} € is {round(earned_interest, 2)} €.")

Here, we called the function with arguments specified by their names.

When you call a function with arguments specified by their names, the order of the arguments does not matter. This is another advantage of specifying parameters by name. For instance, the following call to compound_interest is perfectly valid and will give the same result:

earned_interest = compound_interest(rate=0.07, principal=investment, time=years)
print(f"The compound interest over {years} years for an investment of {investment} € is {round(earned_interest, 2)} €.")

Here, even though rate is the third parameter in the function definition, we provided it first in the function call. Python knows which value goes with which parameter because we specified the parameters by their names.

In summary, function arguments in Python are classified as positional or keyword:

  • Positional arguments are identified by their order and matched accordingly.
  • Keyword arguments are distinguished by an associated keyword (parameter=value), making their order flexible as they are matched by name.

And you can have as many of both types as you want in a function. Just remember, when defining a function, positional arguments must always precede keyword arguments because positional arguments are order-dependent, while keyword arguments are name-identified.

This may sound a bit technical, but do not worry, you will get used to it very fast 😉.

Python Classes

A class in Python provides a blueprint for creating objects that follow the same structure. These objects can have attributes (variables) and methods (functions). Classes play a significant role in structuring and organizing code, as they encapsulate related data and functionalities.

Class example: Bank account

For instance, if we need to model a bank account, offering functions to deposit, withdraw, and check the balance, we can design a BankAccount class:

class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount
        print(f"You've deposited {amount} €. New balance: {self.balance} €.")

    def withdraw(self, amount):
        if amount > self.balance:
            print("Insufficient balance.")
        else:
            self.balance -= amount
            print(f"You've withdrawn {amount} €. New balance: {self.balance} €.")

    def display_balance(self):
        print(f"Your balance is {self.balance} €.")

Here, the BankAccount class has a balance attribute and three methods: deposit, withdraw, and display_balance. The __init__ method, known as the constructor, is invoked whenever an instance of the class is created.

Using the BankAccount class

Let us explore the use of the BankAccount class by creating a few instances and performing standard banking operations like deposits and withdrawals.

# Create a bank account with an initial balance of 500 €
account1 = BankAccount(500)

# Create another bank account with an initial balance of 2000 €
account2 = BankAccount(2000)

# Display the balance of both accounts
account1.display_balance()
account2.display_balance()

# Deposit 1000 € into account1
account1.deposit(1000)

# Withdraw 300 € from account1
account1.withdraw(300)

# Display the balance of both accounts after the operations
account1.display_balance()
account2.display_balance()

# Deposit 1000 € into account2
account2.deposit(1000)

# Withdraw 1500 € from account2
account2.withdraw(1500)

# Display the balance of both accounts after the operations
account1.display_balance()
account2.display_balance()

Here, we have created two BankAccount instances with different initial balances, performed deposits and withdrawals on both accounts, and displayed their balances.

Exercise: The Portfolio Class

In this exercise, you will design a Portfolio class to manage an investment portfolio. This portfolio will hold various financial assets, like stocks or bonds, along with their respective quantities and unit values.

You need to define and use the Portfolio class to:

  1. Add a financial asset to the portfolio with its quantity and unit value.
  2. Remove a financial asset from the portfolio.
  3. Update the quantity or unit value of a financial asset.
  4. Compute the total value of the portfolio.

Instructions

  1. Define a Portfolio class with an assets attribute, which will be a dictionary to store the financial assets and their information (quantity and unit value).
  2. Implement an add_asset method to add a financial asset to the portfolio. This method should take the asset name, quantity, and unit value as parameters. If the asset already exists in the portfolio, the quantity and unit value should be updated.
  3. Create a remove_asset method to remove a financial asset from the portfolio. This method should take the asset name as a parameter. If the asset is not found in the portfolio, an appropriate message should be printed.
  4. Write an update_asset method to update the quantity or unit value of a financial asset. This method should take the asset name, and the new quantity and unit value as parameters. If the asset doesn't exist in the portfolio, an appropriate message should be displayed.
  5. Finally, implement a get_total_value method to compute and return the total value of the portfolio. This should be the sum of the products of the quantity and unit value for each asset.

Here is a starting template for the Portfolio class:

Usage Example

# Creating an empty portfolio
my_portfolio = Portfolio()

# Adding financial assets
my_portfolio.add_asset("AAPL", 10, 150)
my_portfolio.add_asset("GOOG", 5, 1000)
my_portfolio.add_asset("TSLA", 8, 800)

# Updating a financial asset
my_portfolio.update_asset("AAPL", 12, 160)

# Removing a financial asset
my_portfolio.remove_asset("GOOG")

# Calculating the total value of the portfolio
total_value = my_portfolio.get_total_value()
print(f"The total portfolio value is {total_value} €.")

This exercise provides an opportunity to practice creating and using Python classes within the context of finance. A correction example can be found here: https://github.com/RobotTraders/Python_For_Finance/blob/main/exercise_correction_chapter_4.ipynb. Feel free to tailor this example to your own needs and look into other functionalities related to investment portfolio management.

Summary

In this chapter, we've explored Python functions and classes - pivotal tools for structuring and organizing your code. We've learned how to define and utilize them through finance-related real-world examples.

Mastering these concepts enables you to write more intricate and modular programs, simplifying both the maintenance and evolution of your code.

As we progress in this course, we'll look into into specific finance libraries. These tools will equip you to analyze financial data and devise predictive models. Stay tuned!