finrb 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,20 +18,67 @@ module Finrb
18
18
  # @example Borrow $250,000 under a 30 year, fixed-rate loan with a 4.25% APR, but pay $150 extra each month
19
19
  # rate = Rate.new(0.0425, :apr, :duration => (5 * 12))
20
20
  # extra_payments = Finrb::Amortization.new(250000, rate){ |period| period.payment - 150 }
21
- # @api public
22
21
  class Amortization
22
+ # Immutable breakdown of one amortization period. Payments retain finrb's
23
+ # cashflow sign convention and are negative; the other monetary fields are
24
+ # non-negative.
25
+ class Entry
26
+ ATTRIBUTES = %i[period opening_balance payment interest principal additional_payment balloon_payment interest_only closing_balance].freeze
27
+ MONETARY_ATTRIBUTES = ATTRIBUTES - %i[period interest_only]
28
+ private_constant :ATTRIBUTES, :MONETARY_ATTRIBUTES
29
+
30
+ attr_reader(*ATTRIBUTES)
31
+
32
+ def initialize(period:, opening_balance:, payment:, interest:, principal:, additional_payment:, balloon_payment:, interest_only:, closing_balance:)
33
+ raise(ArgumentError, 'period must be a non-negative integer.') unless period.is_a?(Integer) && !period.negative?
34
+ raise(ArgumentError, 'interest_only must be true or false.') unless [true, false].include?(interest_only)
35
+
36
+ @period = period
37
+ @interest_only = interest_only
38
+ MONETARY_ATTRIBUTES.each do |name|
39
+ value = binding.local_variable_get(name)
40
+ instance_variable_set("@#{name}", Validation.decimal(value, name: name.to_s.tr('_', ' ')))
41
+ end
42
+ freeze
43
+ end
44
+
45
+ def ==(other)
46
+ other.instance_of?(self.class) && ATTRIBUTES.all? { |name| public_send(name) == other.public_send(name) }
47
+ end
48
+ alias eql? ==
49
+
50
+ def hash
51
+ attributes = ATTRIBUTES.map { |name| public_send(name) }
52
+ attributes.hash
53
+ end
54
+
55
+ def to_h
56
+ ATTRIBUTES.to_h { |name| [name, public_send(name)] }
57
+ end
58
+
59
+ alias interest_only? interest_only
60
+ end
61
+
23
62
  # @return [Flt::DecNum] the balance of the loan at the end of the amortization period (usually zero)
24
- # @api public
25
63
  attr_reader :balance
64
+ # @return [Flt::DecNum] contractual principal settled as a balloon in the final period
65
+ attr_reader :balloon
66
+ # @return [Flt::DecNum] principal balance including any financed origination fee
67
+ attr_reader :amount_financed
68
+ # @return [Flt::DecNum] cash made available to the borrower after an unfinanced fee
69
+ attr_reader :net_proceeds
70
+ # @return [Flt::DecNum] fee charged when the loan is originated
71
+ attr_reader :origination_fee
72
+ # @return [Integer] number of leading periods that pay interest but no scheduled principal
73
+ attr_reader :interest_only_periods
26
74
  # @return [Flt::DecNum] the required monthly payment. For loans with more than one rate, returns nil
27
- # @api public
28
75
  attr_reader :payment
29
76
  # @return [Flt::DecNum] the principal amount of the loan
30
- # @api public
31
77
  attr_reader :principal
32
78
  # @return [Array] the interest rates used for calculating the amortization
33
- # @api public
34
79
  attr_reader :rates
80
+ # @return [Array<Entry>] immutable period-by-period loan breakdown
81
+ attr_reader :schedule
35
82
 
36
83
  # @return [Flt::DecNum] the periodic payment due on a loan
37
84
  # @param [Flt::DecNum] principal the initial amount of the loan or investment
@@ -43,21 +90,22 @@ module Finrb
43
90
  # rate.duration #=> 360
44
91
  # Amortization.payment(200000, rate.monthly, rate.duration) #=> Flt::DecNum('-926.23')
