Class: Finrb::Amortization

Inherits:
Object
  • Object
show all
Defined in:
lib/finrb/amortization.rb

Overview

Note:

There are two ways to create an amortization. The first example uses the amortize method for the Numeric class. The second calls Amortization.new directly.

the Amortization class provides an interface for working with loan amortizations.

Examples:

Borrow $250,000 under a 30 year, fixed-rate loan with a 4.25% APR

rate = Rate.new(0.0425, :apr, :duration => (30 * 12))
amortization = 250000.amortize(rate)

Borrow $250,000 under a 30 year, adjustable rate loan, with an APR starting at 4.25%, and increasing by 1% every five years

values = %w{ 0.0425 0.0525 0.0625 0.0725 0.0825 0.0925 }
rates = values.collect { |value| Rate.new( value, :apr, :duration = (5 * 12) ) }
arm = Amortization.new(250000, *rates)

Borrow $250,000 under a 30 year, fixed-rate loan with a 4.25% APR, but pay $150 extra each month

rate = Rate.new(0.0425, :apr, :duration => (5 * 12))
extra_payments = 250000.amortize(rate){ |period| period.payment - 150 }

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(principal, *rates, &block) ⇒ Amortization

create a new Amortization instance

Parameters:

  • principal (Flt::DecNum)

    the initial amount of the loan or investment

  • rates (Rate)

    the applicable interest rates

  • block (Proc)


63
64
65
66
67
68
69
70
71
72
73
# File 'lib/finrb/amortization.rb', line 63

def initialize(principal, *rates, &block)
  @principal = Flt::DecNum.new(principal.to_s)
  @rates     = rates
  @block     = block

  # compute the total duration from all of the rates.
  @periods = rates.sum(&:duration)
  @period  = 0

  compute
end

Instance Attribute Details

#balanceFlt::DecNum (readonly)

Returns the balance of the loan at the end of the amortization period (usually zero).

Returns:

  • (Flt::DecNum)

    the balance of the loan at the end of the amortization period (usually zero)



26
27
28
# File 'lib/finrb/amortization.rb', line 26

def balance
  @balance
end

#paymentFlt::DecNum (readonly)

Returns the required monthly payment. For loans with more than one rate, returns nil.

Returns:

  • (Flt::DecNum)

    the required monthly payment. For loans with more than one rate, returns nil



29
30
31
# File 'lib/finrb/amortization.rb', line 29

def payment
  @payment
end

#principalFlt::DecNum (readonly)

Returns the principal amount of the loan.

Returns:

  • (Flt::DecNum)

    the principal amount of the loan



32
33
34
# File 'lib/finrb/amortization.rb', line 32

def principal
  @principal
end

#ratesArray (readonly)

Returns the interest rates used for calculating the amortization.

Returns:

  • (Array)

    the interest rates used for calculating the amortization



35
36
37
# File 'lib/finrb/amortization.rb', line 35

def rates
  @rates
end

Class Method Details

.payment(principal, rate, periods) ⇒ Flt::DecNum

Note:

in most cases, you will probably want to use rate.monthly when calling this function outside of an Amortization instance.

Returns the periodic payment due on a loan.

Examples:

rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
rate.duration #=> 360
Amortization.payment(200000, rate.monthly, rate.duration) #=> Flt::DecNum('-926.23')

Parameters:

  • principal (Flt::DecNum)

    the initial amount of the loan or investment

  • rate (Rate)

    the applicable interest rate (per period)

  • periods (Integer)

    the number of periods needed for repayment

Returns:

  • (Flt::DecNum)

    the periodic payment due on a loan

See Also:



48
49
50
51
52
53
54
55
# File 'lib/finrb/amortization.rb', line 48

def self.payment(principal, rate, periods)
  if rate.zero?
    # simplified formula to avoid division-by-zero when interest rate is zero
    -(principal / periods).round(2)
  else
    -(principal * (rate + (rate / (((rate + 1)**periods) - 1)))).round(2)
  end
end

Instance Method Details

