Class: Finrb::Amortization

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

Overview

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 = Finrb::Amortization.new(250000, 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 = Finrb::Amortization.new(250000, rate){ |period| period.payment - 150 }

Defined Under Namespace

Classes: Entry

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(principal, *rates, balloon: 0, interest_only_periods: 0, origination_fee: 0, finance_origination_fee: false, &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)
  • (number)
  • (Rate)
  • balloon: (number) (defaults to: 0)
  • interest_only_periods: (Integer) (defaults to: 0)
  • origination_fee: (number) (defaults to: 0)
  • finance_origination_fee: (Boolean) (defaults to: false)

Raises:

  • (ArgumentError)


117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/finrb/amortization.rb', line 117

def initialize(principal, *rates, balloon: 0, interest_only_periods: 0, origination_fee: 0, finance_origination_fee: false, &block)
  @principal = Validation.positive_decimal(principal, name: 'principal', message: 'principal must be positive.')

  @origination_fee = Validation.non_negative_decimal(origination_fee, name: 'origination fee')
  raise(ArgumentError, 'finance_origination_fee must be true or false.') unless [true, false].include?(finance_origination_fee)
  raise(ArgumentError, 'an unfinanced origination_fee must be less than principal.') if !finance_origination_fee && @origination_fee >= @principal

  @finance_origination_fee = finance_origination_fee
  @amount_financed = @principal + (finance_origination_fee ? @origination_fee : 0)
  @net_proceeds = @principal - (finance_origination_fee ? 0 : @origination_fee)

  @balloon = Validation.decimal(balloon, name: 'balloon')
  raise(ArgumentError, 'balloon must be non-negative and less than amount financed.') if @balloon.negative? || @balloon >= @amount_financed
  raise(ArgumentError, 'at least one rate is required.') if rates.empty?
  raise(ArgumentError, 'rates must be Finrb::Rate instances.') unless rates.all?(Rate)
  raise(ArgumentError, 'every rate must have a duration.') if rates.any? { |rate| rate.duration.nil? }

  @rates     = rates
  @block     = block

  # compute the total duration from all of the rates.
  @periods = rates.sum(&:duration)
  valid_interest_only = interest_only_periods.is_a?(Integer) && interest_only_periods.between?(0, @periods - 1)
  raise(ArgumentError, 'interest_only_periods must be a non-negative integer shorter than the loan term.') unless valid_interest_only

  @interest_only_periods = interest_only_periods
  @period = 0

  compute
end

Instance Attribute Details

#amount_financedFlt::DecNum (readonly)

Returns principal balance including any financed origination fee.

Returns:

  • (Flt::DecNum)

    principal balance including any financed origination fee



67
68
69
# File 'lib/finrb/amortization.rb', line 67

def amount_financed
  @amount_financed
end

#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)



63
64
65
# File 'lib/finrb/amortization.rb', line 63

def balance
  @balance
end

#balloonFlt::DecNum (readonly)

Returns contractual principal settled as a balloon in the final period.

Returns:

  • (Flt::DecNum)

    contractual principal settled as a balloon in the final period



65
66
67
# File 'lib/finrb/amortization.rb', line 65

def balloon
  @balloon
end

#finance_origination_feeBoolean (readonly) Also known as: finance_origination_fee?

Returns the value of attribute finance_origination_fee.

Returns:

  • (Boolean)


155
156
157
# File 'lib/finrb/amortization.rb', line 155

def finance_origination_fee
  @finance_origination_fee
end

#interest_only_periodsInteger (readonly)

Returns number of leading periods that pay interest but no scheduled principal.

Returns:

  • (Integer)

    number of leading periods that pay interest but no scheduled principal



73
74
75
# File 'lib/finrb/amortization.rb', line 73

def interest_only_periods
  @interest_only_periods
end

#net_proceedsFlt::DecNum (readonly)

Returns cash made available to the borrower after an unfinanced fee.

Returns:

  • (Flt::DecNum)

    cash made available to the borrower after an unfinanced fee



69
70
71
# File 'lib/finrb/amortization.rb', line 69

def net_proceeds
  @net_proceeds
end

#origination_feeFlt::DecNum (readonly)

Returns fee charged when the loan is originated.

Returns:

  • (Flt::DecNum)

    fee charged when the loan is originated



71
72
73
# File 'lib/finrb/amortization.rb', line 71

def origination_fee
  @origination_fee
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



75
76
77
# File 'lib/finrb/amortization.rb', line 75

def payment
  @payment
end

#principalFlt::DecNum (readonly)

Returns the principal amount of the loan.

Returns:



77
78
79
# File 'lib/finrb/amortization.rb', line 77

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



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

def rates
  @rates
end

#scheduleArray<Entry> (readonly)

Returns immutable period-by-period loan breakdown.

Returns:

  • (Array<Entry>)

    immutable period-by-period loan breakdown



81
82
83
# File 'lib/finrb/amortization.rb', line 81

def schedule
  @schedule
end

Class Method Details

.payment(principal, rate, periods, balloon: 0) ⇒ 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

  • (number)
  • (number)
  • (Integer)
  • balloon: (number) (defaults to: 0)

Returns:

Raises:

  • (ArgumentError)

See Also:



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/finrb/amortization.rb', line 93

def self.payment(principal, rate, periods, balloon: 0)
  principal = Validation.positive_decimal(principal, name: 'principal', message: 'principal must be positive.')

  balloon = Validation.decimal(balloon, name: 'balloon')
  raise(ArgumentError, 'balloon must be non-negative and no greater than principal.') unless balloon.between?(0, principal)

  rate = Validation.decimal_greater_than(rate, minimum: -1, name: 'periodic rate')

  periods = Validation.positive_integer(periods, name: 'period count')

  if rate.zero?
    # simplified formula to avoid division-by-zero when interest rate is zero
    -Precision.money((principal - balloon) / periods)
  else
    growth = (rate + 1)**periods
    -Precision.money(((principal * growth) - balloon) * rate / (growth - 1))
  end
end

Instance Method Details

#==(other) ⇒ Numeric

compare two Amortization instances

Parameters:

Returns:



151
152
153
# File 'lib/finrb/amortization.rb', line 151

def ==(other)
  (principal == other.principal) && (origination_fee == other.origination_fee) && (finance_origination_fee? == other.finance_origination_fee?) && (balloon == other.balloon) && (interest_only_periods == other.interest_only_periods) && (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 = Finrb::Amortization.new(300000, 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



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

def additional_payments
  @transactions.filter_map { |trans| trans.difference if trans.payment? }
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 = Finrb::Amortization.new(300000, rate)
amt.duration #=> 360

Extra payments may reduce the duration

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

Returns:

  • (Integer)

    the time required to pay off the loan, in months



238
239
240
# File 'lib/finrb/amortization.rb', line 238

def duration
  payments.length
end

#inspectString

Returns:

  • (String)


242
243
244
# File 'lib/finrb/amortization.rb', line 242

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 = Finrb::Amortization.new(300000, 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 = Finrb::Amortization.new(300000, rate)
amt.interest[0,6].sum #=> Flt::DecNum('5603.74')

Returns:

  • (Array)

    the amount of interest charged in each period



255
256
257
# File 'lib/finrb/amortization.rb', line 255

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 = Finrb::Amortization.new(300000, rate)
amt.payments.sum #=> Flt::DecNum('-500163.94')

Returns:

  • (Array)

    the amount of the payment in each period



264
265
266
# File 'lib/finrb/amortization.rb', line 264

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