finrb 1.0.1 → 1.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +54 -0
- data/lib/finrb/amortization.rb +142 -37
- data/lib/finrb/cashflows.rb +31 -5
- data/lib/finrb/rates.rb +0 -14
- data/lib/finrb/returns.rb +102 -0
- data/lib/finrb/transaction.rb +0 -11
- data/lib/finrb/version.rb +1 -1
- data/sig/finrb.rbs +37 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5331d4b4769cc183d279c1eb4d837d05137cb6b09dd69394addb7be0c6debfae
|
|
4
|
+
data.tar.gz: de97a959862d55711e7e6989dc8c47ffa277dcea32cc8184cc89f2158edad160
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e57aeeac9a952790c895d367170596097d15fe5cbdb43bcab6a9943bfa80bff0a3a4d1e9d016e859d6f554b7a892a78ee2b3896f961b850f9ac1fafd7e064948
|
|
7
|
+
data.tar.gz: 84e26bbd5a9862c6733cbce98cebe4ebe1640751992c6fffe74d33ba31b39dc77f0f0ca5cf4ec8b6fd6bb6d0c62fa5bab1cbbacd3b0a8384d4d1eb1573b9528d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# finrb changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Investment returns and risk
|
|
6
|
+
|
|
7
|
+
- Add compound annual growth rate (CAGR) with explicit value and period-domain validation.
|
|
8
|
+
- Add modified internal rate of return (MIRR) with separate financing and reinvestment rates.
|
|
9
|
+
- Add sample and population volatility, downside deviation, Sortino ratio, and maximum drawdown.
|
|
10
|
+
- Add compound-return and square-root-of-time volatility annualization helpers.
|
|
11
|
+
- Define the statistical conventions explicitly: volatility is sample-based by default, downside deviation includes all observations in its denominator, and maximum drawdown is returned as a non-negative loss fraction.
|
|
12
|
+
|
|
13
|
+
### Loan schedules
|
|
14
|
+
|
|
15
|
+
- Expose each amortization period as an immutable `Finrb::Amortization::Entry` containing its period, opening and closing balances, payment, interest, principal, additional principal, balloon settlement, and interest-only state.
|
|
16
|
+
- Preserve the existing cashflow convention: payments are negative, while balances, interest, principal repaid, and additional principal are non-negative.
|
|
17
|
+
- Add contractual balloon targets. Regular installments amortize toward the target and the final payment settles the residual, including cent-rounding reconciliation.
|
|
18
|
+
- Add leading interest-only periods, including zero-rate periods and combinations with balloon loans. Remaining principal amortizes over the rest of the term.
|
|
19
|
+
- Add upfront and financed origination fees with separate `principal`, `net_proceeds`, and `amount_financed` values. Financed fees enter the opening balance; upfront fees reduce borrower proceeds.
|
|
20
|
+
- Correct schedule period numbering across rate segments and reused payment templates.
|
|
21
|
+
|
|
22
|
+
### API and documentation
|
|
23
|
+
|
|
24
|
+
- Add RBS declarations and API examples for all new return metrics and amortization features.
|
|
25
|
+
- Refine README compatibility badges to identify tested MRI versions, x86-64/ARM64 architectures, and experimental JRuby/TruffleRuby coverage.
|
|
26
|
+
- Remove redundant YARD `@api` annotations and make the internal amortization calculation methods genuinely private.
|
|
27
|
+
|
|
3
28
|
## 1.0.1
|
|
4
29
|
|
|
5
30
|
### Runtime compatibility
|
data/README.md
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
[](https://github.com/ncs1/finrb/actions/workflows/ci.yml)
|
|
4
4
|
[](https://github.com/ncs1/finrb/actions/workflows/codeql.yml)
|
|
5
5
|
[](https://github.com/ncs1/finrb/actions/workflows/rubocop.yml)
|
|
6
|
+
[](https://github.com/ncs1/finrb/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/ncs1/finrb/actions/workflows/ci.yml)
|
|
8
|
+
[](https://github.com/ncs1/finrb/actions/workflows/ci.yml)
|
|
6
9
|
|
|
7
10
|
Precision-first financial mathematics for Ruby.
|
|
8
11
|
|
|
@@ -117,6 +120,15 @@ loan = Finrb::Amortization.new(250_000, rate)
|
|
|
117
120
|
loan.payment # => Flt::DecNum('-1229.85')
|
|
118
121
|
loan.interest.sum # => Flt::DecNum('192745.98')
|
|
119
122
|
loan.balance # => Flt::DecNum('0.00')
|
|
123
|
+
|
|
124
|
+
first = loan.schedule.first
|
|
125
|
+
first.opening_balance
|
|
126
|
+
first.interest
|
|
127
|
+
first.principal
|
|
128
|
+
first.payment
|
|
129
|
+
first.balloon_payment
|
|
130
|
+
first.interest_only?
|
|
131
|
+
first.closing_balance
|
|
120
132
|
```
|
|
121
133
|
|
|
122
134
|
Pass several duration-bearing rates for an adjustable-rate schedule. A block
|
|
@@ -130,6 +142,48 @@ end
|
|
|
130
142
|
|
|
131
143
|
Payments and interest follow the sign convention used throughout finrb:
|
|
132
144
|
money received is positive and money paid is negative.
|
|
145
|
+
Schedule balances, interest, principal repaid, and additional principal are
|
|
146
|
+
non-negative; the schedule's payment field is negative.
|
|
147
|
+
|
|
148
|
+
Set a residual principal target to create a balloon loan. Regular installments
|
|
149
|
+
amortize only the non-balloon portion, and the final payment settles the stated
|
|
150
|
+
balloon:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
balloon_loan = Finrb::Amortization.new(250_000, rate, balloon: 100_000)
|
|
154
|
+
balloon_loan.schedule.last.balloon_payment # => Flt::DecNum('100000')
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`balloon` is the contractual residual target. Because regular postings are
|
|
158
|
+
rounded to cents, the actual `balloon_payment` in the final schedule row can
|
|
159
|
+
differ from that target by a few cents.
|
|
160
|
+
|
|
161
|
+
Leading interest-only periods defer scheduled principal repayment and amortize
|
|
162
|
+
the balance over the remaining term:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
interest_only = Finrb::Amortization.new(250_000, rate, interest_only_periods: 24)
|
|
166
|
+
interest_only.schedule.first.interest_only? # => true
|
|
167
|
+
interest_only.schedule.first.principal # => Flt::DecNum('0')
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Origination fees can either reduce the borrower's net proceeds or be added to
|
|
171
|
+
the financed balance:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
cash_fee = Finrb::Amortization.new(250_000, rate, origination_fee: 2_500)
|
|
175
|
+
cash_fee.net_proceeds # => Flt::DecNum('247500')
|
|
176
|
+
cash_fee.amount_financed # => Flt::DecNum('250000')
|
|
177
|
+
|
|
178
|
+
financed_fee = Finrb::Amortization.new(
|
|
179
|
+
250_000,
|
|
180
|
+
rate,
|
|
181
|
+
origination_fee: 2_500,
|
|
182
|
+
finance_origination_fee: true
|
|
183
|
+
)
|
|
184
|
+
financed_fee.net_proceeds # => Flt::DecNum('250000')
|
|
185
|
+
financed_fee.amount_financed # => Flt::DecNum('252500')
|
|
186
|
+
```
|
|
133
187
|
|
|
134
188
|
## Configuration
|
|
135
189
|
|
data/lib/finrb/amortization.rb
CHANGED
|
@@ -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))
|
|
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,11 +90,13 @@ 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
|
-
|
|
47
|
-
def self.payment(principal, rate, periods)
|
|
93
|
+
def self.payment(principal, rate, periods, balloon: 0)
|
|
48
94
|
principal = Validation.decimal(principal, name: 'principal')
|
|
49
95
|
raise(ArgumentError, 'principal must be positive.') unless principal.positive?
|
|
50
96
|
|
|
97
|
+
balloon = Validation.decimal(balloon, name: 'balloon')
|
|
98
|
+
raise(ArgumentError, 'balloon must be non-negative and no greater than principal.') unless balloon.between?(0, principal)
|
|
99
|
+
|
|
51
100
|
rate = Validation.decimal(rate, name: 'rate')
|
|
52
101
|
raise(ArgumentError, 'periodic rate must be greater than -1.') if rate <= -1
|
|
53
102
|
|
|
@@ -55,9 +104,10 @@ module Finrb
|
|
|
55
104
|
|
|
56
105
|
if rate.zero?
|
|
57
106
|
# simplified formula to avoid division-by-zero when interest rate is zero
|
|
58
|
-
-Precision.money(principal / periods)
|
|
107
|
+
-Precision.money((principal - balloon) / periods)
|
|
59
108
|
else
|
|
60
|
-
|
|
109
|
+
growth = (rate + 1)**periods
|
|
110
|
+
-Precision.money(((principal * growth) - balloon) * rate / (growth - 1))
|
|
61
111
|
end
|
|
62
112
|
end
|
|
63
113
|
|
|
@@ -66,10 +116,21 @@ module Finrb
|
|
|
66
116
|
# @param [Flt::DecNum] principal the initial amount of the loan or investment
|
|
67
117
|
# @param [Rate] rates the applicable interest rates
|
|
68
118
|
# @param [Proc] block
|
|
69
|
-
|
|
70
|
-
def initialize(principal, *rates, &block)
|
|
119
|
+
def initialize(principal, *rates, balloon: 0, interest_only_periods: 0, origination_fee: 0, finance_origination_fee: false, &block)
|
|
71
120
|
@principal = Validation.decimal(principal, name: 'principal')
|
|
72
121
|
raise(ArgumentError, 'principal must be positive.') unless @principal.positive?
|
|
122
|
+
|
|
123
|
+
@origination_fee = Validation.decimal(origination_fee, name: 'origination_fee')
|
|
124
|
+
raise(ArgumentError, 'origination_fee must be non-negative.') if @origination_fee.negative?
|
|
125
|
+
raise(ArgumentError, 'finance_origination_fee must be true or false.') unless [true, false].include?(finance_origination_fee)
|
|
126
|
+
raise(ArgumentError, 'an unfinanced origination_fee must be less than principal.') if !finance_origination_fee && @origination_fee >= @principal
|
|
127
|
+
|
|
128
|
+
@finance_origination_fee = finance_origination_fee
|
|
129
|
+
@amount_financed = @principal + (finance_origination_fee ? @origination_fee : 0)
|
|
130
|
+
@net_proceeds = @principal - (finance_origination_fee ? 0 : @origination_fee)
|
|
131
|
+
|
|
132
|
+
@balloon = Validation.decimal(balloon, name: 'balloon')
|
|
133
|
+
raise(ArgumentError, 'balloon must be non-negative and less than amount financed.') if @balloon.negative? || @balloon >= @amount_financed
|
|
73
134
|
raise(ArgumentError, 'at least one rate is required.') if rates.empty?
|
|
74
135
|
raise(ArgumentError, 'rates must be Finrb::Rate instances.') unless rates.all?(Rate)
|
|
75
136
|
raise(ArgumentError, 'every rate must have a duration.') if rates.any? { |rate| rate.duration.nil? }
|
|
@@ -79,7 +140,11 @@ module Finrb
|
|
|
79
140
|
|
|
80
141
|
# compute the total duration from all of the rates.
|
|
81
142
|
@periods = rates.sum(&:duration)
|
|
82
|
-
|
|
143
|
+
valid_interest_only = interest_only_periods.is_a?(Integer) && interest_only_periods.between?(0, @periods - 1)
|
|
144
|
+
raise(ArgumentError, 'interest_only_periods must be a non-negative integer shorter than the loan term.') unless valid_interest_only
|
|
145
|
+
|
|
146
|
+
@interest_only_periods = interest_only_periods
|
|
147
|
+
@period = 0
|
|
83
148
|
|
|
84
149
|
compute
|
|
85
150
|
end
|
|
@@ -87,17 +152,18 @@ module Finrb
|
|
|
87
152
|
# compare two Amortization instances
|
|
88
153
|
# @return [Numeric] -1, 0, or +1
|
|
89
154
|
# @param [Amortization] other
|
|
90
|
-
# @api public
|
|
91
155
|
def ==(other)
|
|
92
|
-
(principal == other.principal) && (rates == other.rates) && (payments == other.payments)
|
|
156
|
+
(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
157
|
end
|
|
94
158
|
|
|
159
|
+
attr_reader :finance_origination_fee
|
|
160
|
+
alias finance_origination_fee? finance_origination_fee
|
|
161
|
+
|
|
95
162
|
# @return [Array] the amount of any additional payments in each period
|
|
96
163
|
# @example
|
|
97
164
|
# rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
|
98
165
|
# amt = Finrb::Amortization.new(300000, rate){ |payment| payment.amount-100}
|
|
99
166
|
# amt.additional_payments #=> [Flt::DecNum('-100.00'), Flt::DecNum('-100.00'), ... ]
|
|
100
|
-
# @api public
|
|
101
167
|
def additional_payments
|
|
102
168
|
@transactions.filter_map { |trans| trans.difference if trans.payment? }
|
|
103
169
|
end
|
|
@@ -105,32 +171,29 @@ module Finrb
|
|
|
105
171
|
# amortize the balance of loan with the given interest rate
|
|
106
172
|
# @return none
|
|
107
173
|
# @param [Rate] rate the interest rate to use in the amortization
|
|
108
|
-
# @api private
|
|
109
174
|
def amortize(rate)
|
|
110
|
-
|
|
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?
|
|
175
|
+
regular_payment = nil
|
|
119
176
|
|
|
120
177
|
rate.duration.to_i.times do
|
|
121
178
|
# Do this first in case the balance is zero already.
|
|
122
179
|
break if @balance.zero?
|
|
123
180
|
|
|
181
|
+
interest_only = @period < @interest_only_periods
|
|
182
|
+
regular_payment ||= build_regular_payment(rate) unless interest_only
|
|
183
|
+
|
|
124
184
|
# Compute and record interest on the outstanding balance.
|
|
125
185
|
int = Precision.money(@balance * rate.monthly)
|
|
126
186
|
interest = Interest.new(int, period: @period)
|
|
127
187
|
@balance += interest.amount
|
|
128
188
|
@transactions << interest.dup
|
|
129
189
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
@
|
|
190
|
+
payment = interest_only ? build_interest_only_payment(int) : regular_payment
|
|
191
|
+
payment.period = @period
|
|
192
|
+
payment.amount = -@balance if payment.amount.abs > @balance
|
|
193
|
+
@additional_by_period << [-payment.difference, Flt::DecNum(0)].max
|
|
194
|
+
@interest_only_by_period << interest_only
|
|
195
|
+
@transactions << payment.dup
|
|
196
|
+
@balance += payment.amount
|
|
134
197
|
|
|
135
198
|
@period += 1
|
|
136
199
|
end
|
|
@@ -138,26 +201,35 @@ module Finrb
|
|
|
138
201
|
|
|
139
202
|
# compute the amortization of the principal
|
|
140
203
|
# @return none
|
|
141
|
-
# @api private
|
|
142
204
|
def compute
|
|
143
|
-
@balance = @
|
|
205
|
+
@balance = @amount_financed
|
|
144
206
|
@transactions = []
|
|
207
|
+
@additional_by_period = []
|
|
208
|
+
@interest_only_by_period = []
|
|
145
209
|
|
|
146
210
|
@rates.each do |rate|
|
|
147
211
|
amortize(rate)
|
|
148
212
|
end
|
|
149
213
|
|
|
150
|
-
# Add
|
|
214
|
+
# Add the residual balloon and any rounding remainder to the last payment.
|
|
215
|
+
@balloon_by_period = Array.new(@additional_by_period.length, Flt::DecNum(0))
|
|
151
216
|
if @balance.nonzero?
|
|
217
|
+
@balloon_by_period[-1] = [@balloon, @balance].min
|
|
152
218
|
@transactions.reverse.find(&:payment?).amount -= @balance
|
|
153
219
|
@balance = 0
|
|
154
220
|
end
|
|
155
221
|
|
|
156
|
-
@payment = (payments.first if @rates.length == 1)
|
|
222
|
+
@payment = (payments.first if @rates.length == 1 && @interest_only_periods.zero?)
|
|
157
223
|
|
|
158
224
|
@transactions.freeze
|
|
225
|
+
@additional_by_period.freeze
|
|
226
|
+
@balloon_by_period.freeze
|
|
227
|
+
@interest_only_by_period.freeze
|
|
228
|
+
@schedule = build_schedule.freeze
|
|
159
229
|
end
|
|
160
230
|
|
|
231
|
+
private :amortize, :compute
|
|
232
|
+
|
|
161
233
|
# @return [Integer] the time required to pay off the loan, in months
|
|
162
234
|
# @example In most cases, the duration is equal to the total duration of all rates
|
|
163
235
|
# rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
|
@@ -167,12 +239,10 @@ module Finrb
|
|
|
167
239
|
# rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
|
168
240
|
# amt = Finrb::Amortization.new(300000, rate){ |payment| payment.amount-100}
|
|
169
241
|
# amt.duration #=> 319
|
|
170
|
-
# @api public
|
|
171
242
|
def duration
|
|
172
243
|
payments.length
|
|
173
244
|
end
|
|
174
245
|
|
|
175
|
-
# @api public
|
|
176
246
|
def inspect
|
|
177
247
|
"Amortization.new(#{@principal})"
|
|
178
248
|
end
|
|
@@ -186,7 +256,6 @@ module Finrb
|
|
|
186
256
|
# rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
|
187
257
|
# amt = Finrb::Amortization.new(300000, rate)
|
|
188
258
|
# amt.interest[0,6].sum #=> Flt::DecNum('5603.74')
|
|
189
|
-
# @api public
|
|
190
259
|
def interest
|
|
191
260
|
@transactions.filter_map { |trans| trans.amount if trans.interest? }
|
|
192
261
|
end
|
|
@@ -196,9 +265,45 @@ module Finrb
|
|
|
196
265
|
# rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
|
197
266
|
# amt = Finrb::Amortization.new(300000, rate)
|
|
198
267
|
# amt.payments.sum #=> Flt::DecNum('-500163.94')
|
|
199
|
-
# @api public
|
|
200
268
|
def payments
|
|
201
269
|
@transactions.filter_map { |trans| trans.amount if trans.payment? }
|
|
202
270
|
end
|
|
271
|
+
|
|
272
|
+
private
|
|
273
|
+
|
|
274
|
+
def build_schedule
|
|
275
|
+
opening_balance = @amount_financed
|
|
276
|
+
@transactions.each_slice(2).with_index.map do |(interest, payment), index|
|
|
277
|
+
principal = -(payment.amount + interest.amount)
|
|
278
|
+
closing_balance = opening_balance - principal
|
|
279
|
+
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:)
|
|
280
|
+
opening_balance = closing_balance
|
|
281
|
+
entry
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def build_regular_payment(rate)
|
|
286
|
+
periods = @periods - @period
|
|
287
|
+
amount = Amortization.payment(@balance, rate.monthly, periods, balloon: @balloon)
|
|
288
|
+
Payment.new(amount, period: @period).tap do |payment|
|
|
289
|
+
payment.modify(&@block) if @block
|
|
290
|
+
validate_payment!(payment)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def build_interest_only_payment(interest)
|
|
295
|
+
Payment.new(-interest, period: @period).tap do |payment|
|
|
296
|
+
payment.modify(&@block) if @block
|
|
297
|
+
validate_payment!(payment, allow_zero: true)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def validate_payment!(payment, allow_zero: false)
|
|
302
|
+
valid = payment.amount.negative? || (allow_zero && payment.amount.zero?)
|
|
303
|
+
return if valid
|
|
304
|
+
|
|
305
|
+
requirement = allow_zero ? 'must not produce a positive amount' : 'must produce a negative amount'
|
|
306
|
+
raise(ArgumentError, "payment modification #{requirement}.")
|
|
307
|
+
end
|
|
203
308
|
end
|
|
204
309
|
end
|
data/lib/finrb/cashflows.rb
CHANGED
|
@@ -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,7 +73,6 @@ 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') }
|
|
@@ -87,6 +88,33 @@ module Finrb
|
|
|
87
88
|
total
|
|
88
89
|
end
|
|
89
90
|
|
|
91
|
+
# Calculate the modified internal rate of return for equally spaced
|
|
92
|
+
# cashflows using separate financing and reinvestment assumptions.
|
|
93
|
+
# @return [Flt::DecNum] modified per-period internal rate of return
|
|
94
|
+
def mirr(finance_rate:, reinvestment_rate:)
|
|
95
|
+
validate_numeric_cashflows!
|
|
96
|
+
raise(InvalidCashflowError, 'MIRR requires at least two cashflows.') if size < 2
|
|
97
|
+
|
|
98
|
+
cashflows = map { |entry| Validation.decimal(entry, name: 'cashflow amount') }
|
|
99
|
+
raise(InvalidCashflowError, 'Cashflow needs at least one positive and one negative value.') if cashflows.none?(&:positive?) || cashflows.none?(&:negative?)
|
|
100
|
+
|
|
101
|
+
finance_rate = Validation.decimal(finance_rate, name: 'finance_rate')
|
|
102
|
+
reinvestment_rate = Validation.decimal(reinvestment_rate, name: 'reinvestment_rate')
|
|
103
|
+
raise(DomainError, 'Finance and reinvestment rates must be greater than -1.') if finance_rate <= -1 || reinvestment_rate <= -1
|
|
104
|
+
|
|
105
|
+
last_period = cashflows.size - 1
|
|
106
|
+
future_positive =
|
|
107
|
+
cashflows.each_with_index.sum do |amount, index|
|
|
108
|
+
amount.positive? ? amount * ((reinvestment_rate + 1)**(last_period - index)) : Flt::DecNum(0)
|
|
109
|
+
end
|
|
110
|
+
present_negative =
|
|
111
|
+
cashflows.each_with_index.sum do |amount, index|
|
|
112
|
+
amount.negative? ? amount / ((finance_rate + 1)**index) : Flt::DecNum(0)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
((future_positive / -present_negative)**(Flt::DecNum(1) / last_period)) - 1
|
|
116
|
+
end
|
|
117
|
+
|
|
90
118
|
# Calculate the effective annual internal rate of return for an ordered
|
|
91
119
|
# sequence of dated transactions.
|
|
92
120
|
#
|
|
@@ -108,7 +136,6 @@ module Finrb
|
|
|
108
136
|
# @transactions << Transaction.new( 600, :date => Time.new(1990,01,01))
|
|
109
137
|
# @transactions << Transaction.new( 600, :date => Time.new(1995,01,01))
|
|
110
138
|
# Finrb::Cashflow.xirr(@transactions, 0.6) #=> Rate("0.024851", :effective, :compounds => :annually)
|
|
111
|
-
# @api public
|
|
112
139
|
def xirr(guess = nil)
|
|
113
140
|
validate_dated_cashflows!
|
|
114
141
|
|
|
@@ -127,7 +154,6 @@ module Finrb
|
|
|
127
154
|
# @transactions << Transaction.new( 600, :date => Time.new(1990,01,01))
|
|
128
155
|
# @transactions << Transaction.new( 600, :date => Time.new(1995,01,01))
|
|
129
156
|
# Finrb::Cashflow.xnpv(@transactions, 0.6).round(2) #=> -937.41
|
|
130
|
-
# @api public
|
|
131
157
|
def xnpv(rate)
|
|
132
158
|
validate_dated_cashflows!
|
|
133
159
|
rate = Validation.decimal(rate, name: 'rate')
|
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
|
|
|
@@ -31,7 +30,6 @@ module Finrb
|
|
|
31
30
|
# @param [Numeric] periods the number of compounding periods per year
|
|
32
31
|
# @example
|
|
33
32
|
# Rate.to_effective(0.05, 4) #=> Flt::DecNum('0.05095')
|
|
34
|
-
# @api public
|
|
35
33
|
def self.to_effective(rate, periods)
|
|
36
34
|
rate = Validation.decimal(rate, name: 'rate')
|
|
37
35
|
periods = compounding_periods(periods)
|
|
@@ -50,7 +48,6 @@ module Finrb
|
|
|
50
48
|
# @example
|
|
51
49
|
# Rate.to_nominal(0.06, 365) #=> Flt::DecNum('0.05827')
|
|
52
50
|
# @see https://www.miniwebtool.com/nominal-interest-rate-calculator/
|
|
53
|
-
# @api public
|
|
54
51
|
def self.to_nominal(rate, periods)
|
|
55
52
|
rate = Validation.decimal(rate, name: 'rate')
|
|
56
53
|
raise(ArgumentError, 'effective rate must be greater than -1.') if rate <= -1
|
|
@@ -75,7 +72,6 @@ module Finrb
|
|
|
75
72
|
# Rate.new(0.035, :apr) #=> Rate(0.035, :apr)
|
|
76
73
|
# @see https://en.wikipedia.org/wiki/Effective_interest_rate
|
|
77
74
|
# @see https://en.wikipedia.org/wiki/Nominal_interest_rate
|
|
78
|
-
# @api public
|
|
79
75
|
def initialize(rate, type, opts = {})
|
|
80
76
|
raise(ArgumentError, 'options must be a Hash.') unless opts.is_a?(Hash)
|
|
81
77
|
raise(ArgumentError, 'options may only contain compounds and duration.') unless (opts.keys - %i[compounds duration]).empty?
|
|
@@ -97,13 +93,10 @@ module Finrb
|
|
|
97
93
|
end
|
|
98
94
|
|
|
99
95
|
# @return [Integer] the duration for which the rate is valid, in months
|
|
100
|
-
# @api public
|
|
101
96
|
attr_reader :duration
|
|
102
97
|
# @return [Flt::DecNum] the effective interest rate
|
|
103
|
-
# @api public
|
|
104
98
|
attr_reader :effective
|
|
105
99
|
# @return [Flt::DecNum] the nominal interest rate
|
|
106
|
-
# @api public
|
|
107
100
|
attr_reader :nominal
|
|
108
101
|
|
|
109
102
|
# compare two Rates, using the effective rate
|
|
@@ -113,21 +106,18 @@ module Finrb
|
|
|
113
106
|
# r1 = Rate.new(0.15, :nominal) #=> Rate.new(0.160755, :apr)
|
|
114
107
|
# r2 = Rate.new(0.155, :nominal, :compounds => :semiannually) #=> Rate.new(0.161006, :apr)
|
|
115
108
|
# r1 <=> r2 #=> -1
|
|
116
|
-
# @api public
|
|
117
109
|
def <=>(other)
|
|
118
110
|
@effective <=> other.effective
|
|
119
111
|
end
|
|
120
112
|
|
|
121
113
|
# Return the nominal annual percentage rate for the configured compounding frequency.
|
|
122
114
|
# @return [Flt::DecNum] the nominal annual percentage rate
|
|
123
|
-
# @api public
|
|
124
115
|
def apr
|
|
125
116
|
nominal
|
|
126
117
|
end
|
|
127
118
|
|
|
128
119
|
# Return the effective annual percentage yield.
|
|
129
120
|
# @return [Flt::DecNum] the effective annual percentage yield
|
|
130
|
-
# @api public
|
|
131
121
|
def apy
|
|
132
122
|
effective
|
|
133
123
|
end
|
|
@@ -136,7 +126,6 @@ module Finrb
|
|
|
136
126
|
# @return none
|
|
137
127
|
# @param [Symbol, Numeric] input the compounding frequency
|
|
138
128
|
# @raise [ArgumentError] if input is not an accepted keyword or Numeric
|
|
139
|
-
# @api private
|
|
140
129
|
def compounds=(input)
|
|
141
130
|
@periods =
|
|
142
131
|
case input
|
|
@@ -158,7 +147,6 @@ module Finrb
|
|
|
158
147
|
# set the effective interest rate
|
|
159
148
|
# @return none
|
|
160
149
|
# @param [Flt::DecNum] rate the effective interest rate
|
|
161
|
-
# @api private
|
|
162
150
|
def effective=(rate)
|
|
163
151
|
raise(ArgumentError, 'effective rate must be greater than -1.') if rate <= -1
|
|
164
152
|
|
|
@@ -176,7 +164,6 @@ module Finrb
|
|
|
176
164
|
# rate.apr.round(6) #=> Flt::DecNum('0.15')
|
|
177
165
|
# rate.apy.round(6) #=> Flt::DecNum('0.160755')
|
|
178
166
|
# rate.monthly.round(6) #=> Flt::DecNum('0.0125')
|
|
179
|
-
# @api public
|
|
180
167
|
def monthly
|
|
181
168
|
@monthly ||= Precision.rate(Rate.to_nominal(effective, 12) / 12)
|
|
182
169
|
end
|
|
@@ -184,7 +171,6 @@ module Finrb
|
|
|
184
171
|
# set the nominal interest rate
|
|
185
172
|
# @return none
|
|
186
173
|
# @param [Flt::DecNum] rate the nominal interest rate
|
|
187
|
-
# @api private
|
|
188
174
|
def nominal=(rate)
|
|
189
175
|
raise(ArgumentError, 'nominal rate must keep every compounded period greater than -100%.') if !@periods.infinite? && rate <= -@periods
|
|
190
176
|
|
data/lib/finrb/returns.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'decimal'
|
|
4
4
|
require_relative 'errors'
|
|
5
|
+
require_relative 'validation'
|
|
5
6
|
|
|
6
7
|
module Finrb
|
|
7
8
|
# Investment return and risk-adjusted performance calculations.
|
|
@@ -17,6 +18,107 @@ module Finrb
|
|
|
17
18
|
end
|
|
18
19
|
private_class_method :wrap_array
|
|
19
20
|
|
|
21
|
+
# Compound annual growth rate over a positive number of periods.
|
|
22
|
+
#
|
|
23
|
+
# Beginning value must be positive. Ending value may be zero, representing
|
|
24
|
+
# a total loss, but cannot be negative because a fractional growth root
|
|
25
|
+
# would not have a generally meaningful real-valued result.
|
|
26
|
+
#
|
|
27
|
+
# @param beginning_value [Numeric] value at the start of the measurement
|
|
28
|
+
# @param ending_value [Numeric] value at the end of the measurement
|
|
29
|
+
# @param periods [Integer] number of equal annual periods
|
|
30
|
+
# @return [Flt::DecNum] compound growth rate per period
|
|
31
|
+
def self.cagr(beginning_value:, ending_value:, periods:)
|
|
32
|
+
beginning_value = Validation.decimal(beginning_value, name: 'beginning_value')
|
|
33
|
+
ending_value = Validation.decimal(ending_value, name: 'ending_value')
|
|
34
|
+
periods = Validation.positive_integer(periods, name: 'periods')
|
|
35
|
+
|
|
36
|
+
raise(ArgumentError, 'beginning_value must be greater than zero.') unless beginning_value.positive?
|
|
37
|
+
raise(ArgumentError, 'ending_value must be greater than or equal to zero.') if ending_value.negative?
|
|
38
|
+
|
|
39
|
+
((ending_value / beginning_value)**(Flt::DecNum(1) / periods)) - 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.risk_values(values, name:)
|
|
43
|
+
values = wrap_array(values)
|
|
44
|
+
raise(ArgumentError, "#{name} cannot be empty.") if values.empty?
|
|
45
|
+
|
|
46
|
+
values.map { |value| Validation.decimal(value, name:) }
|
|
47
|
+
end
|
|
48
|
+
private_class_method :risk_values
|
|
49
|
+
|
|
50
|
+
# Compound a periodic return into an annual return.
|
|
51
|
+
def self.annualize_return(rate:, periods_per_year:)
|
|
52
|
+
rate = Validation.decimal(rate, name: 'rate')
|
|
53
|
+
periods_per_year = Validation.positive_integer(periods_per_year, name: 'periods_per_year')
|
|
54
|
+
raise(ArgumentError, 'rate must be greater than or equal to -1.') if rate < -1
|
|
55
|
+
|
|
56
|
+
((rate + 1)**periods_per_year) - 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Scale periodic volatility by the square root of periods per year.
|
|
60
|
+
def self.annualize_volatility(volatility:, periods_per_year:)
|
|
61
|
+
volatility = Validation.decimal(volatility, name: 'volatility')
|
|
62
|
+
periods_per_year = Validation.positive_integer(periods_per_year, name: 'periods_per_year')
|
|
63
|
+
raise(ArgumentError, 'volatility must be greater than or equal to zero.') if volatility.negative?
|
|
64
|
+
|
|
65
|
+
volatility * (Flt::DecNum(periods_per_year)**Flt::DecNum('0.5'))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Standard deviation of periodic returns. Sample volatility uses n - 1;
|
|
69
|
+
# population volatility uses n.
|
|
70
|
+
def self.volatility(returns:, sample: true)
|
|
71
|
+
raise(ArgumentError, 'sample must be true or false.') unless [true, false].include?(sample)
|
|
72
|
+
|
|
73
|
+
returns = risk_values(returns, name: 'return')
|
|
74
|
+
raise(ArgumentError, 'sample volatility requires at least two returns.') if sample && returns.size < 2
|
|
75
|
+
|
|
76
|
+
mean = returns.sum / returns.size
|
|
77
|
+
denominator = sample ? returns.size - 1 : returns.size
|
|
78
|
+
variance = returns.sum { |value| (value - mean)**2 } / denominator
|
|
79
|
+
variance**Flt::DecNum('0.5')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Root-mean-square return shortfall below a target return. The denominator
|
|
83
|
+
# includes every observation, including returns at or above the target.
|
|
84
|
+
def self.downside_deviation(returns:, target: 0)
|
|
85
|
+
returns = risk_values(returns, name: 'return')
|
|
86
|
+
target = Validation.decimal(target, name: 'target')
|
|
87
|
+
squared_shortfalls =
|
|
88
|
+
returns.sum do |value|
|
|
89
|
+
shortfall = [value - target, Flt::DecNum(0)].min
|
|
90
|
+
shortfall**2
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
(squared_shortfalls / returns.size)**Flt::DecNum('0.5')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Sortino ratio using arithmetic mean excess return and downside deviation.
|
|
97
|
+
def self.sortino_ratio(returns:, target: 0, periods_per_year: nil)
|
|
98
|
+
returns = risk_values(returns, name: 'return')
|
|
99
|
+
target = Validation.decimal(target, name: 'target')
|
|
100
|
+
downside = downside_deviation(returns:, target:)
|
|
101
|
+
raise(ArgumentError, 'downside deviation must be greater than zero.') if downside.zero?
|
|
102
|
+
|
|
103
|
+
ratio = ((returns.sum / returns.size) - target) / downside
|
|
104
|
+
return ratio if periods_per_year.nil?
|
|
105
|
+
|
|
106
|
+
periods_per_year = Validation.positive_integer(periods_per_year, name: 'periods_per_year')
|
|
107
|
+
ratio * (Flt::DecNum(periods_per_year)**Flt::DecNum('0.5'))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Largest peak-to-trough decline as a non-negative fraction.
|
|
111
|
+
def self.max_drawdown(values:)
|
|
112
|
+
values = risk_values(values, name: 'value')
|
|
113
|
+
raise(ArgumentError, 'values must be greater than zero.') unless values.all?(&:positive?)
|
|
114
|
+
|
|
115
|
+
peak = values.first
|
|
116
|
+
values.reduce(Flt::DecNum(0)) do |maximum, value|
|
|
117
|
+
peak = value if value > peak
|
|
118
|
+
[maximum, (peak - value) / peak].max
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
20
122
|
# Computing Coefficient of variation
|
|
21
123
|
#
|
|
22
124
|
# @param sd standard deviation
|
data/lib/finrb/transaction.rb
CHANGED
|
@@ -4,17 +4,13 @@ require_relative 'validation'
|
|
|
4
4
|
|
|
5
5
|
module Finrb
|
|
6
6
|
# the Transaction class provides a general interface for working with individual cash flows.
|
|
7
|
-
# @api public
|
|
8
7
|
class Transaction
|
|
9
8
|
# @return [Flt::DecNum] the cash value of the transaction
|
|
10
|
-
# @api public
|
|
11
9
|
attr_reader :amount
|
|
12
10
|
# @return [Integer] the period number of the transaction
|
|
13
11
|
# @note this attribute is mainly used in the case of mortgage amortization with no dates
|
|
14
|
-
# @api public
|
|
15
12
|
attr_reader :period
|
|
16
13
|
# @return [Date] the date of the transaction
|
|
17
|
-
# @api public
|
|
18
14
|
attr_reader :date
|
|
19
15
|
|
|
20
16
|
# create a new Transaction
|
|
@@ -26,7 +22,6 @@ module Finrb
|
|
|
26
22
|
# t = Transaction.new(400)
|
|
27
23
|
# @example a transaction with a period number
|
|
28
24
|
# t = Transaction.new(400, :period => 3)
|
|
29
|
-
# @api public
|
|
30
25
|
def initialize(amount, opts = {})
|
|
31
26
|
raise(ArgumentError, 'options must be a Hash.') unless opts.is_a?(Hash)
|
|
32
27
|
raise(ArgumentError, 'options may only contain date and period.') unless (opts.keys - %i[date period]).empty?
|
|
@@ -47,7 +42,6 @@ module Finrb
|
|
|
47
42
|
# t = Transaction.new(500)
|
|
48
43
|
# t.amount = 750
|
|
49
44
|
# t.amount #=> 750
|
|
50
|
-
# @api public
|
|
51
45
|
def amount=(value)
|
|
52
46
|
@amount = Validation.decimal(value, name: 'amount')
|
|
53
47
|
end
|
|
@@ -71,7 +65,6 @@ module Finrb
|
|
|
71
65
|
# t = Transaction.new(500)
|
|
72
66
|
# t.amount = 750
|
|
73
67
|
# t.difference #=> Flt::DecNum('250')
|
|
74
|
-
# @api public
|
|
75
68
|
def difference
|
|
76
69
|
@amount - @original
|
|
77
70
|
end
|
|
@@ -82,12 +75,10 @@ module Finrb
|
|
|
82
75
|
# int = Interest.new(500)
|
|
83
76
|
# pmt.interest? #=> False
|
|
84
77
|
# int.interest? #=> True
|
|
85
|
-
# @api public
|
|
86
78
|
def interest?
|
|
87
79
|
instance_of?(Interest)
|
|
88
80
|
end
|
|
89
81
|
|
|
90
|
-
# @api public
|
|
91
82
|
def inspect
|
|
92
83
|
"Transaction(#{@amount.round(2)}, date: #{@date})"
|
|
93
84
|
end
|
|
@@ -99,7 +90,6 @@ module Finrb
|
|
|
99
90
|
# pmt = Payment.new(-500)
|
|
100
91
|
# pmt.modify { |t| t.amount-100 }
|
|
101
92
|
# pmt.amount #=> -600
|
|
102
|
-
# @api public
|
|
103
93
|
def modify
|
|
104
94
|
self.amount = yield(self)
|
|
105
95
|
end
|
|
@@ -116,7 +106,6 @@ module Finrb
|
|
|
116
106
|
# int = Interest.new(500)
|
|
117
107
|
# pmt.payment? #=> True
|
|
118
108
|
# int.payment? #=> False
|
|
119
|
-
# @api public
|
|
120
109
|
def payment?
|
|
121
110
|
instance_of?(Payment)
|
|
122
111
|
end
|
data/lib/finrb/version.rb
CHANGED
data/sig/finrb.rbs
CHANGED
|
@@ -94,14 +94,40 @@ module Finrb
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
class Amortization
|
|
97
|
-
|
|
97
|
+
class Entry
|
|
98
|
+
def initialize: (period: Integer, opening_balance: number, payment: number, interest: number, principal: number, additional_payment: number, balloon_payment: number, interest_only: bool, closing_balance: number) -> void
|
|
99
|
+
def period: () -> Integer
|
|
100
|
+
def opening_balance: () -> decimal
|
|
101
|
+
def payment: () -> decimal
|
|
102
|
+
def interest: () -> decimal
|
|
103
|
+
def principal: () -> decimal
|
|
104
|
+
def additional_payment: () -> decimal
|
|
105
|
+
def balloon_payment: () -> decimal
|
|
106
|
+
def interest_only: () -> bool
|
|
107
|
+
def interest_only?: () -> bool
|
|
108
|
+
def closing_balance: () -> decimal
|
|
109
|
+
def ==: (untyped) -> bool
|
|
110
|
+
def eql?: (untyped) -> bool
|
|
111
|
+
def hash: () -> Integer
|
|
112
|
+
def to_h: () -> Hash[Symbol, Integer | decimal]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.payment: (number, number, Integer, ?balloon: number) -> decimal
|
|
98
116
|
|
|
99
|
-
def initialize: (number, *Rate) ?{ (Transaction) -> number } -> void
|
|
117
|
+
def initialize: (number, *Rate, ?balloon: number, ?interest_only_periods: Integer, ?origination_fee: number, ?finance_origination_fee: bool) ?{ (Transaction) -> number } -> void
|
|
100
118
|
def ==: (untyped) -> bool
|
|
101
119
|
def balance: () -> decimal
|
|
120
|
+
def balloon: () -> decimal
|
|
121
|
+
def amount_financed: () -> decimal
|
|
122
|
+
def net_proceeds: () -> decimal
|
|
123
|
+
def origination_fee: () -> decimal
|
|
124
|
+
def finance_origination_fee: () -> bool
|
|
125
|
+
def finance_origination_fee?: () -> bool
|
|
126
|
+
def interest_only_periods: () -> Integer
|
|
102
127
|
def payment: () -> decimal?
|
|
103
128
|
def principal: () -> decimal
|
|
104
129
|
def rates: () -> Array[Rate]
|
|
130
|
+
def schedule: () -> Array[Entry]
|
|
105
131
|
def additional_payments: () -> Array[decimal]
|
|
106
132
|
def duration: () -> Integer
|
|
107
133
|
def inspect: () -> String
|
|
@@ -111,11 +137,13 @@ module Finrb
|
|
|
111
137
|
|
|
112
138
|
module Cashflow
|
|
113
139
|
def self.irr: (Enumerable[number], ?number) -> decimal
|
|
140
|
+
def self.mirr: (Enumerable[number], finance_rate: number, reinvestment_rate: number) -> decimal
|
|
114
141
|
def self.npv: (Enumerable[number], number) -> decimal
|
|
115
142
|
def self.xirr: (Enumerable[Transaction], ?number) -> Rate
|
|
116
143
|
def self.xnpv: (Enumerable[Transaction], number) -> decimal
|
|
117
144
|
|
|
118
145
|
def irr: (?number) -> decimal
|
|
146
|
+
def mirr: (finance_rate: number, reinvestment_rate: number) -> decimal
|
|
119
147
|
def npv: (number) -> decimal
|
|
120
148
|
def xirr: (?number) -> Rate
|
|
121
149
|
def xnpv: (number) -> decimal
|
|
@@ -163,15 +191,22 @@ module Finrb
|
|
|
163
191
|
end
|
|
164
192
|
|
|
165
193
|
module Returns
|
|
194
|
+
def self.annualize_return: (rate: number, periods_per_year: Integer) -> decimal
|
|
195
|
+
def self.annualize_volatility: (volatility: number, periods_per_year: Integer) -> decimal
|
|
196
|
+
def self.cagr: (beginning_value: number, ending_value: number, periods: Integer) -> decimal
|
|
166
197
|
def self.coefficient_variation: (sd: number, avg: number) -> decimal
|
|
167
198
|
def self.geometric_mean: (r: number | numbers) -> decimal
|
|
168
199
|
def self.harmonic_mean: (p: number | numbers) -> decimal
|
|
169
200
|
def self.hpr: (ev: number, bv: number, ?cfr: number) -> decimal
|
|
201
|
+
def self.downside_deviation: (returns: number | numbers, ?target: number) -> decimal
|
|
202
|
+
def self.max_drawdown: (values: number | numbers) -> decimal
|
|
170
203
|
def self.sampling_error: (sm: number, mu: number) -> decimal
|
|
171
204
|
def self.sf_ratio: (rp: number, rl: number, sd: number) -> decimal
|
|
172
205
|
def self.sharpe_ratio: (rp: number, rf: number, sd: number) -> decimal
|
|
206
|
+
def self.sortino_ratio: (returns: number | numbers, ?target: number, ?periods_per_year: Integer?) -> decimal
|
|
173
207
|
def self.twrr: (ev: number | numbers, bv: number | numbers, cfr: number | numbers) -> decimal
|
|
174
208
|
def self.wpr: (r: number | numbers, w: number | numbers) -> decimal
|
|
209
|
+
def self.volatility: (returns: number | numbers, ?sample: bool) -> decimal
|
|
175
210
|
end
|
|
176
211
|
|
|
177
212
|
module Yields
|