MF 602

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., in convert_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 your print statements are inside a function scope or insude the if __name__ == '__main__' control struture.


Warm-up Problems

Work on these practice problems before class. We will discuss solutions in class and answer your questions.

  1. 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]
    
  2. Write the function divisors(n, values), that returns a list of all values for which n 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.

  3. 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.

  4. Write the function generate_primes(n), that returns a list of all prime numbers up to and including n.

    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 a for loop or recursion.

  5. Write the function guessing_game(low, high), that asks a user to guess a secret number in the range of low to high.

    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.

  1. Write the function cashflow_times(n, m) to develop the list of the times at which a bond makes coupon payments, with n years and m 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.
  2. Write the function discount_factors(r, n, m) to calculate and return a list of discount factors for a given annualized interest rate r, for n years, and m 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 and n are integers.

  3. 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.

  4. 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;
    and r, the annualized yield to maturity of the bond

    The 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.

  5. 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;
    and price is the current market price of the bond

    For 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.

  1. 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, the bid_amount (dollar value) of bonds the this investor would like to buy, and the bid_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], 
     # ...]
    
  2. 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
    
  3. Write the function find_winning_bids(bids, total_offering_fv, c, n, m) that processes a list of bids 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 fraction bid_amount if the entire bid was not filled, or 0 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 called sort(). 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 to import 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:

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 your print statements are inside a function scope or insude the if __name__ == '__main__' control structure.