Assignment 4
Objective and Overview
Objective: The objective of this assignment is to practice building lists with list comprehensions, work with list data using list comprehensions, and be introduced to the indefinite loop.
Overview: You will write several functions to aid in pricing fixed-income securities (bonds), and calculate the yield-to-maturity for a bond investment. Finally, you will implement a function to simulate the sale of new bonds via an auction process.
Preliminaries
In your work on this assignment, make sure to abide by the collaboration policies of the course.
If you have questions while working on this assignment, please post them on Piazza! This is the best way to get a quick response from your classmates and the course staff.
General Guidelines
-
Refer to the class Coding Standards for important style guidelines. The grader will be awarding/deducting points for writing code that comforms to these standards.
-
Include comments at the top of the file that are similar to the ones that we gave you at the top of
a1task1.py
. -
Your functions must have the exact names that we have specified, or we won’t be able to test them. Note in particular that the case of the letters matters (all of them should be lowercase), and that you should use an underscore character (
_
) wherever we have specified one (e.g., inconvert_from_inches
). -
Each of your functions should include a docstring that describes what your function does and what its inputs are.
-
If a function takes more than one input, you must keep the inputs in the order that we have specified.
-
You should not use any Python features that we have not discussed in class or read about in the textbook.
-
Unless expressly stated, your functions do not need to handle bad inputs – inputs with a type or value that doesn’t correspond to the description of the inputs provided in the problem.
-
Make sure that your functions return the specified value, rather than printing it. Unless it is expressly stated in the problem, none of these functions should use a
print
statement.
Important note regarding test cases and Gradescope:
-
You must test each function after you write it. Here are two ways to do so:
- Run your file after you finish a given function. Doing so will bring you to the Shell, where you can call the function using different inputs and check to see that you obtain the correct outputs.
-
Add test calls to the bottom of your file, inside the
if __name__ == '__main__'
control struture. For example:if __name__ == '__main__': print("mystery(6,7) returned", mystery(6,7))
These tests will be called every time that you run the file, which will save you from having to enter the tests yourself. We have given you an example of one such test in the starter file.
-
You must not leave any
print
statements in the global scope. This will cause an error with the Gradescope autograder. Make sure all of yourprint
statements are inside a function scope or insude theif __name__ == '__main__'
control struture.
Warm-up Problems
Work on these practice problems before class. We will discuss solutions in class and answer your questions.
-
Practice LC Puzzles – Fill in the Blanks:
>>> [__________ for x in range(4)] [0, 14, 28, 42] >>> [__________ for s in ['boston', 'university', 'cs'] ['bos', 'uni', 'cs'] >>> [__________ for c in 'compsci'] ['cc', 'oo', 'mm', 'pp', 'ss', 'cc', 'ii'] >>> [__________ for x in range(20, 30) if ____________] [20, 22, 24, 26, 28] >>> [__________ for w in ['I', 'like', 'ice', 'cream']] [1, 4, 3, 5]
-
Write the function
divisors(n, values)
, that returns a list of all values for whichn
is a divisor without remainder. Use a list comprehension.>>> divisors(4,[4,5,6,7,8]) [4, 8]
Hint: use an if clause in your list comprehension.
-
Write the function
perfect_squares(values)
, that returns a list of all values that are perfect squares. Use a list comprehension.>>> perfect_squares([4,5,6,7,8,9,10]) [4, 9]
Hint: use an if clause in your list comprehension.
-
Write the function
generate_primes(n)
, that returns a list of all prime numbers up to and includingn
.For example: >>> generate_primes(10) [2, 3, 5, 7]
Hint: use an
if
clause in your list comprehension.You will need to first write a helper function:
is_prime(x)
, which returns True if x is a prime number and False otherwise.For example:
>>> is_prime(10) False >>> is_prime(11) True
The
is_prime
function may use afor
loop or recursion. -
Write the function
guessing_game(low, high)
, that asks a user to guess a secret number in the range oflow
tohigh
.The secret number can be chosen at random:
import random secret = random.randint(low, high)
Prompt the user for a guess:
guess = int(input(f'Guess a number between {low} and {high}: '))
Use an indefinite loop to stop the loop when guess is correct. Inside the loop, give feedback, i.e., “too high” or “too low” and prompt for another guess. After the loop, print out “All done!” The function does not need to return any value.
Task 1: Discounted Cashflows and Bond Pricing
60 points; individual-only
Do this entire task in a file called a4task1.py
.
-
Write the function
cashflow_times(n, m)
to develop the list of the times at which a bond makes coupon payments, withn
years andm
coupon payments per year.For example:
>>> cashflow_times(5,2) # 5-year bond with 2 coupons/year [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] >>> cashflow_times(3,4) # 3-year bond with 4 coupons/year [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] >>> cashflow_times(2,12) # 2-year bond with monthly coupons [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] >>>
Notes:
- Your function must use a list comprehension (no loops or recursion),
and requires no
if-elif-else
logic.
- Your function must use a list comprehension (no loops or recursion),
and requires no
-
Write the function
discount_factors(r, n, m)
to calculate and return a list of discount factors for a given annualized interest rater
, forn
years, andm
discounting periods per year.Your function must use a list comprehension (no loops or recursion).
For example:
>>> # 3% rate, 1 years, 2 payments/year >>> discount_factors(0.03, 1, 2) [0.9852216748768474, 0.9706617486471405] >>> # 5% rate, 5 years, 2 payments/year >>> discount_factors(0.05, 5, 2) [0.9756097560975611, 0.9518143961927424, 0.928599410919749, 0.9059506447997552, 0.8838542876095173, 0.8622968659605048, 0.8412652350834192, 0.8207465708130922, 0.8007283617688704, 0.7811984017257273]
Notes:
-
Your function must use a list comprehension (no loops or recursion), and requires no
if-elif-else
logic. -
You may assume that
m
andn
are integers.
-
-
Write the function
bond_cashflows(fv, c, n, m)
to calculate and return a list of cashflows for a bond specified by the parameters. The parameters are:fv
is the future (maturity) value of the bond;
c
is the annual coupon rate expressed as percentage per year;
n
is the number of years until maturity;
m
is the number of coupon payments per year.In general, the coupon payment is given by:
The final payment of the bond is a coupon payment plus the maturity value (fv) of the bond.
For example:
>>> bond_cashflows(1000, 0.08, 5, 2) # 5 year bond with 2 coupons/year [40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 1040.0] >>> bond_cashflows(10000, 0.0575, 2, 2) # 2 year bond with 2 coupons/year [287.5, 287.5, 287.5, 10287.5] >>> bond_cashflows(5000, 0.09, 2, 4) # 2 year bond with 4 coupons/year [112.5, 112.5, 112.5, 112.5, 112.5, 112.5, 112.5, 5112.5]
Notes:
-
Your function must use a list comprehension (no loops or recursion), and requires no
if-elif-else
logic. -
Re-use your
cashflow_times(n, m)
function to obtain the list of the times of the cashflows for this bond – this will give you the correct number of cashflows. -
Notice that the last period’s casflow includes the maturity vale (
fv
) of the bond as well as the last coupon payment.
-
-
Write the function
bond_price(fv, c, n, m, r)
to calculate and return the price of a bond. The parameters are:fv
is the future (maturity) value of the bond;
c
is the annual coupon rate expressed as percentage per year;
n
is the number of years until maturity;
m
is the number of coupon payments per year;
andr
, the annualized yield to maturity of the bondThe price of the bond is the sum of the present values of the cashflows of that bond.
For example:
>>> # 3-year, 4% coupon bond, semi-annual coupons, priced at 4% annualized YTM. >>> bond_price(100, 0.04, 3, 2, 0.04) 100.0 # sells at par >>> # 3-year, 4% coupon bond, semi-annual coupons, priced at 3% annualized YTM. >>> bond_price(100, 0.04, 3, 2, 0.03) 102.84859358273769 # sells at a premium >>> # 3-year, 4% coupon bond, semi-annual coupons, priced at 5% annualized YTM. >>> bond_price(100, 0.04, 3, 2, 0.05) 97.24593731921014 # sells at a discount
Important Implementation Notes:
-
Your function must use a list comprehension (no loop with an accumulator pattern or recursion) and requires no
if-elif-else
logic. Consider the equation above, and how you could use a list comprehension to build this result. -
Re-use your
cashflow_times(n,m)
function from above to find the cashflow times for this bond. -
Re-use your
bond_cashflows(fv, c, n,m)
function from above to find the cashflows associated with this bond. -
Re-use your
discount_factors(r, n, m)
function from above to calculate the relevant discount factors.
-
-
Write the function
bond_yield_to_maturity(fv, c, n, m, price):
to calculate the annualized yield_to_maturity on a bond. The parameters are:fv
is the future (maturity) value of the bond;
c
is the annual coupon rate expressed as percentage per year;
n
is the number of years until maturity;
m
is the number of coupon payments per year;
andprice
is the current market price of the bondFor example, consider a 3-year bond with a 4% coupon rate, with coupons paid semi-annually. We observe the bond is selling for $101.75. The yield to maturity is the implied interest rate for this bond, in this case is 3.38% per year:
>>> bond_yield_to_maturity(100, 0.04, 3, 2, 101.75) 0.03381660580635071
As a second example, consider a 5-year bond with an 8% coupon rate, with coupons paid annually. We observe the bond is selling for $950. The yield to maturity is the implied interest rate for this bond, in this case 9.29% per year:
>>> bond_yield_to_maturity(1000, 0.08, 5, 1, 950) 0.09295327519066632
This function will use a
while
loop to find the appropriate yield by iteration, i.e., trying many values (for the yield) until it the correct yield (within an acceptable margin of error).Notes:
-
Refer to the example from class in which we continue a guessing game until we find a solution. Create variables for the upper and lower bounds for the interest rate and use the average of these as a test rate. Calculate the bond price using the test rate.
If the calculated price is below the parameter
price
, you need to try a higher bond price – by lowering the interest rate. Set the upper bound of the interest rate to be the test rate, so that you pick a rate that is between the lower bound and the previous test rate .If the calculated price is above the parameter
price
, you need try a lower bond price – by increasing the interest rate. -
You should continue your iteration until the calculated bond price is close enough to the actual bond price, i.e., within an acceptable margin of error. Declare a variable to hold the required degree of accuracy, i.e.,
ACCURACY = 0.0001 # iterate until the error is less than $0.0001
and continue iterating until the basolute value of the difference in bond price is less than this amount of accuracy. Note: the example below uses an accuracy of 0.001, so your results will differ.
-
Be sure to consider and test for cases where bond prices are above the maturity value, i.e., where
r < 0
.
Hints:
-
When you first write your function, include a print statement inside your loop to show the test interest rate and the value of the bond at each iteration of the loop. For example:
>>> bond_yield_to_maturity(100, 0.04, 3, 2, 101.75) Iteration 0, test_rate = 0.50000000, price = $32.117248 diff = $-69.632752 Iteration 1, test_rate = 0.25000000, price = $57.434695 diff = $-44.315305 Iteration 2, test_rate = 0.12500000, price = $79.264523 diff = $-22.485477 Iteration 3, test_rate = 0.06250000, price = $93.930828 diff = $-7.819172 Iteration 4, test_rate = 0.03125000, price = $102.487223 diff = $0.737223 Iteration 5, test_rate = 0.04687500, price = $98.096648 diff = $-3.653352 Iteration 6, test_rate = 0.03906250, price = $100.262983 diff = $-1.487017 Iteration 7, test_rate = 0.03515625, price = $101.367754 diff = $-0.382246 Iteration 8, test_rate = 0.03320312, price = $101.925637 diff = $0.175637 Iteration 9, test_rate = 0.03417969, price = $101.646235 diff = $-0.103765 Iteration 10, test_rate = 0.03369141, price = $101.785821 diff = $0.035821 Iteration 11, test_rate = 0.03393555, price = $101.715999 diff = $-0.034001 Iteration 12, test_rate = 0.03381348, price = $101.750903 diff = $0.000903 Iteration 13, test_rate = 0.03387451, price = $101.733449 diff = $-0.016551 Iteration 14, test_rate = 0.03384399, price = $101.742175 diff = $-0.007825 Iteration 15, test_rate = 0.03382874, price = $101.746539 diff = $-0.003461 Iteration 16, test_rate = 0.03382111, price = $101.748721 diff = $-0.001279 Iteration 17, test_rate = 0.03381729, price = $101.749812 diff = $-0.000188 Iteration 18, test_rate = 0.03381538, price = $101.750357 diff = $0.000357 Iteration 19, test_rate = 0.03381634, price = $101.750084 diff = $0.000084 Iteration 20, test_rate = 0.03381681, price = $101.749948 diff = $-0.000052 Iteration 21, test_rate = 0.03381658, price = $101.750016 diff = $0.000016 Iteration 22, test_rate = 0.03381670, price = $101.749982 diff = $-0.000018 Iteration 23, test_rate = 0.03381664, price = $101.749999 diff = $-0.000001 0.03381660580635071
This will help you verify that your algorithm is working correctly. Remove the print statement when you are done.
-
Task 2: Simulating a Bond Auction
40 points; individual-only
The US Treasury sells new bonds to investors by offering an auction. Interested investors must submit their bids in advance of a deadline, and the Treasury sells the bonds to investors at the highest possible prices (i.e., lowest yield to maturity) and thus the least interest cost.
The auction announcement specifies the type of security to be sold (e.g., 5-year bonds with a 3% annual coupon rate), and the amount the Treasury is planning to sell (e.g., $500,000 of maturity value). The potential investors bids must specify the price they are willing to pay per $100 of maturity value (e.g., paying $99.50 per $100 bond in this example would give an annualized yield-to-maturity of about 3.1088%).
In this task, you will write some functions to create a simple bond auction. To begin,
you should download this sample.csv
file containing a list of
investor’s bids. You will process this file to determine which bids will be accepted
(i.e., those with the highest prices), and the minimum price that is successful in the
auction. The design you should follow includes 3 functions, described below.
You may write additional helper function if you find that useful, but they will not be
graded. Do this entire task in a file called a4task2.py
.
-
Write the function
collect_bids(filename)
to process the data file containing the bids. The data file will be in this format:bid_id, bid_amount, bid_price 1, 100000, 101.50 2, 100000, 101.25 3, 200000, 100.75
in which the first row contains the column headers, and each additional row contains three fields: the
bid_id
which identifies the bidder, thebid_amount
(dollar value) of bonds the this investor would like to buy, and thebid_price
that the investor is willing to pay.Your task in this function is to process the file and return a list containing the bids (i.e., a list of lists). For example:
For example:
>>> # call the function >>> bids = collect_bids('./bond_bids.csv') >>> print(bids) # show the data that was returned [[1, 100000, 101.5], [2, 100000, 101.25], [3, 200000, 100.75], # ...]
-
Write the function
print_bids(bids)
to process the a of bids, and produce a beautifully-formatted table of the bids. In your function, you will process a list containing bids one line at a time.For each bid (a list of 3 fields), you will separate the fields into separate variables and print with appropriate formatting, i.e., with dollar signs, clean spacing, nicely lined up, etc. For example:
>>> bids = collect_bids('./bond_bids.csv') >>> print_bids(bids) Bid ID Bid Amount Price 1 $ 100000 $ 101.500 2 $ 100000 $ 101.250 3 $ 200000 $ 100.750
-
Write the function
find_winning_bids(bids, total_offering_fv, c, n, m)
that processes a list ofbids
and determine which are successful in the auction. The parameters are:
bids
, where each bid is a sublist in the form of[bid_id, bid_amount, bid_price]
,
total_offering_fv
is the total amount of bonds being sold,
c
is the annualized coupon rate,
n
is the number of years to maturity for the bonds, and
m
is the number of coupon payments per year.The function must do the following tasks:
-
sort the bids by
bid_price
(in descending order). -
determine which bids will be accepted, including any partial amounts filled.
-
find the auction’s
clearing_price
, which is lowest price at which the auction amount (total_offering_fv
) is sold out. -
print out the table showing all bids and the amount sold to each bidder. The amount sold might be the full
bid_amount
’, a fractionbid_amount
if the entire bid was not filled, or0
if the bid was unsuccessful. -
print out the auction clearing bond price (i.e., the price at which the offering sells out) and the yield to maturity.
-
return the list of bids, with the actual amonut sold to each bidder.
Example 1:
This test code:
if __name__ == '__main__': # read in the bids bids = collect_bids('./bond_bids.csv') print("Here are all the bids:") print_bids(bids) print() # process the bids in an auction of $500,000 of 5-year 3% semi-annual coupon bonds processed_bids = find_winning_bids(bids, 500000, 0.03, 5, 2)
Produces this output:
Here are all the bids: Bid ID Bid Amount Price 1 $ 100000 $ 101.500 2 $ 100000 $ 101.250 3 $ 200000 $ 100.750 4 $ 400000 $ 101.200 5 $ 250000 $ 100.900 6 $ 300000 $ 100.700 7 $ 120000 $ 101.000 8 $ 150000 $ 101.200 9 $ 100000 $ 101.750 10 $ 350000 $ 100.500 Here are all of the bids, sorted by price descending: Bid ID Bid Amount Price 9 $ 100000 $ 101.750 1 $ 100000 $ 101.500 2 $ 100000 $ 101.250 4 $ 400000 $ 101.200 8 $ 150000 $ 101.200 7 $ 120000 $ 101.000 5 $ 250000 $ 100.900 3 $ 200000 $ 100.750 6 $ 300000 $ 100.700 10 $ 350000 $ 100.500 The auction is for $500000.00 of bonds. 4 bids were successful in the auction. The auction clearing price was $101.200, i.e., YTM is 0.027415 per year. Here are the results for all bids: Bid ID Bid Amount Price 9 $ 100000 $ 101.750 1 $ 100000 $ 101.500 2 $ 100000 $ 101.250 4 $ 200000 $ 101.200 8 $ 0 $ 101.200 7 $ 0 $ 101.000 5 $ 0 $ 100.900 3 $ 0 $ 100.750 6 $ 0 $ 100.700 10 $ 0 $ 100.500
Example 2:
This test code:
if __name__ == '__main__': # read in the bids bids = collect_bids('./bond_bids.csv') print("Here are all the bids:") print_bids(bids) print() # process the bids in an auction of $1,400,000 of 5-year 3.25% semi-annual coupon bonds processed_bids = find_winning_bids(bids, 1400000, 0.0325, 5, 2)
Produces this output:
Here are all the bids: Bid ID Bid Amount Price 1 $ 100000 $ 101.500 2 $ 100000 $ 101.250 3 $ 200000 $ 100.750 4 $ 400000 $ 101.200 5 $ 250000 $ 100.900 6 $ 300000 $ 100.700 7 $ 120000 $ 101.000 8 $ 150000 $ 101.200 9 $ 100000 $ 101.750 10 $ 350000 $ 100.500 Here are all of the bids, sorted by price descending: Bid ID Bid Amount Price 9 $ 100000 $ 101.750 1 $ 100000 $ 101.500 2 $ 100000 $ 101.250 4 $ 400000 $ 101.200 8 $ 150000 $ 101.200 7 $ 120000 $ 101.000 5 $ 250000 $ 100.900 3 $ 200000 $ 100.750 6 $ 300000 $ 100.700 10 $ 350000 $ 100.500 The auction is for $1400000.00 of bonds. 8 bids were successful in the auction. The auction clearing price was $100.750, i.e., YTM is 0.030870 per year. Here are the results for all bids: Bid ID Bid Amount Price 9 $ 100000 $ 101.750 1 $ 100000 $ 101.500 2 $ 100000 $ 101.250 4 $ 400000 $ 101.200 8 $ 150000 $ 101.200 7 $ 120000 $ 101.000 5 $ 250000 $ 100.900 3 $ 180000 $ 100.750 6 $ 0 $ 100.700 10 $ 0 $ 100.500
Implementation Notes:
-
To sort the bids by price, use the technique that was shown in class with a list comprehension and a list-of-lists. Once you have the appropriate list-of-lists, you can use the built-in
list
sorting method calledsort()
. For example, experiment with this code at the Python console:>>> somelist = ['z','a','y','b','x','c'] >>> somelist.sort() >>> print(somelist) >>> somelist.reverse() >>> print(somelist)
-
Do not re-write existing code that you have done in your previous functions! For example, re-use your
yield_to_maturity
function from task 1. Remember toimport
it into this file.
-
Submitting Your Work
Use the link to GradeScope (left) to submit your work.
Be sure to name your files correctly!
Under the heading for Assignment 4, attach each of the 3 required files to your submission.
When you upload the files, the autograder will test your program.
Notes:
- You may resubmit multiple times, but only the last submission will be graded.
Warnings about Submissions
-
Make sure to use these exact file names, or Gradescope will not accept your files. If Gradescope reports that a file does not have the correct name, you should rename the file using the name listed in the assignment page.
-
If you make any last-minute changes to one of your Python files (e.g., adding additional comments), you should run the file in Spyder after you make the changes to ensure that it still runs correctly. Even seemingly minor changes can cause your code to become unrunnable.
-
If you submit an unrunnable file, Gradescope will accept your file, but it will not be able to auto-grade it. If time permits, you are strongly encouraged to fix your file and resubmit. Otherwise, your code will fail most if not all of our tests.
Important note regarding test cases and Gradescope:
-
You must test each function after you write it. Here are two ways to do so:
- Run your file after you finish a given function. Doing so will bring you to the Shell, where you can call the function using different inputs and check to see that you obtain the correct outputs.
-
Add test calls to the bottom of your file, inside the
if __name__ == '__main__'
control structure. For example:if __name__ == '__main__': print("mystery(6,7) returned", mystery(6,7))
These tests will be called every time that you run the file, which will save you from having to enter the tests yourself. We have given you an example of one such test in the starter file.
-
You must not leave any
print
statements in the global scope. This will cause an error with the Gradescope autograder. Make sure all of yourprint
statements are inside a function scope or insude theif __name__ == '__main__'
control structure.