45
92
  # @see https://en.wikipedia.org/wiki/Amortization_calculator
46
- # @api public
47
- def self.payment(principal, rate, periods)
48
- principal = Validation.decimal(principal, name: 'principal')
49
- raise(ArgumentError, 'principal must be positive.') unless principal.positive?
93
+ def self.payment(principal, rate, periods, balloon: 0)
94
+ principal = Validation.positive_decimal(principal, name: 'principal', message: 'principal must be positive.')
50
95
 
51
- rate = Validation.decimal(rate, name: 'rate')
52
- raise(ArgumentError, 'periodic rate must be greater than -1.') if rate <= -1
96
+ balloon = Validation.decimal(balloon, name: 'balloon')
97
+ raise(ArgumentError, 'balloon must be non-negative and no greater than principal.') unless balloon.between?(0, principal)
53
98
 
54
- periods = Validation.positive_integer(periods, name: 'periods')
99
+ rate = Validation.decimal_greater_than(rate, minimum: -1, name: 'periodic rate')
100
+
101
+ periods = Validation.positive_integer(periods, name: 'period count')
55
102
 
56
103
  if rate.zero?
57
104
  # simplified formula to avoid division-by-zero when interest rate is zero
58
- -Precision.money(principal / periods)
105
+ -Precision.money((principal - balloon) / periods)
59
106
  else
60
- -Precision.money(principal * (rate + (rate / (((rate + 1)**periods) - 1))))
107
+ growth = (rate + 1)**periods
108
+ -Precision.money(((principal * growth) - balloon) * rate / (growth - 1))
61
109
  end
62
110
  end
63
111
 
@@ -66,10 +114,19 @@ module Finrb
66
114
  # @param [Flt::DecNum] principal the initial amount of the loan or investment
67
115
  # @param [Rate] rates the applicable interest rates
68
116
  # @param [Proc] block
69
- # @api public
70
- def initialize(principal, *rates, &block)
71
- @principal = Validation.decimal(principal, name: 'principal')
72
- raise(ArgumentError, 'principal must be positive.') unless @principal.positive?
117
+ def initialize(principal, *rates, balloon: 0, interest_only_periods: 0, origination_fee: 0, finance_origination_fee: false, &block)
118
+ @principal = Validation.positive_decimal(principal, name: 'principal', message: 'principal must be positive.')
119
+
120
+ @origination_fee = Validation.non_negative_decimal(origination_fee, name: 'origination fee')
121
+ raise(ArgumentError, 'finance_origination_fee must be true or false.') unless [true, false].include?(finance_origination_fee)
122
+ raise(ArgumentError, 'an unfinanced origination_fee must be less than principal.') if !finance_origination_fee && @origination_fee >= @principal
123
+
124
+ @finance_origination_fee = finance_origination_fee
125
+ @amount_financed = @principal + (finance_origination_fee ? @origination_fee : 0)
126
+ @net_proceeds = @principal - (finance_origination_fee ? 0 : @origination_fee)
127
+
128
+ @balloon = Validation.decimal(balloon, name: 'balloon')
129
+ raise(ArgumentError, 'balloon must be non-negative and less than amount financed.') if @balloon.negative? || @balloon >= @amount_financed
73
130
  raise(ArgumentError, 'at least one rate is required.') if rates.empty?
74
131
  raise(ArgumentError, 'rates must be Finrb::Rate instances.') unless rates.all?(Rate)
75
132
  raise(ArgumentError, 'every rate must have a duration.') if rates.any? { |rate| rate.duration.nil? }
@@ -79,7 +136,11 @@ module Finrb
79
136
 
80
137
  # compute the total duration from all of the rates.
81
138
  @periods = rates.sum(&:duration)
82
- @period = 0
139
+ valid_interest_only = interest_only_periods.is_a?(Integer) && interest_only_periods.between?(0, @periods - 1)
140
+ raise(ArgumentError, 'interest_only_periods must be a non-negative integer shorter than the loan term.') unless valid_interest_only
141
+
142
+ @interest_only_periods = interest_only_periods
143
+ @period = 0
83
144
 
