Class: Istox::Quant::Bond

Inherits:
Object
  • Object
show all
Defined in:
lib/istox/quant/bond.rb

Constant Summary collapse

DEFAULT_APPROXIMATION_ERROR =
0.00001

Instance Method Summary collapse

Constructor Details

#initialize(coupon: nil, maturity_date: nil, start_date: nil, coupon_frequency: nil, coupon_payment_dates: nil, face_value: 100, days_of_year: 365) ⇒ Bond

Returns a new instance of Bond.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/istox/quant/bond.rb', line 15

def initialize(coupon: nil, maturity_date: nil, start_date: nil, coupon_frequency: nil, coupon_payment_dates: nil, face_value: 100, days_of_year: 365)
  raise "Invalid coupon #{coupon}" if (coupon.nil? || !is_number?(coupon)) || coupon < 0
  raise "Invalid maturity_date #{maturity_date}" if (maturity_date.nil? || maturity_date.methods.include?("strftime"))
  raise "Invalid start_date #{start_date}" if (start_date.nil? || start_date.methods.include?("strftime"))
  raise "Invalid coupon_frequency #{coupon_frequency}" if (coupon_frequency.nil? || !coupon_frequency.is_a?(Integer) || coupon_frequency < 0)
  raise "Invalid coupon_payment_dates #{coupon_payment_dates}" if (coupon_payment_dates.nil? || (coupon_payment_dates.count == 0 && coupon_frequency != 0) || coupon_payment_dates.any? { |date| date > maturity_date.to_date })
  raise "Invalid days_of_year #{days_of_year}" if (days_of_year != 365 && days_of_year != 360)
  raise "start_date is not before maturity_date" if start_date>=maturity_date

  @coupon               = coupon.to_d
  @maturity_date        = maturity_date.to_date
  @coupon_frequency     = coupon_frequency.to_i # if this is 0, it means zero coupon
  @days_of_year         = days_of_year.to_d
  @face_value           = face_value.to_d
  @coupon_payment_dates = coupon_payment_dates.map(&:to_date).uniq.sort
  # note here we work out the start date based on maturity date and nunber of years
  @start_date           = start_date.to_date

  @pay_accrued_interest = false
  @coupon_payment_dates_include_accrued_interest = false
  if !is_zero_coupon?
    if @coupon_payment_dates.include?(@maturity_date)
      # maturity date is a coupon payment date, check if this should
      # be accrued interest or last normal coupon
      if @coupon_payment_dates.count > 1
        previous_coupon_date = @coupon_payment_dates[@coupon_payment_dates.count-2]
        next_coupon_date = add_month(previous_coupon_date, -(12/@coupon_frequency).to_i)
        # If maturity date is a normal coupon payment, the theorecical next_coupon_date
        # calculated from previous coupon payment should be the maturity date, to be safe,
        # we allow 3 days difference
        if (next_coupon_date - @maturity_date).abs <= 3
          @pay_accrued_interest = false
          @coupon_payment_dates_include_accrued_interest = false
        else
          @pay_accrued_interest = true
          @coupon_payment_dates_include_accrued_interest = true
        end
      else
        # maturity date is only coupon payment date, shouldn't be accrued interest!
        @pay_accrued_interest = false
        @coupon_payment_dates_include_accrued_interest = false
      end
    else
      # maturity date is not included in coupon payment date, consider 
      # this needs to pay accrued interest
      @pay_accrued_interest = true
      @coupon_payment_dates_include_accrued_interest = false
    end
  end

  log.info "Bond info: start_date=#{@start_date} maturity_date=#{@maturity_date} days_of_years=#{days_of_year} coupon=#{@coupon} coupon_frequency=#{coupon_frequency} face_value=#{@face_value} coupon_payment_dates=#{@coupon_payment_dates} pay_accrued_interest=#{@pay_accrued_interest} coupon_payment_dates_include_accrued_interest=#{@coupon_payment_dates_include_accrued_interest}"
end

Instance Method Details

#price(ytm, date, ex_coupon_date: nil, fees: 0) ⇒ Object



68
69
70
71
72
73
# File 'lib/istox/quant/bond.rb', line 68

def price(ytm, date, ex_coupon_date: nil, fees: 0)
  irr = ytm
  irr = ytm/@coupon_frequency if !is_zero_coupon?
  price = price_for_irr(irr, date, ex_coupon_date: ex_coupon_date, fees: fees)
  price
end

#ytm(date, ex_coupon_date: nil, price: 100, fees: 0, approximation_error: DEFAULT_APPROXIMATION_ERROR) ⇒ Object



75
76
77
78
79
80
81
82
83
# File 'lib/istox/quant/bond.rb', line 75

def ytm(date, ex_coupon_date: nil, price: 100, fees: 0, approximation_error: DEFAULT_APPROXIMATION_ERROR)
  if !is_zero_coupon? && price == @face_value && date <= @start_date
    # for non zero coupon bond, if price is face value and date is at or before start date
    # YTM is simply the coupon rate
    return @coupon
  end
  ytm_down, ytm_up = ytm_limits(price, date, ex_coupon_date: ex_coupon_date, fees: fees)
  approximate_ytm(ytm_down, ytm_up, price, date, ex_coupon_date: ex_coupon_date, fees: fees, approximation_error: approximation_error)
end