Module: Xirr::Bonds

Defined in:
lib/xirr/bonds.rb

Overview

Fixed income: pricing a bond, solving for its yield, and the standard risk metrics (Macaulay and modified duration, convexity).

A bond pays a coupon of coupon_rate a year, split across freq payments (semiannual by default), and returns its face value at maturity, years from now. Rates are quoted per year; ytm is the yield to maturity. Settlement is assumed to fall on a coupon date, so prices are clean and the number of coupon periods is whole.

The risk metrics don't depend on the face value (it cancels out), so they omit it and lead with coupon_rate, unlike Bonds.price and Bonds.ytm.

Class Method Summary collapse

Class Method Details

.convexity(coupon_rate, ytm, years, freq = 2, precision: Xirr.config.precision) ⇒ Float

Convexity, in years² — the second-order sensitivity of a bond's price to yield. Pairs with modified duration to refine a price-change estimate.

Returns:

  • (Float)


61
62
63
64
65
# File 'lib/xirr/bonds.rb', line 61

def convexity(coupon_rate, ytm, years, freq = 2, precision: Xirr.config.precision)
  with_metric(coupon_rate, years, freq, precision) do |flows|
    convexity_value(flows, ytm.to_f / freq, freq)
  end
end

.duration(coupon_rate, ytm, years, freq = 2, precision: Xirr.config.precision) ⇒ Float

Macaulay duration — the present-value-weighted average time, in years, until a bond's cash flows are received.

Returns:

  • (Float)


43
44
45
46
47
# File 'lib/xirr/bonds.rb', line 43

def duration(coupon_rate, ytm, years, freq = 2, precision: Xirr.config.precision)
  with_metric(coupon_rate, years, freq, precision) do |flows|
    macaulay(flows, ytm.to_f / freq, freq)
  end
end

.modified_duration(coupon_rate, ytm, years, freq = 2, precision: Xirr.config.precision) ⇒ Float

Modified duration — Macaulay duration divided by +1 + ytm/freq+. Estimates the percentage price change for a small change in yield.

Returns:

  • (Float)


52
53
54
55
56
# File 'lib/xirr/bonds.rb', line 52

def modified_duration(coupon_rate, ytm, years, freq = 2, precision: Xirr.config.precision)
  with_metric(coupon_rate, years, freq, precision) do |flows|
    macaulay(flows, ytm.to_f / freq, freq) / (1 + ytm.to_f / freq)
  end
end

.price(face, coupon_rate, ytm, years, freq = 2, precision: Xirr.config.precision) ⇒ Float

Price of a bond: the present value of its coupons and face value discounted at the yield ytm. Prices at par when the coupon rate equals the yield.

Returns:

  • (Float)


21
22
23
24
25
# File 'lib/xirr/bonds.rb', line 21

def price(face, coupon_rate, ytm, years, freq = 2, precision: Xirr.config.precision)
  n = periods(years, freq)
  value = RtSafe.present_value(bond_flows(face, coupon_rate, n, freq), ytm.to_f / freq)
  round_value(value, precision)
end

.ytm(face, coupon_rate, price, years, freq = 2, guess: 0.05, precision: Xirr.config.precision) ⇒ Float

Yield to maturity: the annual yield that discounts a bond's coupons and face value back to price. The inverse of price; reuses the RtSafe solver.

Returns:

  • (Float)

Raises:

  • (ArgumentError)


31
32
33
34
35
36
37
38
# File 'lib/xirr/bonds.rb', line 31

def ytm(face, coupon_rate, price, years, freq = 2, guess: 0.05, precision: Xirr.config.precision)
  n = periods(years, freq)
  flows = [[0.0, -price * 1.0]] + bond_flows(face, coupon_rate, n, freq)
  periodic = RtSafe.find(flows, guess: guess)
  raise ArgumentError, 'ytm did not converge' if periodic.nil?

  round_value(periodic * freq, precision)
end