84
145
  compute
85
146
  end
@@ -87,17 +148,18 @@ module Finrb
87
148
  # compare two Amortization instances
88
149
  # @return [Numeric] -1, 0, or +1
89
150
  # @param [Amortization] other
90
- # @api public
91
151
  def ==(other)
92
- (principal == other.principal) && (rates == other.rates) && (payments == other.payments)
152
+ (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)
93
153
  end
94
154
 
155
+ attr_reader :finance_origination_fee
156
+ alias finance_origination_fee? finance_origination_fee
157
+
95
158
  # @return [Array] the amount of any additional payments in each period
96
159
  # @example
97
160
  # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
98
161
  # amt = Finrb::Amortization.new(300000, rate){ |payment| payment.amount-100}
99
162
  # amt.additional_payments #=> [Flt::DecNum('-100.00'), Flt::DecNum('-100.00'), ... ]
100
- # @api public
101
163
  def additional_payments
102
164
  @transactions.filter_map { |trans| trans.difference if trans.payment? }
103
165
  end
@@ -105,32 +167,29 @@ module Finrb
105
167
  # amortize the balance of loan with the given interest rate
106
168
  # @return none
107
169
  # @param [Rate] rate the interest rate to use in the amortization
108
- # @api private
109
170
  def amortize(rate)
110
- # For the purposes of calculating a payment, the relevant time
111
- # period is the remaining number of periods in the loan, not
112
- # necessarily the duration of the rate itself.
113
- periods = @periods - @period
114
- amount = Amortization.payment(@balance, rate.monthly, periods)
115
-
116
- pmt = Payment.new(amount, period: @period)
117
- pmt.modify(&@block) if @block
118
- raise(ArgumentError, 'payment modification must produce a negative amount.') unless pmt.amount.negative?
171
+ regular_payment = nil
119
172
 
120
173
  rate.duration.to_i.times do
121
174
  # Do this first in case the balance is zero already.
122
175
  break if @balance.zero?
123
176
 
177
+ interest_only = @period < @interest_only_periods
178
+ regular_payment ||= build_regular_payment(rate) unless interest_only
179
+
124
180
  # Compute and record interest on the outstanding balance.
125
181
  int = Precision.money(@balance * rate.monthly)
126
182
  interest = Interest.new(int, period: @period)
127
183
  @balance += interest.amount
128
184
  @transactions << interest.dup
129
185
 
130
- # Record payment. Don't pay more than the outstanding balance.
131
- pmt.amount = -@balance if pmt.amount.abs > @balance
132
- @transactions << pmt.dup
133
- @balance += pmt.amount
186
+ payment = interest_only ? build_interest_only_payment(int) : regular_payment
187
+ payment.period = @period
188
+ payment.amount = -@balance if payment.amount.abs > @balance
189
+ @additional_by_period << [-payment.difference, Flt::DecNum(0)].max
190
+ @interest_only_by_period << interest_only
191
+ @transactions << payment.dup
192
+ @balance += payment.amount
134
193
 
135
194
  @period += 1
136
195
  end
@@ -138,26 +197,35 @@ module Finrb
138
197
 
139
198
  # compute the amortization of the principal
140
199
  # @return none
141
- # @api private
142
200
  def compute
143
- @balance = @principal
201
+ @balance = @amount_financed
144
202
  @transactions = []
203
+ @additional_by_period = []
204
+ @interest_only_by_period = []
145
205
 
146
206
  @rates.each do |rate|
147
207
  amortize(rate)
148
208
  end
149
209
 
150
- # Add any remaining balance due to rounding error to the last payment.
210
+ # Add the residual balloon and any rounding remainder to the last payment.
211
+ @balloon_by_period = Array.new(@additional_by_period.length, Flt::DecNum(0))
151
212
  if @balance.nonzero?
213
+ @balloon_by_period[-1] = [@balloon, @balance].min
152
214
  @transactions.reverse.find(&:payment?).amount -= @balance
153
215
  @balance = 0
154
216
  end
155
217
 
