Del precio de vuelta al rendimiento
En el mercado un bono se cotiza por su precio, pero los inversores comparan bonos por el rendimiento. El rendimiento al vencimiento (YTM) es la única tasa de descuento con la que el valor actual de todos los flujos del bono iguala su precio: el rendimiento anualizado que obtienes si compras ahora, mantienes hasta el vencimiento y se hacen todos los pagos. Para un bono a 10 años con precio 925,61 y cupón del 5 %, es el 6 %. A diferencia del precio, el YTM no tiene fórmula cerrada, porque la incógnita está dentro de un polinomio de grado alto.
import numpy as np
def bond_price(face, coupon_rate, ytm, years, freq=2):
n = int(round(years * freq))
times = np.arange(1, n + 1) / freq
flows = np.full(n, face * coupon_rate / freq)
flows[-1] += face
return (flows / (1 + ytm / freq) ** (freq * times)).sum()
price = bond_price(1000, 0.05, 0.06, 10)
print(round(price, 4)) # 925.6126 the market price we pretend to observe
# A back-of-the-envelope yield: annual coupon plus the yearly share of the discount, over the average of price and par
coupon = 1000 * 0.05
print(round((coupon + (1000 - price) / 10) / ((1000 + price) / 2), 5)) # 0.05966 close to 6%, but not exactBuscar: bisección
El precio cae de forma continua al subir el rendimiento, así que se puede buscar. Toma un intervalo [low, high] que seguro contenga la respuesta (0 % a 100 %). Valora el bono en el punto medio: si el precio está por encima del objetivo, el rendimiento debe ser mayor, así que sube low al punto medio; si no, baja high. Cada paso divide el intervalo por dos; 40 pasos lo reducen un billón de veces. La bisección es lenta comparada con otros métodos, pero no puede fallar mientras la respuesta esté dentro del intervalo, lo que la hace el recurso seguro.
low, high = 0.0, 1.0
for step in range(6):
mid = (low + high) / 2
price_mid = bond_price(1000, 0.05, mid, 10)
print(step, round(mid, 5), round(price_mid, 2))
if price_mid > price: # priced too high: the yield must be higher
low = mid
else:
high = mid
# 0 0.5 110.38
# 1 0.25 275.86
# 2 0.125 578.47
# 3 0.0625 908.08
# 4 0.03125 1159.97
# 5 0.04688 1024.72 the interval keeps shrinking around 6% (the target price is 925.61)Buscar más rápido: Newton
El método de Newton usa la pendiente. Define f(y) = bond_price(y) - objetivo; queremos f(y) = 0. En la estimación actual, sigue la tangente hasta cero: y_new = y - f(y) / f'(y). La pendiente se estima numéricamente con un paso minúsculo h: (f(y + h) - f(y - h)) / (2h). Cerca de la respuesta el número de dígitos correctos se duplica aproximadamente en cada paso, así que bastan cuatro o cinco pasos donde la bisección necesita cuarenta. Newton puede fallar si se empieza mal o si la pendiente es plana, así que mantén un límite de pasos y recurre a la bisección si no converge.
y, h = 0.05, 1e-6 # start at the coupon rate
for step in range(5):
f = bond_price(1000, 0.05, y) - price
slope = (bond_price(1000, 0.05, y + h) - bond_price(1000, 0.05, y - h)) / (2 * h)
print(step, round(y, 8), round(f, 6))
y = y - f / slope
# 0 0.05 74.387374
# 1 0.05954347 3.245932
# 2 0.05999903 0.006916
# 3 0.06 0.0
# 4 0.06 0.0 converged to 6% after 3 steps# Check the answer by pricing the bond at the yield we found
print(round(bond_price(1000, 0.05, y, 10), 4), round(price, 4)) # 925.6126 925.6126 the same price: the yield is right- El YTM es la tasa de descuento que iguala el valor actual del bono con su precio; no tiene fórmula cerrada, así que se busca.
- Bisección: sigue partiendo
[low, high](precio sobre el objetivo: subelow); lenta pero segura. - Newton:
y = y - f / slope, con pendiente numérica; converge en pocos pasos pero necesita un límite de pasos y un buen inicio.
A continuación: la duración y la convexidad, medir cuán sensible es el precio de un bono al rendimiento.
Lab: qué se califica
- Estimar el rendimiento a ojo con
approx_ytm25 pts - Hallar el rendimiento partiendo el intervalo con
bisect_yield25 pts - Hallar el rendimiento más rápido con
newton_yield25 pts - Nombrar la situación del bono con
classify_price25 pts