loan_creator 0.2.3 → 0.5.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/CapSens_Loan.xlsx +0 -0
- data/README.md +43 -1
- data/lib/loan_creator/bullet.rb +1 -0
- data/lib/loan_creator/common.rb +44 -6
- data/lib/loan_creator/excel_formulas.rb +4 -0
- data/lib/loan_creator/in_fine.rb +1 -3
- data/lib/loan_creator/linear.rb +10 -2
- data/lib/loan_creator/standard.rb +8 -0
- data/lib/loan_creator/term.rb +4 -4
- data/lib/loan_creator/timetable.rb +39 -31
- data/lib/loan_creator/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ce94717f0c667263653597ea2037844157e6cebe
|
4
|
+
data.tar.gz: e498fe9069ca0ff9c3d765083a182f28ddc3ca22
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: da8108111a04280719eba3143444bd2ceac35abaade6521c2e9a5a3297266037c7d8e194156c3a373602fe766bbce49c23ebe515a2db1b64e1a35ac67a4aff06
|
7
|
+
data.tar.gz: 5e49dd7114a25fc67e9230ea0d6120b7a12bb83b6b6c50d28eecbfb7c92cc7bbd68da4ca26d0bf7dffe0b7b1fc2a7ec9acbbbb9369bf2cdfb1eb6837625dd2be
|
data/CapSens_Loan.xlsx
ADDED
Binary file
|
data/README.md
CHANGED
@@ -46,6 +46,7 @@ Each instance of one of the previous classes has the following attributes:
|
|
46
46
|
:starts_at
|
47
47
|
:duration_in_periods
|
48
48
|
:deferred_in_periods (default to zero)
|
49
|
+
:interests_start_date (optional)
|
49
50
|
```
|
50
51
|
|
51
52
|
There is also a `LoanCreator::Timetable` class dedicated to record the data of the loans' terms. Each instance of `LoanCreator::Timetable` represents an array of `LoanCreator::Term` records, each having the following attributes:
|
@@ -55,7 +56,7 @@ There is also a `LoanCreator::Timetable` class dedicated to record the data of t
|
|
55
56
|
:index
|
56
57
|
|
57
58
|
# Term date
|
58
|
-
:
|
59
|
+
:due_on
|
59
60
|
|
60
61
|
# Remaining due capital at the beginning of the term
|
61
62
|
:crd_beginning_of_period
|
@@ -99,8 +100,29 @@ support all those differences.
|
|
99
100
|
`.borrower_timetable(*lenders_timetables)` (class method) intends to sum each attribute of each provided `lender_timetable` on each term and thus to provide an ascending order array of `LoanCreator::Term`. It should be used for the borrower of a loan, once all lenders and their lending amounts
|
100
101
|
are known. It makes the borrower support all financial rounding differences.
|
101
102
|
|
103
|
+
## Example
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
loan_creator = LoanCreator::Standard.new(
|
107
|
+
period: :year,
|
108
|
+
amount: 42_000,
|
109
|
+
annual_interests_rate: 4,
|
110
|
+
starts_on: '2019-03-01',
|
111
|
+
duration_in_periods: 5,
|
112
|
+
deferred_in_periods: 1,
|
113
|
+
interests_start_date: '2019-02-10'
|
114
|
+
)
|
115
|
+
loan_creator.lender_timetable
|
116
|
+
# => #<LoanCreator::Timetable:0x0000000003198bd0 @terms=[...] ...>
|
117
|
+
loan_creator.lender_timetable.terms.first
|
118
|
+
# => #<LoanCreator::Term:0x00000000030f1a88 @crd_beginning_of_period=0.42e5,
|
119
|
+
# [...] @period_amount_to_pay=0.8745e2, @index=0, @due_on=Sun, 10 Feb 2019>
|
120
|
+
````
|
121
|
+
|
102
122
|
## Explanation
|
103
123
|
|
124
|
+
### Classes
|
125
|
+
|
104
126
|
`Standard` loan generates terms with constant payments.
|
105
127
|
|
106
128
|
`Linear` loan generates terms with constant capital share payment.
|
@@ -117,6 +139,26 @@ Capital share shall be repaid in full and all interests paid at loan's end.
|
|
117
139
|
|
118
140
|
There is no deferred time for `InFine` and `Bullet` loans as it would be equivalent to increasing loan's duration.
|
119
141
|
|
142
|
+
### Attributes
|
143
|
+
|
144
|
+
`period`: A `Symbol`. `:month`, `:quarter`, `:semester` or `:year`.
|
145
|
+
|
146
|
+
`duration_in_periods`: An `Integer`.
|
147
|
+
|
148
|
+
`amount`: Any number that can be converted to `BigDecimal`.
|
149
|
+
|
150
|
+
`annual_interests_rate`: In percentage. Any number that can be converted to `BigDecimal`.
|
151
|
+
|
152
|
+
`starts_at`: A `Date`, or a `String` that can be parsed.
|
153
|
+
|
154
|
+
`deferred_in_periods`: Optional. An `Integer`, smaller than `duration_in_periods`. Number of periods during which no
|
155
|
+
capital is refunded, only interest. Only relevant for `Standard` and `Linear` loans.
|
156
|
+
|
157
|
+
`interests_start_date`: Optional. To be used when the loan starts before the first full term date. This then compute an
|
158
|
+
additional term with only interests for the time difference.
|
159
|
+
For example, with a `start_at` in january 2020 and a `interests_start_date` in october 2019, the timetable will include a
|
160
|
+
first term corresponding to 3 months of interests.
|
161
|
+
|
120
162
|
## Development
|
121
163
|
|
122
164
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/loan_creator/bullet.rb
CHANGED
@@ -2,6 +2,7 @@ module LoanCreator
|
|
2
2
|
class Bullet < LoanCreator::Common
|
3
3
|
def lender_timetable
|
4
4
|
raise ArgumentError.new(:deferred_in_periods) unless deferred_in_periods == 0
|
5
|
+
raise ArgumentError.new(:interests_start_date) unless interests_start_date.nil?
|
5
6
|
timetable = new_timetable
|
6
7
|
reset_current_term
|
7
8
|
@crd_beginning_of_period = amount
|
data/lib/loan_creator/common.rb
CHANGED
@@ -19,7 +19,8 @@ module LoanCreator
|
|
19
19
|
|
20
20
|
OPTIONAL_ATTRIBUTES = {
|
21
21
|
# attribute: default_value
|
22
|
-
deferred_in_periods: 0
|
22
|
+
deferred_in_periods: 0,
|
23
|
+
interests_start_date: nil,
|
23
24
|
}.freeze
|
24
25
|
|
25
26
|
attr_reader *REQUIRED_ATTRIBUTES
|
@@ -48,7 +49,7 @@ module LoanCreator
|
|
48
49
|
end
|
49
50
|
|
50
51
|
def self.bigd(value)
|
51
|
-
BigDecimal
|
52
|
+
BigDecimal(value, BIG_DECIMAL_DIGITS)
|
52
53
|
end
|
53
54
|
|
54
55
|
def bigd(value)
|
@@ -65,7 +66,8 @@ module LoanCreator
|
|
65
66
|
@options[:period] = @options[:period].to_sym
|
66
67
|
@options[:amount] = bigd(@options[:amount])
|
67
68
|
@options[:annual_interests_rate] = bigd(@options[:annual_interests_rate])
|
68
|
-
@options[:starts_on] = @options[:starts_on]
|
69
|
+
@options[:starts_on] = Date.parse(@options[:starts_on]) if @options[:starts_on].is_a?(String)
|
70
|
+
@options[:interests_start_date] = Date.parse(@options[:interests_start_date]) if @options[:interests_start_date].is_a?(String)
|
69
71
|
end
|
70
72
|
|
71
73
|
def set_attributes
|
@@ -83,7 +85,7 @@ module LoanCreator
|
|
83
85
|
validate(:period) { |v| PERIODS_IN_MONTHS.keys.include?(v) }
|
84
86
|
validate(:amount) { |v| v.is_a?(BigDecimal) && v > 0 }
|
85
87
|
validate(:annual_interests_rate) { |v| v.is_a?(BigDecimal) && v >= 0 }
|
86
|
-
validate(:starts_on) { |v|
|
88
|
+
validate(:starts_on) { |v| v.is_a?(Date) }
|
87
89
|
validate(:duration_in_periods) { |v| v.is_a?(Integer) && v > 0 }
|
88
90
|
validate(:deferred_in_periods) { |v| v.is_a?(Integer) && v >= 0 && v < duration_in_periods }
|
89
91
|
end
|
@@ -100,6 +102,8 @@ module LoanCreator
|
|
100
102
|
@total_paid_capital_end_of_period = bigd('0')
|
101
103
|
@total_paid_interests_end_of_period = bigd('0')
|
102
104
|
@period_amount_to_pay = bigd('0')
|
105
|
+
@due_on = nil
|
106
|
+
@index = nil
|
103
107
|
end
|
104
108
|
|
105
109
|
def current_term
|
@@ -114,12 +118,46 @@ module LoanCreator
|
|
114
118
|
period_capital: @period_capital,
|
115
119
|
total_paid_capital_end_of_period: @total_paid_capital_end_of_period,
|
116
120
|
total_paid_interests_end_of_period: @total_paid_interests_end_of_period,
|
117
|
-
period_amount_to_pay: @period_amount_to_pay
|
121
|
+
period_amount_to_pay: @period_amount_to_pay,
|
122
|
+
due_on: @due_on,
|
123
|
+
index: @index
|
118
124
|
)
|
119
125
|
end
|
120
126
|
|
121
127
|
def new_timetable
|
122
|
-
LoanCreator::Timetable.new(starts_on: starts_on, period: period)
|
128
|
+
LoanCreator::Timetable.new(starts_on: starts_on, period: period, interests_start_date: interests_start_date)
|
129
|
+
end
|
130
|
+
|
131
|
+
def compute_term_zero
|
132
|
+
@crd_beginning_of_period = @crd_end_of_period
|
133
|
+
@period_theoric_interests = term_zero_interests
|
134
|
+
@delta_interests = @period_theoric_interests - @period_theoric_interests.round(2)
|
135
|
+
@accrued_delta_interests += @delta_interests
|
136
|
+
@period_interests = @period_theoric_interests.round(2)
|
137
|
+
@total_paid_interests_end_of_period += @period_interests
|
138
|
+
@period_amount_to_pay = @period_interests
|
139
|
+
@index = 0
|
140
|
+
end
|
141
|
+
|
142
|
+
def term_zero_interests
|
143
|
+
@crd_beginning_of_period * term_zero_interests_rate
|
144
|
+
end
|
145
|
+
|
146
|
+
def term_zero_interests_rate
|
147
|
+
term_zero_interests_rate_percentage = (annual_interests_rate * term_zero_duration).div(365, BIG_DECIMAL_DIGITS)
|
148
|
+
term_zero_interests_rate_percentage.div(100, BIG_DECIMAL_DIGITS)
|
149
|
+
end
|
150
|
+
|
151
|
+
def term_zero_duration
|
152
|
+
(term_zero_date - interests_start_date).to_i
|
153
|
+
end
|
154
|
+
|
155
|
+
def term_zero_date
|
156
|
+
starts_on.advance(months: -PERIODS_IN_MONTHS.fetch(@period))
|
157
|
+
end
|
158
|
+
|
159
|
+
def term_zero?
|
160
|
+
interests_start_date && interests_start_date < term_zero_date
|
123
161
|
end
|
124
162
|
end
|
125
163
|
end
|
@@ -1,12 +1,16 @@
|
|
1
1
|
# Source: https://gist.github.com/mattetti/1015948
|
2
2
|
|
3
3
|
module LoanCreator::ExcelFormulas
|
4
|
+
# Returns the interest payment
|
5
|
+
# for a given period for an investment based on periodic, constant payments and a constant interest rate.
|
4
6
|
def ipmt(rate, per, nper, pv, fv=0, type=0)
|
5
7
|
p = _pmt(rate, nper, pv, fv, 0);
|
6
8
|
ip = -(pv * _pow1p(rate, per - 1) * rate + p * _pow1pm1(rate, per - 1))
|
7
9
|
(type == 0) ? ip : ip / (1 + rate)
|
8
10
|
end
|
9
11
|
|
12
|
+
# Returns the payment on the principal
|
13
|
+
# for a given period for an investment based on periodic, constant payments and a constant interest rate.
|
10
14
|
def ppmt(rate, per, nper, pv, fv=0, type=0)
|
11
15
|
p = _pmt(rate, nper, pv, fv, type)
|
12
16
|
ip = ipmt(rate, per, nper, pv, fv, type)
|
data/lib/loan_creator/in_fine.rb
CHANGED
@@ -3,9 +3,7 @@ module LoanCreator
|
|
3
3
|
# InFine is the same as a Linear loan with (duration - 1) deferred periods.
|
4
4
|
# Thus we're generating a Linear loan instead of rewriting already existing code.
|
5
5
|
def lender_timetable
|
6
|
-
|
7
|
-
options = { deferred_in_periods: duration_in_periods - 1 }
|
8
|
-
options = REQUIRED_ATTRIBUTES.each_with_object(options) { |k,h| h[k] = send(k) }
|
6
|
+
options = @options.merge(deferred_in_periods: duration_in_periods - 1)
|
9
7
|
LoanCreator::Linear.new(options).lender_timetable
|
10
8
|
end
|
11
9
|
end
|
data/lib/loan_creator/linear.rb
CHANGED
@@ -5,10 +5,16 @@ module LoanCreator
|
|
5
5
|
timetable = new_timetable
|
6
6
|
reset_current_term
|
7
7
|
@crd_end_of_period = amount
|
8
|
+
|
9
|
+
if term_zero?
|
10
|
+
compute_term_zero
|
11
|
+
timetable << current_term
|
12
|
+
end
|
13
|
+
|
8
14
|
duration_in_periods.times do |idx|
|
9
15
|
@last_period = last_period?(idx)
|
10
16
|
@deferred_period = idx < deferred_in_periods
|
11
|
-
compute_current_term
|
17
|
+
compute_current_term(idx)
|
12
18
|
timetable << current_term
|
13
19
|
end
|
14
20
|
timetable
|
@@ -20,7 +26,7 @@ module LoanCreator
|
|
20
26
|
idx == (duration_in_periods - 1)
|
21
27
|
end
|
22
28
|
|
23
|
-
def compute_current_term
|
29
|
+
def compute_current_term(idx)
|
24
30
|
# Reminder: CRD beginning of period = CRD end of period **of previous period**
|
25
31
|
@crd_beginning_of_period = @crd_end_of_period
|
26
32
|
@period_theoric_interests = @crd_beginning_of_period * periodic_interests_rate
|
@@ -42,6 +48,8 @@ module LoanCreator
|
|
42
48
|
@total_paid_interests_end_of_period += @period_interests
|
43
49
|
@period_amount_to_pay = @period_interests + @period_capital
|
44
50
|
@crd_end_of_period -= @period_capital
|
51
|
+
@due_on = nil
|
52
|
+
@index = idx + 1
|
45
53
|
end
|
46
54
|
|
47
55
|
def period_capital
|
@@ -6,6 +6,12 @@ module LoanCreator
|
|
6
6
|
timetable = new_timetable
|
7
7
|
reset_current_term
|
8
8
|
@crd_end_of_period = amount
|
9
|
+
|
10
|
+
if term_zero?
|
11
|
+
compute_term_zero
|
12
|
+
timetable << current_term
|
13
|
+
end
|
14
|
+
|
9
15
|
duration_in_periods.times do |idx|
|
10
16
|
@last_period = last_period?(idx)
|
11
17
|
@deferred_period = idx < deferred_in_periods
|
@@ -42,6 +48,8 @@ module LoanCreator
|
|
42
48
|
@total_paid_interests_end_of_period += @period_interests
|
43
49
|
@period_amount_to_pay = @period_interests + @period_capital
|
44
50
|
@crd_end_of_period -= @period_capital
|
51
|
+
@due_on = nil
|
52
|
+
@index = idx + 1
|
45
53
|
end
|
46
54
|
|
47
55
|
def period_theoric_interests(idx)
|
data/lib/loan_creator/term.rb
CHANGED
@@ -36,7 +36,7 @@ module LoanCreator
|
|
36
36
|
:period_amount_to_pay
|
37
37
|
].freeze
|
38
38
|
|
39
|
-
|
39
|
+
OPTIONAL_ARGUMENTS = [
|
40
40
|
# Term number (starts at 1)
|
41
41
|
# This value is to be set by Timetable
|
42
42
|
:index,
|
@@ -44,15 +44,15 @@ module LoanCreator
|
|
44
44
|
# Term date
|
45
45
|
# This value is to be set by Timetable
|
46
46
|
:due_on,
|
47
|
+
]
|
47
48
|
|
48
|
-
|
49
|
-
*ARGUMENTS
|
50
|
-
].freeze
|
49
|
+
ATTRIBUTES = (ARGUMENTS + OPTIONAL_ARGUMENTS).freeze
|
51
50
|
|
52
51
|
attr_accessor *ATTRIBUTES
|
53
52
|
|
54
53
|
def initialize(**options)
|
55
54
|
ARGUMENTS.each { |k| instance_variable_set(:"@#{k}", options.fetch(k)) }
|
55
|
+
OPTIONAL_ARGUMENTS.each { |k| instance_variable_set(:"@#{k}", options.fetch(k, nil)) }
|
56
56
|
end
|
57
57
|
|
58
58
|
def to_csv
|
@@ -3,39 +3,34 @@ module LoanCreator
|
|
3
3
|
class Timetable
|
4
4
|
# Used to calculate next term's date (see ActiveSupport#advance)
|
5
5
|
PERIODS = {
|
6
|
-
month:
|
7
|
-
quarter:
|
8
|
-
semester: {
|
9
|
-
year:
|
6
|
+
month: {months: 1},
|
7
|
+
quarter: {months: 3},
|
8
|
+
semester: {months: 6},
|
9
|
+
year: {years: 1}
|
10
10
|
}
|
11
11
|
|
12
|
-
attr_reader :terms, :starts_on, :period
|
12
|
+
attr_reader :terms, :starts_on, :period #, :interests_start_date
|
13
13
|
|
14
|
-
def initialize(starts_on:, period:)
|
15
|
-
@terms = []
|
16
|
-
@starts_on = (Date === starts_on ? starts_on : Date.parse(starts_on))
|
14
|
+
def initialize(starts_on:, period:, interests_start_date: nil)
|
17
15
|
raise ArgumentError.new(:period) unless PERIODS.keys.include?(period)
|
18
|
-
|
16
|
+
|
17
|
+
@terms = []
|
18
|
+
@starts_on = (starts_on.is_a?(Date) ? starts_on : Date.parse(starts_on))
|
19
|
+
@period = period
|
20
|
+
|
21
|
+
if interests_start_date
|
22
|
+
@interests_start_date = (interests_start_date.is_a?(Date) ? interests_start_date : Date.parse(interests_start_date))
|
23
|
+
end
|
19
24
|
end
|
20
25
|
|
21
26
|
def <<(term)
|
22
|
-
raise ArgumentError.new('LoanCreator::Term expected') unless LoanCreator::Term
|
23
|
-
term.index
|
24
|
-
term.due_on
|
27
|
+
raise ArgumentError.new('LoanCreator::Term expected') unless term.is_a?(LoanCreator::Term)
|
28
|
+
term.index ||= autoincrement_index
|
29
|
+
term.due_on ||= date_for(term.index)
|
25
30
|
@terms << term
|
26
31
|
self
|
27
32
|
end
|
28
33
|
|
29
|
-
def reset_indexes_and_due_on_dates
|
30
|
-
@autoincrement_index = 0
|
31
|
-
@autoincrement_date = @starts_on
|
32
|
-
@terms.each do |term|
|
33
|
-
term[:index] = autoincrement_index
|
34
|
-
term[:due_on] = autoincrement_date
|
35
|
-
end
|
36
|
-
self
|
37
|
-
end
|
38
|
-
|
39
34
|
def to_csv(header: true)
|
40
35
|
output = []
|
41
36
|
output << terms.first.to_h.keys.join(',') if header
|
@@ -43,20 +38,33 @@ module LoanCreator
|
|
43
38
|
output
|
44
39
|
end
|
45
40
|
|
41
|
+
def term(index)
|
42
|
+
@terms.find { |term| term.index == index }
|
43
|
+
end
|
44
|
+
|
46
45
|
private
|
47
46
|
|
48
|
-
# First term index of a timetable term is 1
|
49
47
|
def autoincrement_index
|
50
|
-
@
|
51
|
-
|
48
|
+
@current_index = @current_index.nil? ? 1 : @current_index + 1
|
49
|
+
end
|
50
|
+
|
51
|
+
def date_for(index)
|
52
|
+
@_dates ||= Hash.new do |dates, index|
|
53
|
+
dates[index] =
|
54
|
+
if index < 1
|
55
|
+
dates[index + 1].advance(PERIODS.fetch(period).transform_values {|n| -n})
|
56
|
+
elsif index == 1
|
57
|
+
starts_on
|
58
|
+
else
|
59
|
+
dates[index - 1].advance(PERIODS.fetch(period))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
@_dates[index]
|
52
64
|
end
|
53
65
|
|
54
|
-
|
55
|
-
|
56
|
-
@autoincrement_date ||= @starts_on
|
57
|
-
date = @autoincrement_date
|
58
|
-
@autoincrement_date = @autoincrement_date.advance(PERIODS.fetch(@period))
|
59
|
-
date
|
66
|
+
def reset_dates
|
67
|
+
@_dates = nil
|
60
68
|
end
|
61
69
|
end
|
62
70
|
end
|
data/lib/loan_creator/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: loan_creator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- thibaulth
|
@@ -126,6 +126,7 @@ files:
|
|
126
126
|
- ".ruby-gemset"
|
127
127
|
- ".ruby-version"
|
128
128
|
- ".travis.yml"
|
129
|
+
- CapSens_Loan.xlsx
|
129
130
|
- Gemfile
|
130
131
|
- LICENSE.txt
|
131
132
|
- README.md
|