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_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_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 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)
def __parse_am_entry(am_entry) -> AmortizationScheduleEntry: str_date = am_entry.get("amortdate") am_date = datetime.date.fromisoformat(str_date) value_prc = float(am_entry.get("valueprc")) value = float(am_entry.get("value")) return AmortizationScheduleEntry(am_date, value_prc, value)
def test_can_parse_bond(self, sample_bond_xml: str): bond = m.parse_coupon_schedule_xml(sample_bond_xml) assert bond.isin == "RU000A0JWSQ7" assert bond.name == "Мордовия 34003 обл." assert bond.initial_notional == 1000 assert bond.notional_ccy == "RUB" assert bond.coupons == [ CouponScheduleEntry(date(2016, 12, 9), None, date(2016, 9, 9), 29.17, 11.7), CouponScheduleEntry(date(2017, 3, 10), None, date(2016, 12, 9), 29.17, 11.7), CouponScheduleEntry(date(2017, 6, 9), None, date(2017, 3, 10), 29.17, 11.7), CouponScheduleEntry(date(2017, 9, 8), None, date(2017, 6, 9), 29.17, 11.7), CouponScheduleEntry(date(2017, 12, 8), date(2017, 12, 7), date(2017, 9, 8), 29.17, 11.7), CouponScheduleEntry(date(2018, 3, 9), date(2018, 3, 7), date(2017, 12, 8), 29.17, 11.7), CouponScheduleEntry( date(2018, 6, 8), date(2018, 6, 7), # note start coupon date != prev coupon date! due to holidays. # but coupon value disregards this start date and takes it from # prev. coupon date date(2018, 3, 12), 29.17, 11.7), CouponScheduleEntry(date(2018, 9, 7), date(2018, 9, 6), date(2018, 6, 8), 29.17, 11.7), CouponScheduleEntry(date(2018, 12, 7), date(2018, 12, 6), date(2018, 9, 7), 29.17, 11.7), CouponScheduleEntry(date(2019, 3, 8), date(2019, 3, 7), date(2018, 12, 7), 29.17, 11.7), CouponScheduleEntry(date(2019, 6, 7), date(2019, 6, 6), date(2019, 3, 8), 29.17, 11.7), CouponScheduleEntry(date(2019, 9, 6), date(2019, 9, 5), date(2019, 6, 7), 29.17, 11.7), CouponScheduleEntry(date(2019, 12, 6), date(2019, 12, 5), date(2019, 9, 6), 20.42, 11.7), CouponScheduleEntry(date(2020, 3, 6), date(2020, 3, 5), date(2019, 12, 6), 20.42, 11.7), CouponScheduleEntry(date(2020, 6, 5), date(2020, 6, 4), date(2020, 3, 6), 20.42, 11.7), CouponScheduleEntry(date(2020, 9, 4), date(2020, 9, 3), date(2020, 6, 5), 20.42, 11.7), CouponScheduleEntry(date(2020, 12, 4), date(2020, 12, 3), date(2020, 9, 4), 11.67, 11.7), CouponScheduleEntry(date(2021, 3, 5), date(2021, 3, 4), date(2020, 12, 4), 11.67, 11.7), CouponScheduleEntry(date(2021, 6, 4), date(2021, 6, 3), date(2021, 3, 5), 11.67, 11.7), CouponScheduleEntry(date(2021, 9, 3), date(2021, 9, 2), date(2021, 6, 4), 11.67, 11.7) ] assert bond.amortizations == [ AmortizationScheduleEntry(date(2019, 9, 6), 30.0, 300.0), AmortizationScheduleEntry(date(2020, 9, 4), 30.0, 300.0), AmortizationScheduleEntry(date(2021, 9, 3), 40.0, 400.0) ] # assert our calculations match those from exchange for coupon in bond.coupons: try: assert coupon.value == bond.accrued_coupon_on_date( coupon.coupon_date) except: print(coupon) raise