def test_unstable(self, dtype): """Demonstrates the instability of Hagan West for extreme cases.""" cashflows = [ # 1 year bond with 5% three monthly coupon. np.array([12.5, 12.5, 12.5, 1012.5], dtype=dtype), # 2 year bond with 6% semi-annual coupon. np.array([30, 30, 30, 1030], dtype=dtype), # 3 year bond with 8% semi-annual coupon. np.array([40, 40, 40, 40, 40, 1040], dtype=dtype), # 4 year bond with 3% semi-annual coupon. np.array([15, 15, 15, 15, 15, 15, 15, 1015], dtype=dtype) ] cashflow_times = [ np.array([0.25, 0.5, 0.75, 1.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], dtype=dtype) ] # Computed with discount rates of [5.0, 4.75, 4.53333333, 4.775] # which are 100 times the values in the previous test case. pvs = np.array([ 11.561316110080888, 2.6491572753698067, 3.4340789041846866, 1.28732090544209 ], dtype=dtype) true_discount_rates = np.array([5.0, 4.75, 4.53333333, 4.775], dtype=dtype) # Check failure with default initial rates. results_default = self.evaluate( bond_curve.bond_curve(cashflows, cashflow_times, pvs, maximum_iterations=100, validate_args=True, dtype=dtype)) self.assertFalse(results_default.converged) self.assertTrue(results_default.failed) self.assertFalse(np.isnan(results_default.discount_rates[0])) self.assertTrue(np.isnan(results_default.discount_rates[1])) # It even fails if we underestimate the result even marginally. # However the behaviour is different if we start above the true values. # See next test. results_close = self.evaluate( bond_curve.bond_curve(cashflows, cashflow_times, pvs, initial_discount_rates=true_discount_rates * 0.9999, maximum_iterations=100)) with self.subTest('Converged'): self.assertFalse(results_close.converged) with self.subTest('Failed'): self.assertTrue(results_close.failed) with self.subTest('DiscountRates'): self.assertFalse(np.isnan(results_close.discount_rates[0])) self.assertFalse(np.isnan(results_close.discount_rates[1])) self.assertTrue(np.isnan(results_close.discount_rates[2]))
def test_zero_coupon_raises(self): dtypes = [np.float64, np.float32] for dtype in dtypes: cashflows = [ # 1 year bond with 5% three monthly coupon. np.array([12.5, 12.5, 12.5, 1012.5], dtype=dtype), # 2 year bond with no coupons. This should cause an error. np.array([1030], dtype=dtype), # 3 year bond with 8% semi-annual coupon. np.array([40, 40, 40, 40, 40, 1040], dtype=dtype), # 4 year bond with 3% semi-annual coupon. np.array([15, 15, 15, 15, 15, 15, 15, 1015], dtype=dtype) ] cashflow_times = [ np.array([0.25, 0.5, 0.75, 1.0], dtype=dtype), np.array([2.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], dtype=dtype) ] pvs = np.array([ 999.68155223943393, 1022.322872470043, 1093.9894418810143, 934.20885689015677 ], dtype=dtype) with self.assertRaises(tf.errors.InvalidArgumentError): self.evaluate( bond_curve.bond_curve(cashflows, cashflow_times, pvs))
def test_negative_rates(self): """Checks that method works even if the actual rates are negative.""" dtypes = [np.float64, np.float32] for dtype in dtypes: cashflows = [ # 1 year bond with 5% three monthly coupon. np.array([12.5, 12.5, 12.5, 1012.5], dtype=dtype), # 2 year bond with 6% semi-annual coupon. np.array([30, 30, 30, 1030], dtype=dtype), # 3 year bond with 8% semi-annual coupon. np.array([40, 40, 40, 40, 40, 1040], dtype=dtype), # 4 year bond with 3% semi-annual coupon. np.array([15, 15, 15, 15, 15, 15, 15, 1015], dtype=dtype) ] cashflow_times = [ np.array([0.25, 0.5, 0.75, 1.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], dtype=dtype) ] pvs = np.array( [1029.54933442, 1097.95320227, 1268.65376174, 1249.84175959], dtype=dtype) true_discount_rates = np.array([0.02, 0.01, -0.01, -0.03], dtype=dtype) results = self.evaluate( bond_curve.bond_curve(cashflows, cashflow_times, pvs)) self.assertTrue(results.converged) self.assertFalse(results.failed) np.testing.assert_allclose(results.discount_rates, true_discount_rates, atol=1e-4)
def test_flat_curve(self): """Checks that flat curves work.""" dtypes = [np.float64, np.float32] for dtype in dtypes: cashflows = [ # 1 year bond with 5% three monthly coupon. np.array([12.5, 12.5, 12.5, 1012.5], dtype=dtype), # 2 year bond with 6% semi-annual coupon. np.array([30, 30, 30, 1030], dtype=dtype), # 3 year bond with 8% semi-annual coupon. np.array([40, 40, 40, 40, 40, 1040], dtype=dtype), # 4 year bond with 3% semi-annual coupon. np.array([15, 15, 15, 15, 15, 15, 15, 1015], dtype=dtype) ] cashflow_times = [ np.array([0.25, 0.5, 0.75, 1.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], dtype=dtype) ] # Computed with a flat curve of 15%. pvs = np.array( [906.27355957, 840.6517334, 823.73626709, 635.7076416], dtype=dtype) true_discount_rates = np.array([0.15] * 4, dtype=dtype) results = self.evaluate( bond_curve.bond_curve(cashflows, cashflow_times, pvs)) self.assertTrue(results.converged) self.assertFalse(results.failed) np.testing.assert_allclose(results.discount_rates, true_discount_rates, atol=1e-6)
def test_non_convex(self): """Demonstrates the nonconvexity of Hagan West for extreme cases.""" # This is the same example as the previous one but with different starting # point. dtypes = [np.float64, np.float32] for dtype in dtypes: cashflows = [ # 1 year bond with 5% three monthly coupon. np.array([12.5, 12.5, 12.5, 1012.5], dtype=dtype), # 2 year bond with 6% semi-annual coupon. np.array([30, 30, 30, 1030], dtype=dtype), # 3 year bond with 8% semi-annual coupon. np.array([40, 40, 40, 40, 40, 1040], dtype=dtype), # 4 year bond with 3% semi-annual coupon. np.array([15, 15, 15, 15, 15, 15, 15, 1015], dtype=dtype) ] cashflow_times = [ np.array([0.25, 0.5, 0.75, 1.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], dtype=dtype) ] # Computed with discount rates of [5.0, 4.75, 4.53333333, 4.775] # which are 100 times the values in the previous test case. pvs = np.array([ 11.561316110080888, 2.6491572753698067, 3.4340789041846866, 1.28732090544209 ], dtype=dtype) true_discount_rates = np.array([5.0, 4.75, 4.53333333, 4.775], dtype=dtype) initial_rates = true_discount_rates * 1.01 # Check failure with default initial rates. results = self.evaluate( bond_curve.bond_curve(cashflows, cashflow_times, pvs, initial_discount_rates=initial_rates, maximum_iterations=100, validate_args=True, dtype=dtype)) self.assertTrue(results.converged) self.assertFalse(results.failed) # It converges to a different set of rates. np.testing.assert_allclose( results.discount_rates, [4.9610328, 4.17662715, 2.84038942, 2.38737021], atol=1e-6) # However, the actual bond prices with the returned rates are indeed # correct. implied_pvs = self.evaluate( _compute_pv(cashflows, cashflow_times, results.discount_rates, np.array([1.0, 2.0, 3.0, 4.0], dtype=dtype))) np.testing.assert_allclose(implied_pvs, pvs, rtol=1e-5)
def test_zero_coupon_bond(self): dtypes = [np.float64, np.float32] for dtype in dtypes: cashflows = [ # 6 months bond with no coupons. np.array([1020], dtype=dtype), # 1 year bond with 5% semi-annual coupon. np.array([25, 1025], dtype=dtype), # 2 year bond with 8% annual coupon. np.array([80, 1080], dtype=dtype), # 3 year bond with 3% annual coupon. np.array([30, 30, 1030], dtype=dtype) ] cashflow_times = [ np.array([0.5], dtype=dtype), np.array([0.5, 1.0], dtype=dtype), np.array([1.0, 2.0], dtype=dtype), np.array([1.0, 2.0, 3.0], dtype=dtype) ] pvs = np.array([1000.0, 1000.0, 1000.0, 1000.0], dtype=dtype) # We can calculate discount rates going step-by-step. r1 = -math.log(pvs[0] / cashflows[0][0]) / cashflow_times[0] r2 = -(math.log( (pvs[1] - cashflows[1][0] * math.exp(-r1 * cashflow_times[1][0])) / cashflows[1][1]) / cashflow_times[1][1]) r3 = -(math.log( (pvs[2] - cashflows[2][0] * math.exp(-r2 * cashflow_times[2][0])) / cashflows[2][1]) / cashflow_times[2][1]) r4 = -(math.log( (pvs[3] - cashflows[3][0] * math.exp(-r2 * cashflow_times[3][0]) - cashflows[3][1] * math.exp(-r3 * cashflow_times[3][1])) / cashflows[3][2]) / cashflow_times[3][2]) true_discount_rates = np.array([r1, r2, r3, r4], dtype=dtype) results = self.evaluate( bond_curve.bond_curve(cashflows, cashflow_times, pvs, validate_args=True, dtype=dtype)) self.assertTrue(results.converged) self.assertFalse(results.failed) self.assertEqual(results.iterations, 4) np.testing.assert_allclose(results.discount_rates, true_discount_rates, atol=1e-6) np.testing.assert_allclose(results.times, [0.5, 1.0, 2.0, 3.0], atol=1e-6)
def test_final_cashflow_is_the_largest_error(self, dtype): with self.assertRaises(tf.errors.InvalidArgumentError): self.evaluate( bond_curve.bond_curve(bond_cashflows=[ np.array([12.5, 12.5, 12.5, 1012.5], dtype=dtype), np.array([30.0, 30.0, 30.0, 3.0], dtype=dtype) ], bond_cashflow_times=[ np.array([0.25, 0.5, 0.75, 1.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0], dtype=dtype) ], present_values=np.array([999.0, 1022.0], dtype=dtype), validate_args=True, dtype=dtype))
def test_correctness(self, dtype): cashflows = [ # 1 year bond with 5% three monthly coupon. np.array([12.5, 12.5, 12.5, 1012.5], dtype=dtype), # 2 year bond with 6% semi-annual coupon. np.array([30, 30, 30, 1030], dtype=dtype), # 3 year bond with 8% semi-annual coupon. np.array([40, 40, 40, 40, 40, 1040], dtype=dtype), # 4 year bond with 3% semi-annual coupon. np.array([15, 15, 15, 15, 15, 15, 15, 1015], dtype=dtype) ] cashflow_times = [ np.array([0.25, 0.5, 0.75, 1.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], dtype=dtype) ] pvs = np.array([ 999.68155223943393, 1022.322872470043, 1093.9894418810143, 934.20885689015677 ], dtype=dtype) results = self.evaluate( bond_curve.bond_curve(cashflows, cashflow_times, pvs, validate_args=True, dtype=dtype)) with self.subTest('Times'): np.testing.assert_allclose(results.times, [1.0, 2.0, 3.0, 4.0]) with self.subTest('Converged'): self.assertTrue(results.converged) with self.subTest('NotFailed'): self.assertFalse(results.failed) expected_discount_rates = np.array([5.0, 4.75, 4.53333333, 4.775], dtype=dtype) / 100 expected_discount_factors = np.exp(-expected_discount_rates * [1.0, 2.0, 3.0, 4.0]) with self.subTest('DiscountRates'): np.testing.assert_allclose(results.discount_rates, expected_discount_rates, atol=1e-6) with self.subTest('DiscountFactors'): np.testing.assert_allclose(results.discount_factors, expected_discount_factors, atol=1e-6)
def test_negative_forwards(self, dtype): """Checks that method works if the rates are positive by fwds are not.""" true_discount_rates = np.array([0.12, 0.09, 0.02, 0.01, 0.01318182], dtype=dtype) # Note the implied forward rates for this rate curve are: # [0.12, 0.06, -0.05, -0.01, 0.02] cashflows = [ np.array([1.2, 10.], dtype=dtype), np.array([1.1, 2.2, 1.4, 15.5], dtype=dtype), np.array([1.22, 0.45, 2.83, 96.0], dtype=dtype), np.array([12.33, 9.84, 1.15, 11.87, 0.66, 104.55], dtype=dtype), np.array([5.84, 0.23, 5.23, 114.95], dtype=dtype) ] cashflow_times = [ np.array([0.15, 0.25], dtype=dtype), np.array([0.1, 0.2, 0.4, 0.5], dtype=dtype), np.array([0.22, 0.45, 0.93, 1.0], dtype=dtype), np.array([0.33, 0.84, 0.92, 1.22, 1.45, 1.5], dtype=dtype), np.array([0.43, 0.77, 1.3, 2.2], dtype=dtype) ] pvs = np.array([ 10.88135262, 19.39268844, 98.48426722, 137.91938533, 122.63546542 ], dtype=dtype) results = self.evaluate( bond_curve.bond_curve(cashflows, cashflow_times, pvs, validate_args=True, dtype=dtype)) with self.subTest('Converged'): self.assertTrue(results.converged) with self.subTest('NotFailed'): self.assertFalse(results.failed) with self.subTest('NumIterations'): self.assertEqual(results.iterations, 6) with self.subTest('DiscountRates'): np.testing.assert_allclose(results.discount_rates, true_discount_rates, atol=1e-6) with self.subTest('Times'): np.testing.assert_allclose(results.times, [0.25, 0.5, 1., 1.5, 2.2], atol=1e-6)
def test_cashflow_times_are_strongly_ordered_error(self): dtypes = [np.float64, np.float32] for dtype in dtypes: with self.assertRaises(tf.errors.InvalidArgumentError): self.evaluate( bond_curve.bond_curve(bond_cashflows=[ np.array([12.5, 12.5, 12.5, 1012.5], dtype=dtype), np.array([30.0, 30.0, 30.0, 1030.0], dtype=dtype) ], bond_cashflow_times=[ np.array([0.25, 0.5, 0.75, 1.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 1.5], dtype=dtype) ], present_values=np.array( [999.0, 1022.0], dtype=dtype), validate_args=True, dtype=dtype))
def test_only_zero_coupon_bonds(self, dtype): cashflows = [ # 1 year bond with no coupons. np.array([1010], dtype=dtype), # 2 year bond with no coupons. np.array([1030], dtype=dtype), # 3 year bond with no coupons. np.array([1020], dtype=dtype), # 4 year bond with no coupons. np.array([1040], dtype=dtype) ] cashflow_times = [ np.array([1.0], dtype=dtype), np.array([2.0], dtype=dtype), np.array([3.0], dtype=dtype), np.array([4.0], dtype=dtype) ] true_discount_rates = np.array([0.001, 0.2, 0.03, 0.0], dtype=dtype) pvs = np.array( [(cashflows[i][0] * math.exp(-rate * cashflow_times[i][0])) for i, rate in enumerate(true_discount_rates)], dtype=dtype) results = self.evaluate( bond_curve.bond_curve(cashflows, cashflow_times, pvs, discount_tolerance=1e-6, validate_args=True, dtype=dtype)) with self.subTest('Converged'): self.assertTrue(results.converged) with self.subTest('NotFailed'): self.assertFalse(results.failed) with self.subTest('NumIterations'): self.assertEqual(results.iterations, 1) with self.subTest('DiscountRates'): np.testing.assert_allclose(results.discount_rates, true_discount_rates, atol=1e-6) with self.subTest('ResultTimes'): np.testing.assert_allclose(results.times, [1.0, 2.0, 3.0, 4.0], atol=1e-6)
def test_only_zero_coupon_bonds(self): dtypes = [np.float64, np.float32] for dtype in dtypes: cashflows = [ # 1 year bond with no coupons. np.array([1010], dtype=dtype), # 2 year bond with no coupons. np.array([1030], dtype=dtype), # 3 year bond with no coupons. np.array([1020], dtype=dtype), # 4 year bond with no coupons. np.array([1040], dtype=dtype) ] cashflow_times = [ np.array([1.0], dtype=dtype), np.array([2.0], dtype=dtype), np.array([3.0], dtype=dtype), np.array([4.0], dtype=dtype) ] true_discount_rates = np.array([0.001, 0.2, 0.03, 0.0], dtype=dtype) pvs = np.array( [(cashflows[i][0] * math.exp(-rate * cashflow_times[i][0])) for i, rate in enumerate(true_discount_rates)], dtype=dtype) results = self.evaluate( bond_curve.bond_curve(cashflows, cashflow_times, pvs, validate_args=True, dtype=dtype)) self.assertTrue(results.converged) self.assertFalse(results.failed) self.assertEqual(results.iterations, 1) np.testing.assert_allclose(results.discount_rates, true_discount_rates, atol=1e-6) np.testing.assert_allclose(results.times, [1.0, 2.0, 3.0, 4.0], atol=1e-6)
def test_flat_curve(self, dtype): """Checks that flat curves work.""" cashflows = [ # 1 year bond with 5% three monthly coupon. np.array([12.5, 12.5, 12.5, 1012.5], dtype=dtype), # 2 year bond with 6% semi-annual coupon. np.array([30, 30, 30, 1030], dtype=dtype), # 3 year bond with 8% semi-annual coupon. np.array([40, 40, 40, 40, 40, 1040], dtype=dtype), # 4 year bond with 3% semi-annual coupon. np.array([15, 15, 15, 15, 15, 15, 15, 1015], dtype=dtype) ] cashflow_times = [ np.array([0.25, 0.5, 0.75, 1.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0], dtype=dtype), np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], dtype=dtype) ] # Computed with a flat curve of 15%. pvs = np.array([906.27355957, 840.6517334, 823.73626709, 635.7076416], dtype=dtype) true_discount_rates = np.array([0.15] * 4, dtype=dtype) results = self.evaluate( bond_curve.bond_curve(cashflows, cashflow_times, pvs, discount_tolerance=1e-6, validate_args=True, dtype=dtype)) with self.subTest('Converged'): self.assertTrue(results.converged) with self.subTest('NotFailed'): self.assertFalse(results.failed) with self.subTest('DiscountRates'): np.testing.assert_allclose(results.discount_rates, true_discount_rates, atol=1e-6)