156
- @payment = (payments.first if @rates.length == 1)
218
+ @payment = (payments.first if @rates.length == 1 && @interest_only_periods.zero?)
157
219
 
158
220
  @transactions.freeze
221
+ @additional_by_period.freeze
222
+ @balloon_by_period.freeze
223
+ @interest_only_by_period.freeze
224
+ @schedule = build_schedule.freeze
159
225
  end
160
226
 
227
+ private :amortize, :compute
228
+
161
229
  # @return [Integer] the time required to pay off the loan, in months
162
230
  # @example In most cases, the duration is equal to the total duration of all rates
163
231
  # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
@@ -167,12 +235,10 @@ module Finrb
167
235
  # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
168
236
  # amt = Finrb::Amortization.new(300000, rate){ |payment| payment.amount-100}
169
237
  # amt.duration #=> 319
170
- # @api public
171
238
  def duration
172
239
  payments.length
173
240
  end
174
241
 
175
- # @api public
176
242
  def inspect
177
243
  "Amortization.new(#{@principal})"
178
244
  end
@@ -186,7 +252,6 @@ module Finrb
186
252
  # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
187
253
  # amt = Finrb::Amortization.new(300000, rate)
188
254
  # amt.interest[0,6].sum #=> Flt::DecNum('5603.74')
189
- # @api public
190
255
  def interest
191
256
  @transactions.filter_map { |trans| trans.amount if trans.interest? }
192
257
  end
@@ -196,9 +261,45 @@ module Finrb
196
261
  # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
197
262
  # amt = Finrb::Amortization.new(300000, rate)
198
263
  # amt.payments.sum #=> Flt::DecNum('-500163.94')
199
- # @api public
200
264
  def payments
201
265
  @transactions.filter_map { |trans| trans.amount if trans.payment? }
202
266
  end
267
+
268
+ private
269
+
270
+ def build_schedule
271
+ opening_balance = @amount_financed
272
+ @transactions.each_slice(2).with_index.map do |(interest, payment), index|
273
+ principal = -(payment.amount + interest.amount)
274
+ closing_balance = opening_balance - principal
275
+ entry = Entry.new(period: payment.period, opening_balance:, payment: payment.amount, interest: interest.amount, principal:, additional_payment: @additional_by_period.fetch(index), balloon_payment: @balloon_by_period.fetch(index), interest_only: @interest_only_by_period.fetch(index), closing_balance:)
276
+ opening_balance = closing_balance
277
+ entry
278
+ end
279
+ end
280
+
281
+ def build_regular_payment(rate)
282
+ periods = @periods - @period
283
+ amount = Amortization.payment(@balance, rate.monthly, periods, balloon: @balloon)
284
+ Payment.new(amount, period: @period).tap do |payment|
285
+ payment.modify(&@block) if @block
286
+ validate_payment!(payment)
287
+ end
288
+ end
289
+
290
+ def build_interest_only_payment(interest)
291
+ Payment.new(-interest, period: @period).tap do |payment|
292
+ payment.modify(&@block) if @block
293
+ validate_payment!(payment, allow_zero: true)
294
+ end
295
+ end
296
+
297
+ def validate_payment!(payment, allow_zero: false)
298
+ valid = payment.amount.negative? || (allow_zero && payment.amount.zero?)
299
+ return if valid
300
+
301
+ requirement = allow_zero ? 'must not produce a positive amount' : 'must produce a negative amount'
302
+ raise(ArgumentError, "payment modification #{requirement}.")
303
+ end
203
304
  end
204
305
  end
@@ -12,7 +12,6 @@ require 'date'
12
12
 
13
13
  module Finrb
14
14
  # Provides methods for working with cash flows (collections of transactions)
15
- # @api public
16
15
  module Cashflow
17
16
  class << self
18
17
  def irr(cashflows, guess = nil)
@@ -23,6 +22,10 @@ module Finrb
23
22
  sequence(cashflows).npv(rate)
24
23
  end
25
24
 
