Une obligation est un flux de promesses
Une obligation est un prêt découpé en morceaux négociables. L'émetteur promet un coupon fixe à chaque période et la valeur nominale (pair) à l'échéance. Une obligation à 10 ans de valeur nominale 1000 et de coupon 5 % versé deux fois par an promet 25 tous les six mois et 1000 en plus au dernier paiement. Son prix est ce que valent aujourd'hui ces promesses : la valeur actuelle des flux, actualisés au rendement que le marché exige actuellement pour ce risque et cette échéance.
import numpy as np
def bond_cashflows(face, coupon_rate, years, freq):
n = int(round(years * freq)) # number of payments
times = np.arange(1, n + 1) / freq # in years: 0.5, 1.0, 1.5, ...
flows = np.full(n, face * coupon_rate / freq) # a coupon each time
flows[-1] += face # and the face value with the last one
return times, flows
times, flows = bond_cashflows(1000, 0.05, 2, 2) # a 2-year bond, to keep the output short
print(times.tolist()) # [0.5, 1.0, 1.5, 2.0]
print(flows.tolist()) # [25.0, 25.0, 25.0, 1025.0]Prix et rendement évoluent en sens inverse
Actualisez chaque flux à (1 + ytm / freq) ** (freq * time) et additionnez. Quand le rendement égale le taux du coupon, l'obligation vaut exactement sa valeur nominale (elle se négocie au pair). Si les rendements de marché dépassent le coupon, les nouvelles obligations paient davantage que celle-ci, son prix tombe donc sous le pair (une décote) ; si les rendements baissent, elle se négocie avec une prime. C'est le mécanisme du sujet précédent, avec de vraies dates de paiement. Le rendement courant, coupon annuel sur prix, est une mesure rapide mais incomplète car il ignore le gain ou la perte à l'échéance.
def bond_price(face, coupon_rate, ytm, years, freq=2):
times, flows = bond_cashflows(face, coupon_rate, years, freq)
return (flows / (1 + ytm / freq) ** (freq * times)).sum()
for ytm in (0.03, 0.05, 0.06, 0.08):
print(ytm, round(bond_price(1000, 0.05, ytm, 10), 2))
# 0.03 1171.69 yield below the coupon: premium
# 0.05 1000.0 yield = coupon: par
# 0.06 925.61 yield above the coupon: discount
# 0.08 796.15
print(round(50 / bond_price(1000, 0.05, 0.06, 10), 5)) # 0.05402 current yield: 5.40%, not the 6% yield to maturityLes obligations longues réagissent plus
La variation relative du prix pour un mouvement de rendement donné dépend de l'échéance. Les paiements lointains sont actualisés par une puissance élevée, ils sont donc très sensibles au taux. Une obligation à 1 an perd environ 1 % quand les rendements montent d'un point, une à 10 ans environ 7 %, une à 30 ans près de 14 %. C'est pourquoi les investisseurs obligataires parlent de risque de taux et pourquoi les sujets suivants construisent des mesures (rendement à l'échéance, duration) pour le quantifier.
for years in (1, 5, 10, 30):
change = bond_price(1000, 0.05, 0.06, years) / 1000 - 1 # the coupon is 5%, the yield rises to 6%
print(years, round(change, 4))
# 1 -0.0096
# 5 -0.0427
# 10 -0.0744
# 30 -0.1384# The same 6% yield with annual, semiannual and monthly coupons: the quoting convention matters a little
for freq in (1, 2, 12):
print(freq, round(bond_price(1000, 0.05, 0.06, 10, freq), 2))
# 1 926.4
# 2 925.61
# 12 924.94freq) à côté d'un rendement. Les vraies obligations ont aussi un coupon couru entre les dates de coupon et des conventions de décompte des jours, que ce lab ignore en valorisant aux dates de coupon.- Prix = somme de
flux / (1 + ytm / freq) ** (freq * temps); des coupons à chaque période, le nominal avec le dernier. - Rendement = taux du coupon : prix = pair ; rendement au-dessus du coupon : décote ; en dessous : prime. Prix et rendement évoluent en sens inverse.
- Plus l'échéance est longue, plus le prix varie pour un même mouvement de rendement : ce risque de taux est ce que mesure la duration ensuite.
À suivre : le rendement à l'échéance, dans l'autre sens : du prix de marché au rendement.
Lab : ce qui est noté
- Lister les paiements d'une obligation avec
bond_cashflows25 pts - Actualiser les paiements en un prix avec
bond_price25 pts - Comparer le coupon au prix avec
current_yield25 pts - Voir l'effet d'un mouvement de taux avec
price_change25 pts