finrb 0.0.1

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.
data/finrb.gemspec ADDED
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ SPEC =
4
+ Gem::Specification.new do |s|
5
+ s.name = 'finrb'
6
+ s.version = '0.0.1'
7
+ s.authors = ['Nadir Cohen', 'Martin Bjeldbak Madsen', 'Bill Kranec']
8
+ s.license = 'LGPL-3.0'
9
+ s.email = ['nadircs11@gmail.com', 'me@martinbjeldbak.com', 'wkranec@gmail.com']
10
+ s.platform = Gem::Platform::RUBY
11
+ s.summary = 'Ruby gem for financial calculations/modeling'
12
+
13
+ s.description = <<~EOF
14
+ The finrb library (forked from the finance gem) provides a Ruby interface for financial calculations/modeling.
15
+
16
+ - Working with interest rates
17
+ - Mortgage amortization
18
+ - Cashflows (NPV, IRR, etc.)
19
+ - Computing bank discount yield (BDY) for a T-bill
20
+ - Computing money market yield (MMY) for a T-bill
21
+ - Cash ratio - Liquidity ratios measure the firm's ability to satisfy its short-term obligations as they come due.
22
+ - Computing Coefficient of variation
23
+ - Cost of goods sold and ending inventory under three methods (FIFO,LIFO,Weighted average)
24
+ - Current ratio - Liquidity ratios measure the firm's ability to satisfy its short-term obligations as they come due.
25
+ - Depreciation Expense Recognition - double-declining balance (DDB), the most common declining balance method, which applies two times the straight-line rate to the declining balance.
26
+ - Debt ratio - Solvency ratios measure the firm's ability to satisfy its long-term obligations.
27
+ - Diluted Earnings Per Share
28
+ - Computing the rate of return for each period
29
+ - Convert stated annual rate to the effective annual rate
30
+ - Convert stated annual rate to the effective annual rate with continuous compounding
31
+ - Bond-equivalent yield (BEY), 2 x the semiannual discount rate
32
+ - Computing HPR, the holding period return
33
+ - Equivalent/proportional Interest Rates
34
+ - Basic Earnings Per Share
35
+ - Financial leverage - Solvency ratios measure the firm's ability to satisfy its long-term obligations.
36
+ - Estimate future value (fv)
37
+ - Estimate future value of an annuity
38
+ - Estimate future value (fv) of a single sum
39
+ - Computing the future value of an uneven cash flow series
40
+ - Geometric mean return
41
+ - Gross profit margin - Evaluate a company's financial performance
42
+ - Harmonic mean, average price
43
+ - Computing HPR, the holding period return
44
+ - Bond-equivalent yield (BEY), 2 x the semiannual discount rate
45
+ - Convert holding period return to the effective annual rate
46
+ - Computing money market yield (MMY) for a T-bill
47
+ - Computing IRR, the internal rate of return
48
+ - Calculate the net increase in common shares from the potential exercise of stock options or warrants
49
+ - Long-term debt-to-equity - Solvency ratios measure the firm's ability to satisfy its long-term obligations.
50
+ - Computing HPR, the holding period return
51
+ - Estimate the number of periods
52
+ - Net profit margin - Evaluate a company's financial performance
53
+ - Computing NPV, the PV of the cash flows less the initial (time = 0) outlay
54
+ - Estimate period payment
55
+ - Estimate present value (pv)
56
+ - Estimate present value (pv) of an annuity
57
+ - Estimate present value of a perpetuity
58
+ - Estimate present value (pv) of a single sum
59
+ - Computing the present value of an uneven cash flow series
60
+ - Quick ratio - Liquidity ratios measure the firm's ability to satisfy its short-term obligations as they come due.
61
+ - Convert a given norminal rate to a continuous compounded rate
62
+ - Convert a given continuous compounded rate to a norminal rate
63
+ - Rate of return for a perpetuity
64
+ - Computing Sampling error
65
+ - Computing Roy's safety-first ratio
66
+ - Computing Sharpe Ratio
67
+ - Depreciation Expense Recognition - Straight-line depreciation (SL) allocates an equal amount of depreciation each year over the asset's useful life
68
+ - Total debt-to-equity - Solvency ratios measure the firm's ability to satisfy its long-term obligations.
69
+ - Computing TWRR, the time-weighted rate of return
70
+ - Calculate weighted average shares - weighted average number of common shares
71
+ - Weighted mean as a portfolio return
72
+
73
+ EOF
74
+
75
+ s.homepage = 'https://rubygems.org/gems/finrb'
76
+
77
+ s.required_ruby_version = '>= 3.0'
78
+
79
+ s.add_dependency('activesupport')
80
+ s.add_dependency('business_time')
81
+ s.add_dependency('flt')
82
+
83
+ s.add_development_dependency('minitest')
84
+ s.add_development_dependency('pry')
85
+ s.add_development_dependency('rake')
86
+ s.add_development_dependency('rubocop')
87
+ s.add_development_dependency('rubocop-minitest')
88
+ s.add_development_dependency('rubocop-performance')
89
+ s.add_development_dependency('rubocop-rake')
90
+ s.add_development_dependency('semver')
91
+ s.add_development_dependency('solargraph')
92
+
93
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
94
+
95
+ s.extra_rdoc_files = ['README.md', 'COPYING', 'COPYING.LESSER', 'CHANGELOG.md']
96
+
97
+ s.metadata['rubygems_mfa_required'] = 'true'
98
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'cashflows'
4
+ require_relative 'decimal'
5
+ require_relative 'transaction'
6
+
7
+ module Finrb
8
+ # the Amortization class provides an interface for working with loan amortizations.
9
+ # @note There are _two_ ways to create an amortization. The first
10
+ # example uses the amortize method for the Numeric class. The second
11
+ # calls Amortization.new directly.
12
+ # @example Borrow $250,000 under a 30 year, fixed-rate loan with a 4.25% APR
13
+ # rate = Rate.new(0.0425, :apr, :duration => (30 * 12))
14
+ # amortization = 250000.amortize(rate)
15
+ # @example Borrow $250,000 under a 30 year, adjustable rate loan, with an APR starting at 4.25%, and increasing by 1% every five years
16
+ # values = %w{ 0.0425 0.0525 0.0625 0.0725 0.0825 0.0925 }
17
+ # rates = values.collect { |value| Rate.new( value, :apr, :duration = (5 * 12) ) }
18
+ # arm = Amortization.new(250000, *rates)
19
+ # @example Borrow $250,000 under a 30 year, fixed-rate loan with a 4.25% APR, but pay $150 extra each month
20
+ # rate = Rate.new(0.0425, :apr, :duration => (5 * 12))
21
+ # extra_payments = 250000.amortize(rate){ |period| period.payment - 150 }
22
+ # @api public
23
+ class Amortization
24
+ # @return [DecNum] the balance of the loan at the end of the amortization period (usually zero)
25
+ # @api public
26
+ attr_reader :balance
27
+ # @return [DecNum] the required monthly payment. For loans with more than one rate, returns nil
28
+ # @api public
29
+ attr_reader :payment
30
+ # @return [DecNum] the principal amount of the loan
31
+ # @api public
32
+ attr_reader :principal
33
+ # @return [Array] the interest rates used for calculating the amortization
34
+ # @api public
35
+ attr_reader :rates
36
+
37
+ # create a new Amortization instance
38
+ # @return [Amortization]
39
+ # @param [DecNum] principal the initial amount of the loan or investment
40
+ # @param [Rate] rates the applicable interest rates
41
+ # @param [Proc] block
42
+ # @api public
43
+ def initialize(principal, *rates, &block)
44
+ @principal = Flt::DecNum.new(principal.to_s)
45
+ @rates = rates
46
+ @block = block
47
+
48
+ # compute the total duration from all of the rates.
49
+ @periods = rates.sum(&:duration)
50
+ @period = 0
51
+
52
+ compute
53
+ end
54
+
55
+ # compare two Amortization instances
56
+ # @return [Numeric] -1, 0, or +1
57
+ # @param [Amortization]
58
+ # @api public
59
+ def ==(other)
60
+ (principal == other.principal) && (rates == other.rates) && (payments == other.payments)
61
+ end
62
+
63
+ # @return [Array] the amount of any additional payments in each period
64
+ # @example
65
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
66
+ # amt = 300000.amortize(rate){ |payment| payment.amount-100}
67
+ # amt.additional_payments #=> [DecNum('-100.00'), DecNum('-100.00'), ... ]
68
+ # @api public
69
+ def additional_payments
70
+ @transactions.select(&:payment?).map(&:difference)
71
+ end
72
+
73
+ # amortize the balance of loan with the given interest rate
74
+ # @return none
75
+ # @param [Rate] rate the interest rate to use in the amortization
76
+ # @api private
77
+ def amortize(rate)
78
+ # For the purposes of calculating a payment, the relevant time
79
+ # period is the remaining number of periods in the loan, not
80
+ # necessarily the duration of the rate itself.
81
+ periods = @periods - @period
82
+ amount = Amortization.payment(@balance, rate.monthly, periods)
83
+
84
+ pmt = Payment.new(amount, period: @period)
85
+ pmt.modify(&@block) if @block
86
+
87
+ rate.duration.to_i.times do
88
+ # Do this first in case the balance is zero already.
89
+ break if @balance.zero?
90
+
91
+ # Compute and record interest on the outstanding balance.
92
+ int = (@balance * rate.monthly).round(2)
93
+ interest = Interest.new(int, period: @period)
94
+ @balance += interest.amount
95
+ @transactions << interest.dup
96
+
97
+ # Record payment. Don't pay more than the outstanding balance.
98
+ pmt.amount = -@balance if pmt.amount.abs > @balance
99
+ @transactions << pmt.dup
100
+ @balance += pmt.amount
101
+
102
+ @period += 1
103
+ end
104
+ end
105
+
106
+ # compute the amortization of the principal
107
+ # @return none
108
+ # @api private
109
+ def compute
110
+ @balance = @principal
111
+ @transactions = []
112
+
113
+ @rates.each do |rate|
114
+ amortize(rate)
115
+ end
116
+
117
+ # Add any remaining balance due to rounding error to the last payment.
118
+ unless @balance.zero?
119
+ @transactions.reverse.find(&:payment?).amount -= @balance
120
+ @balance = 0
121
+ end
122
+
123
+ @payment = (payments[0] if @rates.length == 1)
124
+
125
+ @transactions.freeze
126
+ end
127
+
128
+ # @return [Integer] the time required to pay off the loan, in months
129
+ # @example In most cases, the duration is equal to the total duration of all rates
130
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
131
+ # amt = 300000.amortize(rate)
132
+ # amt.duration #=> 360
133
+ # @example Extra payments may reduce the duration
134
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
135
+ # amt = 300000.amortize(rate){ |payment| payment.amount-100}
136
+ # amt.duration #=> 319
137
+ # @api public
138
+ def duration
139
+ payments.length
140
+ end
141
+
142
+ # @api public
143
+ def inspect
144
+ "Amortization.new(#{@principal})"
145
+ end
146
+
147
+ # @return [Array] the amount of interest charged in each period
148
+ # @example find the total cost of interest for a loan
149
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
150
+ # amt = 300000.amortize(rate)
151
+ # amt.interest.sum #=> DecNum('200163.94')
152
+ # @example find the total interest charges in the first six months
153
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
154
+ # amt = 300000.amortize(rate)
155
+ # amt.interest[0,6].sum #=> DecNum('5603.74')
156
+ # @api public
157
+ def interest
158
+ @transactions.select(&:interest?).map(&:amount)
159
+ end
160
+
161
+ # @return [DecNum] the periodic payment due on a loan
162
+ # @param [DecNum] principal the initial amount of the loan or investment
163
+ # @param [Rate] rate the applicable interest rate (per period)
164
+ # @param [Integer] periods the number of periods needed for repayment
165
+ # @note in most cases, you will probably want to use rate.monthly when calling this function outside of an Amortization instance.
166
+ # @example
167
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
168
+ # rate.duration #=> 360
169
+ # Amortization.payment(200000, rate.monthly, rate.duration) #=> DecNum('-926.23')
170
+ # @see http://en.wikipedia.org/wiki/Amortization_calculator
171
+ # @api public
172
+ def self.payment(principal, rate, periods)
173
+ if rate.zero?
174
+ # simplified formula to avoid division-by-zero when interest rate is zero
175
+ -(principal / periods).round(2)
176
+ else
177
+ -(principal * (rate + (rate / (((1 + rate)**periods) - 1)))).round(2)
178
+ end
179
+ end
180
+
181
+ # @return [Array] the amount of the payment in each period
182
+ # @example find the total payments for a loan
183
+ # rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
184
+ # amt = 300000.amortize(rate)
185
+ # amt.payments.sum #=> DecNum('-500163.94')
186
+ # @api public
187
+ def payments
188
+ @transactions.select(&:payment?).map(&:amount)
189
+ end
190
+ end
191
+ end
192
+
193
+ class Numeric
194
+ # @see Amortization#new
195
+ # @api public
196
+ def amortize(*rates, &block)
197
+ Finrb::Amortization.new(self, *rates, &block)
198
+ end
199
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'decimal'
4
+ require_relative 'rates'
5
+
6
+ require 'bigdecimal'
7
+ require 'bigdecimal/newton'
8
+ require 'business_time'
9
+ include Newton
10
+
11
+ module Finrb
12
+ # Provides methods for working with cash flows (collections of transactions)
13
+ # @api public
14
+ module Cashflow
15
+ # Base class for working with Newton's Method.
16
+ # @api private
17
+ class Function
18
+ values = { eps: Finrb.config.eps, one: '1.0', two: '2.0', ten: '10.0', zero: '0.0' }
19
+
20
+ values.each do |key, value|
21
+ define_method key do
22
+ BigDecimal(value)
23
+ end
24
+ end
25
+
26
+ def initialize(transactions, function)
27
+ @transactions = transactions
28
+ @function = function
29
+ end
30
+
31
+ def values(x)
32
+ value = @transactions.send(@function, Flt::DecNum.new(x[0].to_s))
33
+ begin
34
+ [BigDecimal(value.to_s)]
35
+ rescue ArgumentError
36
+ [0]
37
+ end
38
+ end
39
+ end
40
+
41
+ # calculate the internal rate of return for a sequence of cash flows
42
+ # @return [DecNum] the internal rate of return
43
+ # @param [Numeric] Initial guess rate, Defaults to 1.0
44
+ # @example
45
+ # [-4000,1200,1410,1875,1050].irr #=> 0.143
46
+ # @see http://en.wikipedia.org/wiki/Internal_rate_of_return
47
+ # @api public
48
+ def irr(guess = nil)
49
+ # Make sure we have a valid sequence of cash flows.
50
+ positives, negatives = partition { |i| i >= 0 }
51
+ raise(ArgumentError, 'Calculation does not converge.') if positives.empty? || negatives.empty?
52
+
53
+ func = Function.new(self, :npv)
54
+ rate = [valid(guess)]
55
+ nlsolve(func, rate)
56
+ rate[0]
57
+ end
58
+
59
+ def method_missing(name, *args, &block)
60
+ return sum if name.to_s == 'sum'
61
+
62
+ super
63
+ end
64
+
65
+ # calculate the net present value of a sequence of cash flows
66
+ # @return [DecNum] the net present value
67
+ # @param [Numeric] rate the discount rate to be applied
68
+ # @example
69
+ # [-100.0, 60, 60, 60].npv(0.1) #=> 49.211
70
+ # @see http://en.wikipedia.org/wiki/Net_present_value
71
+ # @api public
72
+ def npv(rate)
73
+ cashflows = map { |entry| Flt::DecNum.new(entry.to_s) }
74
+
75
+ rate = Flt::DecNum.new(rate.to_s)
76
+ total = Flt::DecNum.new(0.to_s)
77
+ cashflows.each_with_index do |cashflow, index|
78
+ total += cashflow / ((1 + rate)**index)
79
+ end
80
+
81
+ total
82
+ end
83
+
84
+ # calculate the internal rate of return for a sequence of cash flows with dates
85
+ # @param[Numeric] Initial guess rate, Deafults to 1.0
86
+ # @return [Rate] the internal rate of return
87
+ # @example
88
+ # @transactions = []
89
+ # @transactions << Transaction.new(-1000, :date => Time.new(1985,01,01))
90
+ # @transactions << Transaction.new( 600, :date => Time.new(1990,01,01))
91
+ # @transactions << Transaction.new( 600, :date => Time.new(1995,01,01))
92
+ # @transactions.xirr(0.6) #=> Rate("0.024851", :apr, :compounds => :annually)
93
+ # @api public
94
+ def xirr(guess = nil)
95
+ # Make sure we have a valid sequence of cash flows.
96
+ positives, negatives = partition { |t| t.amount >= 0 }
97
+ if positives.empty? || negatives.empty?
98
+ raise(
99
+ ArgumentError,
100
+ 'Calculation does not converge. Cashflow needs to have a least one positive and one negative value.'
101
+ )
102
+ end
103
+
104
+ func = Function.new(self, :xnpv)
105
+ rate = [valid(guess)]
106
+ nlsolve(func, rate)
107
+ Rate.new(rate[0], :apr, compounds: Finrb.config.periodic_compound ? :continuously : :annually)
108
+ end
109
+
110
+ # calculate the net present value of a sequence of cash flows
111
+ # @return [DecNum]
112
+ # @example
113
+ # @transactions = []
114
+ # @transactions << Transaction.new(-1000, :date => Time.new(1985,01,01))
115
+ # @transactions << Transaction.new( 600, :date => Time.new(1990,01,01))
116
+ # @transactions << Transaction.new( 600, :date => Time.new(1995,01,01))
117
+ # @transactions.xnpv(0.6).round(2) #=> -937.41
118
+ # @api public
119
+ def xnpv(rate)
120
+ rate = Flt::DecNum.new(rate.to_s)
121
+
122
+ sum do |t|
123
+ t.amount / ((1 + rate)**(date_diff(start, t.date) / days_in_period))
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ def date_diff(from, to)
130
+ if Finrb.config.business_days
131
+ from.to_date.business_days_until(to)
132
+ else
133
+ to - from
134
+ end
135
+ end
136
+
137
+ def days_in_period
138
+ if Finrb.config.periodic_compound && Finrb.config.business_days
139
+ start.to_date.business_days_until(stop).to_f
140
+ else
141
+ Flt::DecNum.new(365.days.to_s)
142
+ end
143
+ end
144
+
145
+ def start
146
+ @start ||= self[0].date
147
+ end
148
+
149
+ def stop
150
+ @stop ||= self[-1].date.to_date
151
+ end
152
+
153
+ def valid(guess)
154
+ if guess.nil?
155
+ unless Finrb.config.guess.is_a?(Numeric)
156
+ raise(ArgumentError, 'Invalid Guess. Default guess should be a [Numeric] value.')
157
+ end
158
+
159
+ Finrb.config.guess
160
+ else
161
+ raise(ArgumentError, 'Invalid Guess. Use a [Numeric] value.') unless guess.is_a?(Numeric)
162
+
163
+ guess
164
+ end.to_f
165
+ end
166
+ end
167
+ end
168
+
169
+ class Array
170
+ include Finrb::Cashflow
171
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Finrb
4
+ include ActiveSupport::Configurable
5
+
6
+ default_values = { eps: '1.0e-16', guess: 1.0, business_days: false, periodic_compound: false }
7
+
8
+ default_values.each do |key, value|
9
+ config.send("#{key.to_sym}=", value)
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'flt'
5
+ include Flt
6
+
7
+ DecNum.context.define_conversion_from(BigDecimal) do |x, _context|
8
+ DecNum(x.to_s)
9
+ end
10
+
11
+ DecNum.context.define_conversion_to(BigDecimal) do |x|
12
+ BigDecimal(x.to_s)
13
+ end
14
+
15
+ class Numeric
16
+ def to_d
17
+ if instance_of?(DecNum)
18
+ self
19
+ else
20
+ DecNum(to_s)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'decimal'
4
+
5
+ module Finrb
6
+ # the Rate class provides an interface for working with interest rates.
7
+ # {render:Rate#new}
8
+ # @api public
9
+ class Rate
10
+ include Comparable
11
+ # create a new Rate instance
12
+ # @return [Rate]
13
+ # @param [Numeric] rate the decimal value of the interest rate
14
+ # @param [Symbol] type a valid {TYPES rate type}
15
+ # @param [optional, Hash] opts set optional attributes
16
+ # @option opts [String] :duration a time interval for which the rate is valid
17
+ # @option opts [String] :compounds (:monthly) the number of compounding periods per year
18
+ # @example create a 3.5% APR rate
19
+ # Rate.new(0.035, :apr) #=> Rate(0.035, :apr)
20
+ # @see http://en.wikipedia.org/wiki/Effective_interest_rate
21
+ # @see http://en.wikipedia.org/wiki/Nominal_interest_rate
22
+ # @api public
23
+ def initialize(rate, type, opts = {})
24
+ # Default monthly compounding.
25
+ opts = { compounds: :monthly }.merge(opts)
26
+
27
+ # Set optional attributes..
28
+ opts.each do |key, value|
29
+ send("#{key}=", value)
30
+ end
31
+
32
+ # Set the rate in the proper way, based on the value of type.
33
+ begin
34
+ send("#{TYPES.fetch(type)}=", Flt::DecNum.new(rate.to_s))
35
+ rescue KeyError
36
+ raise(ArgumentError, "type must be one of #{TYPES.keys.join(', ')}", caller)
37
+ end
38
+ end
39
+
40
+ # Accepted rate types
41
+ TYPES = { apr: 'effective', apy: 'effective', effective: 'effective', nominal: 'nominal' }.freeze
42
+
43
+ # @return [Integer] the duration for which the rate is valid, in months
44
+ # @api public
45
+ attr_accessor :duration
46
+ # @return [DecNum] the effective interest rate
47
+ # @api public
48
+ attr_reader :effective
49
+ # @return [DecNum] the nominal interest rate
50
+ # @api public
51
+ attr_reader :nominal
52
+
53
+ # compare two Rates, using the effective rate
54
+ # @return [Numeric] one of -1, 0, +1
55
+ # @param [Rate] rate the comparison Rate
56
+ # @example Which is better, a nominal rate of 15% compounded monthly, or 15.5% compounded semiannually?
57
+ # r1 = Rate.new(0.15, :nominal) #=> Rate.new(0.160755, :apr)
58
+ # r2 = Rate.new(0.155, :nominal, :compounds => :semiannually) #=> Rate.new(0.161006, :apr)
59
+ # r1 <=> r2 #=> -1
60
+ # @api public
61
+ def <=>(other)
62
+ @effective <=> other.effective
63
+ end
64
+
65
+ # (see #effective)
66
+ # @api public
67
+ def apr
68
+ effective
69
+ end
70
+
71
+ # (see #effective)
72
+ # @api public
73
+ def apy
74
+ effective
75
+ end
76
+
77
+ # a convenience method which sets the value of @periods
78
+ # @return none
79
+ # @param [Symbol, Numeric] input the compounding frequency
80
+ # @raise [ArgumentError] if input is not an accepted keyword or Numeric
81
+ # @api private
82
+ def compounds=(input)
83
+ @periods =
84
+ case input
85
+ when :annually then Flt::DecNum.new(1)
86
+ when :continuously then Flt::DecNum.infinity
87
+ when :daily then Flt::DecNum.new(365)
88
+ when :monthly then Flt::DecNum.new(12)
89
+ when :quarterly then Flt::DecNum.new(4)
90
+ when :semiannually then Flt::DecNum.new(2)
91
+ when Numeric then Flt::DecNum.new(input.to_s)
92
+ else raise(ArgumentError)
93
+ end
94
+ end
95
+
96
+ # set the effective interest rate
97
+ # @return none
98
+ # @param [DecNum] rate the effective interest rate
99
+ # @api private
100
+ def effective=(rate)
101
+ @effective = rate
102
+ @nominal = Rate.to_nominal(rate, @periods)
103
+ end
104
+
105
+ def inspect
106
+ "Rate.new(#{apr.round(6)}, :apr)"
107
+ end
108
+
109
+ # @return [DecNum] the monthly effective interest rate
110
+ # @example
111
+ # rate = Rate.new(0.15, :nominal)
112
+ # rate.apr.round(6) #=> DecNum('0.160755')
113
+ # rate.monthly.round(6) #=> DecNum('0.013396')
114
+ # @api public
115
+ def monthly
116
+ (effective / 12).round(15)
117
+ end
118
+
119
+ # set the nominal interest rate
120
+ # @return none
121
+ # @param [DecNum] rate the nominal interest rate
122
+ # @api private
123
+ def nominal=(rate)
124
+ @nominal = rate
125
+ @effective = Rate.to_effective(rate, @periods)
126
+ end
127
+
128
+ # convert a nominal interest rate to an effective interest rate
129
+ # @return [DecNum] the effective interest rate
130
+ # @param [Numeric] rate the nominal interest rate
131
+ # @param [Numeric] periods the number of compounding periods per year
132
+ # @example
133
+ # Rate.to_effective(0.05, 4) #=> DecNum('0.05095')
134
+ # @api public
135
+ def self.to_effective(rate, periods)
136
+ rate = Flt::DecNum.new(rate.to_s)
137
+ periods = Flt::DecNum.new(periods.to_s)
138
+
139
+ if periods.infinite?
140
+ rate.exp - 1
141
+ else
142
+ ((1 + (rate / periods))**periods) - 1
143
+ end
144
+ end
145
+
146
+ # convert an effective interest rate to a nominal interest rate
147
+ # @return [DecNum] the nominal interest rate
148
+ # @param [Numeric] rate the effective interest rate
149
+ # @param [Numeric] periods the number of compounding periods per year
150
+ # @example
151
+ # Rate.to_nominal(0.06, 365) #=> DecNum('0.05827')
152
+ # @see http://www.miniwebtool.com/nominal-interest-rate-calculator/
153
+ # @api public
154
+ def self.to_nominal(rate, periods)
155
+ rate = Flt::DecNum.new(rate.to_s)
156
+ periods = Flt::DecNum.new(periods.to_s)
157
+
158
+ if periods.infinite?
159
+ (rate + 1).log
160
+ else
161
+ periods * (((1 + rate)**(1 / periods)) - 1)
162
+ end
163
+ end
164
+
165
+ private :compounds=, :effective=, :nominal=
166
+ end
167
+ end