def test_coupon_on_date(self): initial_notional = 1000 coup1_start_date = date(2017, 12, 8) coup1_date = date(2018, 3, 9) # note start coupon date != prev coupon date! due to holidays. # but coupon value disregards this start date and takes it from # prev. coupon date coup2_start_date = date(2018, 3, 12) coup2_date = date(2018, 6, 8) coupons = [ CouponScheduleEntry(coup1_date, None, coup1_start_date, 0.0, 11.7), CouponScheduleEntry(coup2_date, None, coup2_start_date, 0.0, 11.7) ] amortizations = [ AmortizationScheduleEntry(date(2018, 1, 1), 30.0, 300.0), AmortizationScheduleEntry(coup2_date, 70.0, 700.0) ] b = Bond(initial_notional=initial_notional, coupons=coupons, amortizations=amortizations) notional_on_coup2 = b.notional_on_date(coup2_date) assert notional_on_coup2 == 700.0 # note we find days between coupon date 1 & 2, not from coupon 2 start/end date, because otherwise they # won't match due to different day counts due to holidays coupon_days = (coup2_date - coup1_date).days expected_coupon1 = round( (coupon_days / YEAR_BASE) * (coupons[0].yearly_prc / 100.0) * notional_on_coup2, 2) coupon_days_wrong = (coup2_date - coupons[1].start_date).days expected_coupon1_wrong = round( (coupon_days_wrong / YEAR_BASE) * (coupons[0].yearly_prc / 100.0) * notional_on_coup2, 2) assert b.accrued_coupon_on_date(coup2_date) == pytest.approx( expected_coupon1) assert expected_coupon1 != pytest.approx(expected_coupon1_wrong)
def test_cannot_create_bond_with_not_increasing_amort_dates(self): with pytest.raises(ValueError): amortizations = [ AmortizationScheduleEntry(date(2019, 9, 6), 30.0, 300.0), # note date goes before first date, this is wrong AmortizationScheduleEntry(date(2019, 9, 3), 70.0, 700.0) ] Bond(coupons=[], amortizations=amortizations)
def test_payments_since_date(self): coup1_start_date = date(2017, 12, 8) coup1_date = date(2018, 3, 9) coup2_start_date = date(2018, 3, 12) coup2_date = date(2018, 6, 8) coupons = [ CouponScheduleEntry(coup1_date, None, coup1_start_date, 0.0, 11.7), CouponScheduleEntry(coup2_date, None, coup2_start_date, 0.0, 11.7) ] amortizations = [ AmortizationScheduleEntry(date(2018, 1, 1), 30.0, 300.0), AmortizationScheduleEntry(coup2_date, 70.0, 700.0) ] b = Bond(coupons=coupons, amortizations=amortizations) cps, ams = b.payments_since_date(date(2018, 5, 8)) assert cps == [coupons[1]] assert ams == [amortizations[1]]
def test_cannot_create_bond_with_not_increasing_coupon_dates(self): with pytest.raises(ValueError): amortizations = [ AmortizationScheduleEntry(date(2019, 9, 3), 100.0, 1000.0) ] coupons = [ CouponScheduleEntry(date(2018, 1, 1), None, date(2017, 1, 1), 10.0, 10.0), # note date goes before first date, this is wrong CouponScheduleEntry(date(2017, 12, 1), None, date(2017, 11, 30), 10.0, 10.0) ] Bond(coupons=coupons, amortizations=amortizations)
def test_ytm(self): coup1_start_date = date(2017, 3, 20) coup1_date = date(2018, 3, 20) coup2_start_date = date(2018, 3, 21) coup2_date = date(2019, 3, 20) coupons = [ CouponScheduleEntry(coup1_date, None, coup1_start_date, 100.0, 0.0), CouponScheduleEntry(coup2_date, None, coup2_start_date, 100.0, 0.0) ] amortizations = [AmortizationScheduleEntry(coup2_date, 100.0, 1000.0)] b = Bond(coupons=coupons, amortizations=amortizations) # assume we buy slightly after last coupon period start buy_price = 103.0 buy_date = date(2018, 5, 20) ytm = b.yield_to_maturity(buy_price, buy_date, date(2018, 5, 21)) assert ytm == pytest.approx(0.0821) # assert ytm definition holds yf = ((coup2_date - buy_date).days / YEAR_BASE) accrued = b.accrued_coupon_on_date(buy_date) fv_initial_investment = (buy_price * 10 + accrued) * (1.0 + ytm)**yf # no need to accrue since they already are on correct date fv_payments = coupons[1].value + amortizations[0].value assert fv_initial_investment == pytest.approx(fv_payments, rel=5E-5)
def parse_coupon_schedule_xml(data: str) -> Bond: root = ET.fromstring(data) first_row = next(root.iter("row")) isin = first_row.get("isin") name = first_row.get("name") # note: reply rows contain "facevalue" but it's incorrect, it's "current facevalue" initial_notional = float(first_row.get("initialfacevalue")) notional_ccy = first_row.get("faceunit") am_schedule = [ __parse_am_entry(am_entry) for am_entry in root.findall(".//data[@id='amortizations']//row") ] cp_schedule = [ __parse_coupon_entry(cp_entry) for cp_entry in root.findall(".//data[@id='coupons']//row") ] return Bond(isin=isin, name=name, initial_notional=initial_notional, notional_ccy=notional_ccy, coupons=cp_schedule, amortizations=am_schedule)
def test_notional_on_date(self): initial_notional = 1000 am_dt1 = date(2019, 9, 6) am_dt2 = date(2020, 9, 4) am_dt3 = date(2021, 9, 3) amortizations = [ AmortizationScheduleEntry(am_dt1, 30.0, 300.0), AmortizationScheduleEntry(am_dt2, 30.0, 300.0), AmortizationScheduleEntry(am_dt3, 40.0, 400.0) ] b = Bond(initial_notional=initial_notional, coupons=[], amortizations=amortizations) # date before first amortization - notional should equal initial notional one_day = timedelta(days=1) assert b.notional_on_date(am_dt1 - one_day) == initial_notional # on amortization date - notional still stays the same, it affects only next coupons assert b.notional_on_date(am_dt1) == initial_notional notional_after_am_dt1 = initial_notional * ( 1 - amortizations[0].value_prc / 100.0) assert b.notional_on_date(am_dt1 + one_day) == pytest.approx( notional_after_am_dt1) assert b.notional_on_date(am_dt2 - one_day) == notional_after_am_dt1 assert b.notional_on_date(am_dt2) == notional_after_am_dt1 notional_after_am_dt2 = initial_notional * ( 1 - (amortizations[0].value_prc + amortizations[0].value_prc) / 100.0) assert b.notional_on_date(am_dt2 + one_day) == pytest.approx( notional_after_am_dt2) assert b.notional_on_date(am_dt3 - one_day) == notional_after_am_dt2 assert b.notional_on_date(am_dt3) == notional_after_am_dt2 # last amortization date is settlement date notional_after_settlement = 0.0 assert b.notional_on_date(am_dt3 + one_day) == pytest.approx( notional_after_settlement)
def test_cannot_create_bond_with_not_fully_amortized_notional(self): with pytest.raises(ValueError): amortizations = [ AmortizationScheduleEntry(date(2019, 9, 3), 100.0, 999.0) ] Bond(coupons=[], amortizations=amortizations)