25
+ def mirr(cashflows, finance_rate:, reinvestment_rate:)
26
+ sequence(cashflows).mirr(finance_rate:, reinvestment_rate:)
27
+ end
28
+
26
29
  def xirr(transactions, guess = nil)
27
30
  sequence(transactions).xirr(guess)
28
31
  end
@@ -55,7 +58,6 @@ module Finrb
55
58
  # @example
56
59
  # Finrb::Cashflow.irr([-4000,1200,1410,1875,1050]) #=> 0.143
57
60
  # @see https://en.wikipedia.org/wiki/Internal_rate_of_return
58
- # @api public
59
61
  def irr(guess = nil)
60
62
  validate_numeric_cashflows!
61
63
 
@@ -71,13 +73,11 @@ module Finrb
71
73
  # @example
72
74
  # Finrb::Cashflow.npv([-100.0, 60, 60, 60], 0.1) #=> 49.211
73
75
  # @see https://en.wikipedia.org/wiki/Net_present_value
74
- # @api public
75
76
  def npv(rate)
76
77
  validate_numeric_cashflows!
77
78
  cashflows = map { |entry| Validation.decimal(entry, name: 'cashflow amount') }
78
79
 
79
- rate = Validation.decimal(rate, name: 'rate')
80
- raise(DomainError, 'Rate must be greater than -1.') if rate <= -1
80
+ rate = Validation.decimal_greater_than(rate, minimum: -1, name: 'rate', error: DomainError)
81
81
 
82
82
  total = Flt::DecNum.new(0.to_s)
83
83
  cashflows.each_with_index do |cashflow, index|
@@ -87,6 +87,32 @@ module Finrb
87
87
  total
88
88
  end
89
89
 
90
+ # Calculate the modified internal rate of return for equally spaced
91
+ # cashflows using separate financing and reinvestment assumptions.
92
+ # @return [Flt::DecNum] modified per-period internal rate of return
93
+ def mirr(finance_rate:, reinvestment_rate:)
94
+ validate_numeric_cashflows!
95
+ raise(InvalidCashflowError, 'MIRR requires at least two cashflows.') if size < 2
96
+
97
+ cashflows = map { |entry| Validation.decimal(entry, name: 'cashflow amount') }
98
+ raise(InvalidCashflowError, 'Cashflow needs at least one positive and one negative value.') if cashflows.none?(&:positive?) || cashflows.none?(&:negative?)
99
+
100
+ finance_rate = Validation.decimal_greater_than(finance_rate, minimum: -1, name: 'finance rate', error: DomainError)
101
+ reinvestment_rate = Validation.decimal_greater_than(reinvestment_rate, minimum: -1, name: 'reinvestment rate', error: DomainError)
102
+
103
+ last_period = cashflows.size - 1
104
+ future_positive =
105
+ cashflows.each_with_index.sum do |amount, index|
106
+ amount.positive? ? amount * ((reinvestment_rate + 1)**(last_period - index)) : Flt::DecNum(0)
107
+ end
108
+ present_negative =
109
+ cashflows.each_with_index.sum do |amount, index|
110
+ amount.negative? ? amount / ((finance_rate + 1)**index) : Flt::DecNum(0)
111
+ end
112
+
113
+ ((future_positive / -present_negative)**(Flt::DecNum(1) / last_period)) - 1
114
+ end
115
+
90
116
  # Calculate the effective annual internal rate of return for an ordered
91
117
  # sequence of dated transactions.
92
118
  #
@@ -108,7 +134,6 @@ module Finrb
108
134
  # @transactions << Transaction.new( 600, :date => Time.new(1990,01,01))
109
135
  # @transactions << Transaction.new( 600, :date => Time.new(1995,01,01))
110
136
  # Finrb::Cashflow.xirr(@transactions, 0.6) #=> Rate("0.024851", :effective, :compounds => :annually)
111
- # @api public
112
137
  def xirr(guess = nil)
113
138
  validate_dated_cashflows!
114
139
 
@@ -127,11 +152,9 @@ module Finrb
127
152
  # @transactions << Transaction.new( 600, :date => Time.new(1990,01,01))