#==(other) ⇒ Numeric

compare two Amortization instances

Parameters:

Returns:



79
80
81
# File 'lib/finrb/amortization.rb', line 79

def ==(other)
  (principal == other.principal) && (rates == other.rates) && (payments == other.payments)
end

#additional_paymentsArray

Returns the amount of any additional payments in each period.

Examples:

rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
amt = 300000.amortize(rate){ |payment| payment.amount-100}
amt.additional_payments #=> [Flt::DecNum('-100.00'), Flt::DecNum('-100.00'), ... ]

Returns:

  • (Array)

    the amount of any additional payments in each period



89
90
91
# File 'lib/finrb/amortization.rb', line 89

def additional_payments
  @transactions.filter_map { |trans| trans.difference if trans.payment? }
end

#amortize(rate) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

amortize the balance of loan with the given interest rate

Parameters:

  • rate (Rate)

    the interest rate to use in the amortization

Returns:

  • none



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/finrb/amortization.rb', line 97

def amortize(rate)
  # For the purposes of calculating a payment, the relevant time
  # period is the remaining number of periods in the loan, not
  # necessarily the duration of the rate itself.
  periods = @periods - @period
  amount = Amortization.payment(@balance, rate.monthly, periods)

  pmt = Payment.new(amount, period: @period)
  pmt.modify(&@block) if @block

  rate.duration.to_i.times do
    # Do this first in case the balance is zero already.
    break if @balance.zero?

    # Compute and record interest on the outstanding balance.
    int = (@balance * rate.monthly).round(2)
    interest = Interest.new(int, period: @period)
    @balance += interest.amount
    @transactions << interest.dup

    # Record payment.  Don't pay more than the outstanding balance.
    pmt.amount = -@balance if pmt.amount.abs > @balance
    @transactions << pmt.dup
    @balance += pmt.amount

    @period += 1
  end
end

#computeObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

compute the amortization of the principal

Returns:

  • none



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/finrb/amortization.rb', line 129

def compute
  @balance = @principal
  @transactions = []

  @rates.each do |rate|
    amortize(rate)
  end

  # Add any remaining balance due to rounding error to the last payment.
  if @balance.nonzero?
    @transactions.reverse.find(&:payment?).amount -= @balance
    @balance = 0
  end

  @payment = (payments[0] if @rates.length == 1)

  @transactions.freeze
end

#durationInteger

Returns the time required to pay off the loan, in months.

Examples:

In most cases, the duration is equal to the total duration of all rates

rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
amt = 300000.amortize(rate)
amt.duration #=> 360

Extra payments may reduce the duration

rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
amt = 300000.amortize(rate){ |payment| payment.amount-100}
amt.duration #=> 319

Returns:

  • (Integer)

    the time required to pay off the loan, in months



158
159
160
# File 'lib/finrb/amortization.rb', line 158

def duration
  payments.length
end

#inspectObject



163
164
165
# File 'lib/finrb/amortization.rb', line 163

def inspect
  "Amortization.new(#{@principal})"
end

#interestArray

Returns the amount of interest charged in each period.

Examples:

find the total cost of interest for a loan

rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
amt = 300000.amortize(rate)
amt.interest.sum #=> Flt::DecNum('200163.94')

find the total interest charges in the first six months

rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
amt = 300000.amortize(rate)
amt.interest[0,6].sum #=> Flt::DecNum('5603.74')

Returns:

  • (Array)

    the amount of interest charged in each period



177
178
179
# File 'lib/finrb/amortization.rb', line 177

def interest
  @transactions.filter_map { |trans| trans.amount if trans.interest? }
end

#paymentsArray

Returns the amount of the payment in each period.

Examples:

find the total payments for a loan

rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
amt = 300000.amortize(rate)
amt.payments.sum #=> Flt::DecNum('-500163.94')

Returns:

  • (Array)

    the amount of the payment in each period



187
188
189
# File 'lib/finrb/amortization.rb', line 187

def payments
  @transactions.filter_map { |trans| trans.amount if trans.payment? }
end