This is an INDIVIDUAL workshop. We learn how to think algorithmically and how to translate an algorithm into Python code. We cover variables, conditionals, loops, lists, dictionaries and functions, and we apply them to classic finance problems: paying off a mortgage, pricing a bond, building an amortization table, and finding the yield of a bond with a search algorithm.
1 General directions for each workshop
You will work in Google Colab for all the workshops. Log in with your @tec.mx account and create a new Colab notebook for each workshop. Rename your notebook (click on the default name, “Untitled.ipynb”) as:
W1-Algorithms-YourFirstName-YourLastName
When you finish the Workshop, download it as Jupyter Notebook (as a .ipynb file), and then submit that .ipynb file through Canvas.
1.1 How to work in this workshop
Read each section and re-type and run the code yourself. Do not only copy and paste: when you type the code, you notice what each line does. Add your own notes in text cells, as if this notebook were your personal study guide for the Evidence (in-class exam).
In this course we follow a simple learning cycle for every piece of code:
Predict: before running a code chunk, write in a text cell what you think it will print.
Run: run the chunk and compare with your prediction.
Investigate: if your prediction was wrong, find out why. This is where most of the learning happens.
Modify: change one thing (a number, a condition, a variable) and see what happens.
Make: write your own code for the challenges.
1.2 Using an LLM (Gemini, ChatGPT, Claude, etc.)
You are allowed, and encouraged, to use an LLM to help you write code. However, the goal of this course is that you understand Python well enough to read and check any code, including code generated by an LLM. Then, for every challenge:
Write your own algorithm (pseudocode) first, in a text cell. Then use it as your prompt.
If you used an LLM, paste your prompt in a text cell (in quotes).
Verify the result with an independent check: Excel, a closed-form formula, or a simple assert statement. Code that runs without errors is not necessarily correct!
Be ready to explain any line of your code. You will be asked to do so in the Canvas quizzes and in the Evidence.
1.3 Grading of this workshop
Your workshop grade has two parts:
Part
Weight
What is evaluated
Notebook (Jupyter Notebook file (.ipynb file) submitted in Canvas)
60%
Completeness: all code replicated, all challenges attempted, your algorithms (pseudocode), your notes and your own explanations
Canvas Quiz W1
40%
Automatically graded: you enter key results from your notebook and answer short code-reading questions
The Canvas Quiz W1 is due on the same date as the workshop. You have 2 attempts; Canvas keeps your highest score. Numeric answers accept a small tolerance, so round as indicated in each question.
For the notebook part:
Complete (100%): ORIGINAL and COMPLETE notebook, with all the activities, your notes and your OWN answers to the questions.
Incomplete (75%): ORIGINAL notebook with all the activities, but you did not answer some of the questions.
Very incomplete (10%-70%): you completed between 10% and 75% of the workshop, or parts of your work were copied from other students.
Not submitted (0%).
Before submitting, run all the cells from top to bottom (in Colab: Runtime → Run all, or Ctrl+F9) to make sure that your notebook runs without errors. And then, download your Notebook as .iypnb file, and then submit that file.
2 What is an algorithm?
An algorithm is a finite sequence of clear, unambiguous steps that transforms some inputs into a desired output. A cooking recipe is an algorithm: the ingredients are the inputs, the instructions are the steps, and the dish is the output.
In Finance, we use algorithms all the time, even if we do not call them that way. When you build an amortization table in Excel, you are executing an algorithm: for each month, calculate the interest, subtract the payment, and move to the next month.
2.1 How to design an algorithm
We will follow a 5-step design recipe for every problem in this course:
State the problem in your own words, with short and clear sentences.
Identify the inputs: what information do you need to solve the problem?
Identify the output: what is the final result you expect? In what units?
Write the general approach: 3 to 5 big steps in sequential order.
Detail each step: specify calculations, conditions (if … then …) and repetitions (repeat until …). This detailed version is your pseudocode.
2.2 Example: how many years to double an investment?
1. Problem. I invest $100 at 10% annual interest. How many years do I need to wait until my balance is at least twice my initial investment?
3. Output. The number of years (an integer), and the balance at that year.
4. General approach. Start with the initial investment. Grow the balance one year at a time. Stop when the balance reaches the target.
5. Pseudocode.
SET balance = initial investment
SET year = 0
REPEAT WHILE balance < multiple × initial investment:
year = year + 1
balance = balance × (1 + rate)
END REPEAT
DISPLAY year and balance
Notice that the pseudocode is not written in any programming language. It uses plain words, but it is precise: there is only one way to interpret each line. Anybody (or any LLM) can translate this pseudocode into Python, R or Excel.
2.3 Pseudocode vs programming code
The same algorithm in Python:
initial_investment =100rate =0.10multiple =2balance = initial_investmentyear =0while balance < multiple * initial_investment: year = year +1 balance = balance * (1+ rate)print(f"You need {year} years. Your balance will be ${balance:,.2f}")
You need 8 years. Your balance will be $214.36
Compare the pseudocode and the Python code line by line. They are almost the same! This is the main idea of this course: if you can write a clear algorithm, you can write (or check) the code.
2.4 How to use an LLM to learn to think algorithmically
LLMs are very good at translating a clear algorithm into code, and very good at explaining code line by line. They are much less reliable when the prompt is vague. Some good practices:
Give the LLM your inputs, output and steps, not only the problem statement.
Ask for simple code (“use a while loop, do not use external libraries”) so you can understand it.
Ask the LLM to explain the code line by line, and then check whether the explanation matches what the code does.
Never accept a result without an independent check.
3 Python foundations
A programming language is a set of instructions that the computer executes to automate tasks. In Economics and Finance we use programming languages mainly to:
collect data,
clean data,
transform and merge data,
store data,
analyze data with descriptive statistics and predictive models, and
deliver information: reports with tables and graphs.
Python and R are free, open-source languages, and they are the two most popular languages for Data Science in the world. In this course we use Python.
Any programming language has the following building blocks:
Variables and variable types
Conditionals (decisions) using comparison operators
Loops to do repetitive tasks
Data structures: lists, dictionaries, arrays, data frames
Functions: reusable blocks of code with inputs and outputs
We now review each one.
3.1 Variables and types
A variable is a name that refers to a value stored in memory. We create a variable with the assignment operator =:
loan =3000000# an integer (int)apr =0.11# a decimal number (float)bank ="Banorte"# text (str, string)approved =True# a logical value (bool): True or Falseprint(type(loan), type(apr), type(bank), type(approved))
Important: the = sign does not mean equality as in mathematics. It means “compute the right-hand side, and store the result in the name on the left”. Then, this is perfectly valid:
balance =100balance = balance *1.10# take the current balance, multiply it, and store it againprint(balance)
We display results with f-strings: a string that starts with f and contains variables inside curly braces {}. After a colon we can specify the format:
payment =40000.456print(f"The payment is {payment}")print(f"The payment is ${payment:,.2f}") # thousands separator and 2 decimalsprint(f"The rate is {apr:.2%}") # as a percentage with 2 decimals
The payment is 40000.456
The payment is $40,000.46
The rate is 11.00%
3.2 Conditionals
A conditional executes a block of code only if a condition is True. Comparison operators are: == (equal), != (different), <, <=, >, >=. We combine conditions with and, or, not.
Indentation matters in Python. The lines that belong to an if block must be indented (4 spaces). Python uses indentation, not braces or END statements, to know where a block ends.
Example: a bond is traded at a premium if its price is higher than its face value, at par if it is equal, and at a discount if it is lower:
price =1050face_value =1000if price > face_value: status ="premium"elif price == face_value: status ="par"else: status ="discount"print(f"The bond trades at a {status}")
The bond trades at a premium
WarningCommon error: = vs ==
= assigns a value to a variable; == compares two values. Writing if price = face_value: produces a SyntaxError.
3.3 Loops
A loop repeats a block of code. Python has two types of loops:
for loops repeat a block a known number of times, or once for each element of a collection.
while loops repeat a block while a condition is True. We use them when we do not know in advance how many iterations we need (as in the “double my investment” example).
The function range(start, stop) generates integers from start up to stop - 1:
for month inrange(1, 4):print("Month", month)
Month 1
Month 2
Month 3
3.3.1 The accumulator pattern
Many financial calculations follow the accumulator pattern: start a variable at zero (or at an initial value) before the loop, and update it inside the loop.
Example: I save $1,000 at the end of each month in an account that pays 12% annual interest compounded monthly. What is my balance after 6 months?
A trace table is a table where you write the value of each variable after each iteration. It is the best tool to understand (and debug) a loop. Here is the trace table for the first 3 iterations of the previous loop:
month
balance before
interest (1%)
deposit
balance after
1
0.00
0.00
1,000
1,000.00
2
1,000.00
10.00
1,000
2,010.00
3
2,010.00
20.10
1,000
3,030.10
You will build trace tables in some challenges and in the quizzes. Get used to them!
3.4 Lists
A list is an ordered collection of values, written with square brackets. Lists are very useful to store cash flows, prices or names:
cash_flows = [-100, 30, 40, 50, 20] # year 0 to year 4print(len(cash_flows)) # number of elementsprint(cash_flows[0]) # first element: Python starts counting at 0!print(cash_flows[-1]) # last elementprint(cash_flows[1:3]) # a slice: elements 1 and 2 (the stop is NOT included)
5
-100
20
[30, 40]
WarningPython counts from 0
The first element of a list is cash_flows[0], not cash_flows[1]. A slice [1:3] includes position 1 but excludes position 3. This is the most common source of “off-by-one” errors.
We can add elements with the append method, and we can loop over the elements of a list directly:
prices = [] # an empty listprices.append(10.5)prices.append(11.2)prices.append(10.9)for p in prices:print(p)
10.5
11.2
10.9
When we need both the position and the value, we use enumerate. Example: the Net Present Value (NPV) of the previous cash flows at a 10% discount rate:
A list comprehension builds a new list from another list in one line. It is a compact version of a for loop that appends values:
# Present value of each cash flow, in one line:pv_flows = [cf / (1+ rate) ** t for t, cf inenumerate(cash_flows)]print(pv_flows)print(f"NPV = {sum(pv_flows):.4f}")
Read it as: “for each t, cf in enumerate(cash_flows), compute cf / (1+rate)**t, and collect the results in a list”. You will see list comprehensions very often in advanced Finance courses, so learn to read them now.
3.5 Dictionaries
A dictionary stores key: value pairs, written with curly braces. It is ideal to store the characteristics of an object, such as a bond or a loan:
bond = {"face_value": 1000, "coupon_rate": 0.06, "years": 10, "frequency": 2}print(bond["coupon_rate"]) # access a value by its keybond["issuer"] ="CEMEX"# add a new keyfor key, value in bond.items():print(key, "->", value)
0.06
face_value -> 1000
coupon_rate -> 0.06
years -> 10
frequency -> 2
issuer -> CEMEX
A list of dictionaries is a very natural way to store a table: each dictionary is a row, and the keys are the column names. The pandas library converts it into a data frame (a table) in one line:
import pandas as pdrows = []for year inrange(1, 4): rows.append({"year": year, "balance": 100*1.10** year})table = pd.DataFrame(rows)table
year
balance
0
1
110.0
1
2
121.0
2
3
133.1
We will use this pattern to build an amortization table in Challenge 3.
3.6 Functions
If we need to run the same algorithm many times with different inputs, we write a function. A function receives parameters (inputs), executes its steps, and returns an output.
def npv(rate, cash_flows):"""Net present value of a list of cash flows. cash_flows[0] happens today (t=0), cash_flows[1] at t=1, and so on.""" total =0for t, cf inenumerate(cash_flows): total = total + cf / (1+ rate) ** treturn totalprint(npv(0.10, [-100, 30, 40, 50, 20]))print(npv(0.15, [-100, 30, 40, 50, 20]))
11.55658766477697
0.6435797470706639
Some important details:
The text between triple quotes """ is the docstring: it documents what the function does. Always write one.
Variables created inside a function (like total) only exist inside the function.
return sends the result back. A function without return returns None.
3.6.1 Default values and multiple outputs
Parameters can have default values, so the user only needs to specify them when they are different from the default. A function can also return several values (Python packs them in a tuple), and we can unpack them into several variables:
def future_value(pv, rate, years, frequency=1):"""Future value of pv invested at an annual rate compounded `frequency` times a year. Returns the future value and the total interest earned.""" fv = pv * (1+ rate / frequency) ** (years * frequency) interest = fv - pvreturn fv, interestfv1, int1 = future_value(1000, 0.12, 5) # annual compounding (default)fv12, int12 = future_value(1000, 0.12, 5, frequency=12) # monthly compoundingprint(f"Annual: {fv1:,.2f} (interest {int1:,.2f}); monthly: {fv12:,.2f} (interest {int12:,.2f})")
Why is this important? In advanced courses (for example, Hedge Funds), most of the code is organized as small functions with default values, docstrings and multiple outputs, like this one:
You do not need to understand what this function does yet, but you can already read its structure: one required parameter, one parameter with a default value, a docstring, and 3 outputs unpacked into 3 variables.
3.7 Objects and methods
In Python, everything is an object: a number, a list, a data frame. An object contains data and methods, which are functions that belong to the object. We call a method with a dot:
name ="grupo bimbo"print(name.upper()) # a method of stringsflows = [30, 40]flows.append(50) # a method of listsprint(flows)print(table.head(2)) # a method of data frames
GRUPO BIMBO
[30, 40, 50]
year balance
0 1 110.0
1 2 121.0
When you see something.method(), read it as “apply method to something”. This is all you need to know about Object-Oriented Programming for now.
3.8 Reading error messages
Errors are normal. Read the last line of the error message first: it tells you the type of error and usually the line where it happened. The most common errors are:
Error
Typical cause
NameError: name 'x' is not defined
You used a variable before creating it, or misspelled it (Python is case-sensitive: Rate ≠ rate)
SyntaxError
Missing colon : after if, for, while or def; missing parenthesis; = instead of ==
IndentationError
The lines of a block are not aligned
TypeError
You combined incompatible types, for example "100" + 5
IndexError: list index out of range
You asked for position n in a list of length n (remember: the last position is n-1)
ZeroDivisionError
A division by zero, for example a rate of 0 in an annuity formula
A while loop whose condition never becomes False will run forever. If a cell does not finish, stop it (the stop button in Colab) and check the loop condition.
4 CHALLENGE 1: Paying off a mortgage
You have to write an algorithm and the corresponding Python code to calculate the number of months needed to pay off a mortgage loan:
Monthly fixed payment = $40,000.00 (includes interest and capital)
Assume that each month the balance first grows with the monthly interest (APR/12), and then the payment is made at the end of the month. In the last month, you only pay what is left (balance plus that month’s interest), which is less than the fixed payment.
Your program must report: (a) the number of months needed to pay off the loan, (b) the amount of the last payment, and (c) the total amount of interest paid over the life of the loan. Your program must work for any change in the values of the inputs.
Do the following:
Write the 5 steps of the design recipe (problem, inputs, output, general approach, pseudocode) in a text cell.
Build a trace table by hand (in a text cell or in Excel) for the first 3 months.
Write (or generate with an LLM from your pseudocode) the Python code. Compare the first 3 months with your trace table.
Verify your number of months with the closed-form formula for the number of periods of an annuity:
where C is the periodic coupon, i is the periodic interest rate (the annual rate divided by the number of payments per year), and N is the total number of periods.
Write the design recipe for a functionbond_price that receives the annual market interest rate, the years to maturity, the frequency of payments, the annual coupon rate and the face value, and returns the price of the bond. Use default values for the frequency (2) and the face value (1,000).
Write the function using a loop to discount each coupon (do not use the annuity formula).
Using a list of market rates [0.08, 0.11, 0.13] and a for loop, calculate the price of the ABC bond for each rate. For each rate, use a conditional to display whether the bond trades at a premium, at par or at a discount.
Verify one of your prices with the annuity formula: Price = C\times\frac{1-(1+i)^{-N}}{i}+\frac{FV}{(1+i)^{N}}
Explain with your own words why the price goes down when the market rate goes up.
6 CHALLENGE 3: Amortization table
Using the mortgage of Challenge 1 ($3,000,000, APR = 11%, payment = $40,000):
Build the full amortization table as a list of dictionaries, where each dictionary is a month with the following keys: month, beginning_balance, interest, payment, principal (the part of the payment that reduces the balance) and ending_balance.
Convert the list into a pandas data frame and show the first 5 and the last 5 rows (use the methods .head() and .tail()).
Using the data frame, calculate the total interest paid (hint: table["interest"].sum()), and check with assert that it matches the total interest of Challenge 1 (allow a difference smaller than 0.01), and that the last ending balance is zero.
In which month does the principal part of the payment become larger than the interest part for the first time? Hint: use a condition on the data frame, for example table[table["principal"] > table["interest"]], and look at the first row.
7 CHALLENGE 4: Finding the yield of a bond with a search algorithm
(Optional, it’s a little hard problem)
In Challenge 2 we calculated the price of a bond for a given market rate. In practice, we often have the opposite problem: we observe the price in the market, and we want to know the yield to maturity (YTM), the market rate that makes the price formula equal to the observed price. There is no closed formula for the YTM, so we need an algorithm.
The bisection method is a classic search algorithm. The idea: the bond price goes down when the rate goes up. If we know a rate that is too low (the price is above the observed price) and a rate that is too high (the price is below the observed price), the YTM must be between them. We try the midpoint, and we keep the half where the YTM must be. We repeat until the interval is very small.
SET low = 0.0001, high = 1.0
REPEAT WHILE (high − low) > tolerance:
mid = (low + high) / 2
IF bond_price(mid) > observed_price THEN
low = mid (the rate is too low: the YTM is in the upper half)
ELSE
high = mid (the rate is too high: the YTM is in the lower half)
END IF
END REPEAT
RETURN mid
The ABC bond of Challenge 2 is trading at $2,600,000. Write a function ytm_bisection(observed_price, years, coupon_rate, frequency=2, face_value=1000, tolerance=1e-8) that implements this algorithm and uses your bond_price function inside it.
Report the YTM (annual, in %, with 4 decimals) and the number of iterations your algorithm needed.
Verify: plug the YTM into bond_price and check that you get the observed price.
Explain with your own words why this algorithm always finds the answer, and why the number of iterations is small.
8 CHALLENGE 5: Code reading
Without running the code, predict what each piece of code prints. Write your prediction in a text cell. Then run it and explain any difference.
5.1
total =0for i inrange(1, 5): total = total + i *10print(total)
5.3 The following code should calculate the future value of $1,000 invested for 3 years at 10% annually, but it prints the wrong result. Find the bug and fix it.
balance =1000for year inrange(1, 3): balance = balance *1.10print(balance)
5.4
def fee(amount, rate=0.02, minimum=50): f = amount * rateif f < minimum: f = minimumreturn fprint(fee(1000), fee(10000), fee(10000, rate=0.01))
9 Coursera - Google: online courses
You MUST TAKE the Coursera Course Get Started with Python. You have to finish this course before we start Week 3. The successful completion of this course for for your final grade.
10 W1 submission
Submit the .ipnb file of your Google Colab Notebook in Canvas.
Answer the Canvas Quiz W1 before the deadline. Keep your notebook open while you answer it: you will need your results.