128
153
  # @transactions << Transaction.new( 600, :date => Time.new(1995,01,01))
129
154
  # Finrb::Cashflow.xnpv(@transactions, 0.6).round(2) #=> -937.41
130
- # @api public
131
155
  def xnpv(rate)
132
156
  validate_dated_cashflows!
133
- rate = Validation.decimal(rate, name: 'rate')
134
- raise(DomainError, 'Rate must be greater than -1.') if rate <= -1
157
+ rate = Validation.decimal_greater_than(rate, minimum: -1, name: 'rate', error: DomainError)
135
158
 
136
159
  sum do |t|
137
160
  t.amount / ((rate + 1)**(date_diff(start, t.date) / days_in_period))
data/lib/finrb/config.rb CHANGED
@@ -41,16 +41,16 @@ module Finrb
41
41
  end
42
42
 
43
43
  def self.build_configuration(values)
44
- eps = configuration_decimal(values.fetch(:eps), name: 'eps')
45
- raise(ArgumentError, 'eps must be positive.') unless eps.positive?
44
+ eps = configuration_decimal(values.fetch(:eps), name: 'solver tolerance')
45
+ raise(ArgumentError, 'solver tolerance must be positive.') unless eps.positive?
46
46
 
47
- guess = configuration_decimal(values.fetch(:guess), name: 'guess')
48
- raise(ArgumentError, 'guess must be greater than -1.') if guess <= -1
47
+ guess = configuration_decimal(values.fetch(:guess), name: 'rate guess')
48
+ raise(ArgumentError, 'rate guess must be greater than -1.') if guess <= -1
49
49
 
50
50
  business_days = values.fetch(:business_days)
51
51
  periodic_compound = values.fetch(:periodic_compound)
52
52
  booleans = [business_days, periodic_compound].all? { |value| value.equal?(true) || value.equal?(false) }
53
- raise(ArgumentError, 'business_days and periodic_compound must be boolean.') unless booleans
53
+ raise(ArgumentError, 'business days and periodic compounding settings must be boolean.') unless booleans
54
54
 
55
55
  Configuration.new(eps:, guess:, business_days:, periodic_compound:)
56
56
  end
data/lib/finrb/rates.rb CHANGED
@@ -6,7 +6,6 @@ require_relative 'validation'
6
6
  module Finrb
7
7
  # the Rate class provides an interface for working with interest rates.
8
8
  # {render:Rate#new}
9
- # @api public
10
9
  class Rate
11
10
  include Comparable
12
11
 
@@ -18,10 +17,7 @@ module Finrb
18
17
  infinite = value.infinite? if value.respond_to?(:infinite?)
19
18
  return Flt::DecNum.infinity if [true, 1].include?(infinite)
20
19
 
21
- periods = Validation.decimal(value, name: 'compounding periods')
22
- raise(ArgumentError, 'compounding periods must be positive.') unless periods.positive?
23
-
24
- periods
20
+ Validation.positive_decimal(value, name: 'compounding periods', message: 'compounding periods must be positive.')
25
21
  end
26
22
  private_class_method :compounding_periods
27
23
 
@@ -31,7 +27,6 @@ module Finrb
31
27
  # @param [Numeric] periods the number of compounding periods per year
32
28
  # @example
33
29
  # Rate.to_effective(0.05, 4) #=> Flt::DecNum('0.05095')
34
- # @api public
35
30
  def self.to_effective(rate, periods)
36
31
  rate = Validation.decimal(rate, name: 'rate')
37
32
  periods = compounding_periods(periods)
@@ -50,10 +45,8 @@ module Finrb
50
45
  # @example
51
46
  # Rate.to_nominal(0.06, 365) #=> Flt::DecNum('0.05827')
52
47
  # @see https://www.miniwebtool.com/nominal-interest-rate-calculator/
53
- # @api public
54
48
  def self.to_nominal(rate, periods)
55
- rate = Validation.decimal(rate, name: 'rate')
56
- raise(ArgumentError, 'effective rate must be greater than -1.') if rate <= -1
49
+ rate = Validation.decimal_greater_than(rate, minimum: -1, name: 'effective rate')
57
50
 
