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.
- checksums.yaml +7 -0
- data/.dockerignore +2 -0
- data/.gitattributes +83 -0
- data/.gitignore +114 -0
- data/.rubocop.yml +19 -0
- data/.ruby-version +1 -0
- data/.semver +5 -0
- data/.travis.yml +7 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +74 -0
- data/COPYING +674 -0
- data/COPYING.LESSER +165 -0
- data/Dockerfile +35 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +116 -0
- data/README.md +99 -0
- data/Rakefile +33 -0
- data/docs/.gitkeep +0 -0
- data/docs/api.md +1104 -0
- data/finrb.gemspec +98 -0
- data/lib/finrb/amortization.rb +199 -0
- data/lib/finrb/cashflows.rb +171 -0
- data/lib/finrb/config.rb +11 -0
- data/lib/finrb/decimal.rb +23 -0
- data/lib/finrb/rates.rb +167 -0
- data/lib/finrb/transaction.rb +124 -0
- data/lib/finrb/utils.rb +1171 -0
- data/lib/finrb.rb +23 -0
- metadata +306 -0
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
|
data/lib/finrb/config.rb
ADDED
@@ -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
|
data/lib/finrb/rates.rb
ADDED
@@ -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
|