58
51
  periods = compounding_periods(periods)
59
52
 
@@ -75,7 +68,6 @@ module Finrb
75
68
  # Rate.new(0.035, :apr) #=> Rate(0.035, :apr)
76
69
  # @see https://en.wikipedia.org/wiki/Effective_interest_rate
77
70
  # @see https://en.wikipedia.org/wiki/Nominal_interest_rate
78
- # @api public
79
71
  def initialize(rate, type, opts = {})
80
72
  raise(ArgumentError, 'options must be a Hash.') unless opts.is_a?(Hash)
81
73
  raise(ArgumentError, 'options may only contain compounds and duration.') unless (opts.keys - %i[compounds duration]).empty?
@@ -97,13 +89,10 @@ module Finrb
97
89
  end
98
90
 
99
91
  # @return [Integer] the duration for which the rate is valid, in months
100
- # @api public
101
92
  attr_reader :duration
102
93
  # @return [Flt::DecNum] the effective interest rate
103
- # @api public
104
94
  attr_reader :effective
105
95
  # @return [Flt::DecNum] the nominal interest rate
106
- # @api public
107
96
  attr_reader :nominal
108
97
 
109
98
  # compare two Rates, using the effective rate
@@ -113,21 +102,18 @@ module Finrb
113
102
  # r1 = Rate.new(0.15, :nominal) #=> Rate.new(0.160755, :apr)
114
103
  # r2 = Rate.new(0.155, :nominal, :compounds => :semiannually) #=> Rate.new(0.161006, :apr)
115
104
  # r1 <=> r2 #=> -1
116
- # @api public
117
105
  def <=>(other)
118
106
  @effective <=> other.effective
119
107
  end
120
108
 
121
109
  # Return the nominal annual percentage rate for the configured compounding frequency.
122
110
  # @return [Flt::DecNum] the nominal annual percentage rate
123
- # @api public
124
111
  def apr
125
112
  nominal
126
113
  end
127
114
 
128
115
  # Return the effective annual percentage yield.
129
116
  # @return [Flt::DecNum] the effective annual percentage yield
130
- # @api public
131
117
  def apy
132
118
  effective
133
119
  end
@@ -136,7 +122,6 @@ module Finrb
136
122
  # @return none
137
123
  # @param [Symbol, Numeric] input the compounding frequency
138
124
  # @raise [ArgumentError] if input is not an accepted keyword or Numeric
139
- # @api private
140
125
  def compounds=(input)
141
126
  @periods =
142
127
  case input
@@ -152,13 +137,12 @@ module Finrb
152
137
  end
153
138
 
154
139
  def duration=(value)
155
- @duration = Validation.positive_integer(value, name: 'duration')
140
+ @duration = Validation.positive_integer(value, name: 'duration in months')
156
141
  end
157
142
 
158
143
  # set the effective interest rate
159
144
  # @return none
160
145
  # @param [Flt::DecNum] rate the effective interest rate
161
- # @api private
162
146
  def effective=(rate)
163
147
  raise(ArgumentError, 'effective rate must be greater than -1.') if rate <= -1
164
148
 
@@ -176,7 +160,6 @@ module Finrb
176
160
  # rate.apr.round(6) #=> Flt::DecNum('0.15')
177
161
  # rate.apy.round(6) #=> Flt::DecNum('0.160755')
178
162
  # rate.monthly.round(6) #=> Flt::DecNum('0.0125')
179
- # @api public
180
163
  def monthly
181
164
  @monthly ||= Precision.rate(Rate.to_nominal(effective, 12) / 12)
182
165
  end
@@ -184,7 +167,6 @@ module Finrb
184
167
  # set the nominal interest rate
185
168
  # @return none
186
169
  # @param [Flt::DecNum] rate the nominal interest rate
187
- # @api private
188
170
  def nominal=(rate)
189
171
  raise(ArgumentError, 'nominal rate must keep every compounded period greater than -100%.') if !@periods.infinite? && rate <= -@periods
190
172