loan_creator 0.8.0 → 0.8.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 +4 -4
- data/CHANGELOG.md +7 -3
- data/README.md +8 -1
- data/lib/loan_creator/borrower_timetable.rb +1 -2
- data/lib/loan_creator/bullet.rb +15 -9
- data/lib/loan_creator/common.rb +38 -9
- data/lib/loan_creator/linear.rb +10 -13
- data/lib/loan_creator/standard.rb +19 -21
- data/lib/loan_creator/timetable.rb +11 -38
- data/lib/loan_creator/uncapitalized_bullet.rb +15 -9
- data/lib/loan_creator/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4dcaafbce29a00ebcdea57133619e566cc210be6ed613003af14e0a199cc126a
|
4
|
+
data.tar.gz: 2da6fe223cfc40fd8455a5c1ec0d8ef857bfd48effb22b285f46da7c3800a81b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 215f9a9b34c5ac6f9eef09f8658bbcd7c322e1964f0d4a0c9f55d3f97c99d1c9fc804c9e69f2c5509085bca9ec6ed848e92561ce456d6348d7b5ea343e38ada6
|
7
|
+
data.tar.gz: 98b6cf146fd802777c156266cd9378d8816ef00d4c4ab8aafdc092f4b52b001e2b8862d61a260502803f888f3b7f0f1b34bea5e29ea4a5843f8b01b5d99ce729
|
data/CHANGELOG.md
CHANGED
@@ -1,14 +1,18 @@
|
|
1
|
+
v0.8.1
|
2
|
+
-------------------------
|
3
|
+
- add `realistic_durations` option
|
4
|
+
|
1
5
|
v0.8.0
|
2
6
|
-------------------------
|
3
7
|
|
4
|
-
|
8
|
+
#### Feature
|
5
9
|
- due interests to date are now store into `due_interests_` columns for `LoanCreator::UncapitalizedBullet`
|
6
10
|
|
7
|
-
|
11
|
+
#### Bugfix
|
8
12
|
- fix `initial_values` not taking `paid_capital` and `paid_interests` into account for `LoanCreator::UncapitalizedBullet`
|
9
13
|
and `LoanCreator::Bullet`
|
10
14
|
|
11
|
-
|
15
|
+
#### Breaking changes
|
12
16
|
- rename columns `capitalized_interests_beginning_of_period` and `capitalized_interests_end_of_period` for
|
13
17
|
`due_interests_beginning_of_period` and `due_interests_end_of_period`
|
14
18
|
- rename `capitalized_interests` option in `:inital_values` into `due_interests`
|
data/README.md
CHANGED
@@ -44,9 +44,10 @@ Each instance of one of the previous classes has the following attributes:
|
|
44
44
|
:annual_interests_rate
|
45
45
|
:starts_at
|
46
46
|
:duration_in_periods
|
47
|
-
:deferred_in_periods (
|
47
|
+
:deferred_in_periods (defaults to zero)
|
48
48
|
:interests_start_date (optional)
|
49
49
|
:initial_values (to generate a timetable from a previous term or at a given state)
|
50
|
+
:realistic_durations (optional, defaults to false)
|
50
51
|
```
|
51
52
|
|
52
53
|
Initial values must be a hash with specific keys, like so:
|
@@ -102,6 +103,9 @@ There is also a `LoanCreator::Timetable` class dedicated to record the data of t
|
|
102
103
|
|
103
104
|
# Amount to pay this term
|
104
105
|
:period_amount_to_pay
|
106
|
+
|
107
|
+
# Whether or not to use the real number of days in each month
|
108
|
+
:realistic_durations
|
105
109
|
```
|
106
110
|
|
107
111
|
`#periodic_interests_rate` renders a precise calculation of the loan's periodic interests rate based on two inputs: `#annual_interests_rate` and `#period`.
|
@@ -176,6 +180,9 @@ additional term with only interests for the time difference.
|
|
176
180
|
For example, with a `start_at` in january 2020 and a `interests_start_date` in october 2019, the timetable will include a
|
177
181
|
first term corresponding to 3 months of interests.
|
178
182
|
|
183
|
+
`realistic_durations`: Optional. A boolean tha specifies whether or not to use the real number of days in each month when calculating the periodic interests rate. Note that leap years are taken into account. Default: `false`.
|
184
|
+
The default behaviour is to use the `period` in relation to the number of months in a year (ie: 12)
|
185
|
+
|
179
186
|
## Calculation
|
180
187
|
|
181
188
|
An excel simulator for standard case can be found [here](CapSens_Loan.xlsx).
|
@@ -21,8 +21,7 @@ module LoanCreator
|
|
21
21
|
|
22
22
|
borrower_timetable = LoanCreator::Timetable.new(
|
23
23
|
starting_index: lenders_timetables.first.starting_index,
|
24
|
-
|
25
|
-
period: lenders_timetables.first.period
|
24
|
+
loan: lenders_timetables.first.loan
|
26
25
|
)
|
27
26
|
|
28
27
|
# Borrower timetable is not concerned with computation-related value (delta, etc.),
|
data/lib/loan_creator/bullet.rb
CHANGED
@@ -7,18 +7,24 @@ module LoanCreator
|
|
7
7
|
reset_current_term
|
8
8
|
@crd_beginning_of_period = amount
|
9
9
|
@crd_end_of_period = amount
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
|
11
|
+
duration_in_periods.times { |idx| timetable << compute_current_term(idx, timetable) }
|
12
|
+
|
13
13
|
timetable
|
14
14
|
end
|
15
15
|
|
16
16
|
private
|
17
17
|
|
18
|
-
def
|
18
|
+
def compute_current_term(idx, timetable)
|
19
|
+
@due_on = timetable_term_dates[timetable.next_index]
|
20
|
+
last_period?(idx) ? compute_last_term(timetable) : compute_term(timetable)
|
21
|
+
current_term
|
22
|
+
end
|
23
|
+
|
24
|
+
def compute_last_term(timetable)
|
19
25
|
@crd_end_of_period = bigd('0')
|
20
26
|
@due_interests_beginning_of_period = @due_interests_end_of_period
|
21
|
-
@period_interests = @due_interests_end_of_period + compute_capitalized_interests
|
27
|
+
@period_interests = @due_interests_end_of_period + compute_capitalized_interests(@due_on, timetable)
|
22
28
|
@due_interests_end_of_period = 0
|
23
29
|
@period_capital = @crd_beginning_of_period
|
24
30
|
@total_paid_capital_end_of_period += @period_capital
|
@@ -26,14 +32,14 @@ module LoanCreator
|
|
26
32
|
@period_amount_to_pay = @period_capital + @period_interests
|
27
33
|
end
|
28
34
|
|
29
|
-
def compute_capitalized_interests
|
30
|
-
|
35
|
+
def compute_capitalized_interests(due_date, timetable)
|
36
|
+
computed_periodic_interests_rate = periodic_interests_rate(due_date, relative_to_date: timetable_term_dates[timetable.next_index - 1])
|
37
|
+
(amount + @due_interests_beginning_of_period).mult(computed_periodic_interests_rate, BIG_DECIMAL_DIGITS)
|
31
38
|
end
|
32
39
|
|
33
40
|
def compute_term(timetable)
|
34
41
|
@due_interests_beginning_of_period = @due_interests_end_of_period
|
35
|
-
@due_interests_end_of_period += compute_capitalized_interests
|
36
|
-
timetable << current_term
|
42
|
+
@due_interests_end_of_period += compute_capitalized_interests(@due_on, timetable)
|
37
43
|
end
|
38
44
|
end
|
39
45
|
end
|
data/lib/loan_creator/common.rb
CHANGED
@@ -21,7 +21,8 @@ module LoanCreator
|
|
21
21
|
# attribute: default_value
|
22
22
|
deferred_in_periods: 0,
|
23
23
|
interests_start_date: nil,
|
24
|
-
initial_values: {}
|
24
|
+
initial_values: {},
|
25
|
+
realistic_durations: false
|
25
26
|
}.freeze
|
26
27
|
|
27
28
|
attr_reader *REQUIRED_ATTRIBUTES
|
@@ -37,14 +38,26 @@ module LoanCreator
|
|
37
38
|
validate_initial_values
|
38
39
|
end
|
39
40
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
41
|
+
def periodic_interests_rate(date = nil, relative_to_date: nil)
|
42
|
+
if realistic_durations?
|
43
|
+
compute_realistic_periodic_interests_rate_percentage_for(date, relative_to_date: relative_to_date).div(100, BIG_DECIMAL_DIGITS)
|
44
|
+
else
|
45
|
+
@periodic_interests_rate ||=
|
46
|
+
annual_interests_rate.div(12 / PERIODS_IN_MONTHS[period], BIG_DECIMAL_DIGITS).div(100, BIG_DECIMAL_DIGITS)
|
47
|
+
end
|
43
48
|
end
|
44
49
|
|
45
|
-
def
|
46
|
-
@
|
47
|
-
|
50
|
+
def timetable_term_dates
|
51
|
+
@_timetable_term_dates ||= Hash.new do |dates, index|
|
52
|
+
dates[index] =
|
53
|
+
if index < 1
|
54
|
+
dates[index + 1].advance(months: -PERIODS_IN_MONTHS.fetch(period))
|
55
|
+
elsif index == 1
|
56
|
+
starts_on
|
57
|
+
else
|
58
|
+
starts_on.advance(months: PERIODS_IN_MONTHS.fetch(period) * (index - 1))
|
59
|
+
end
|
60
|
+
end
|
48
61
|
end
|
49
62
|
|
50
63
|
def lender_timetable
|
@@ -152,8 +165,7 @@ module LoanCreator
|
|
152
165
|
|
153
166
|
def new_timetable
|
154
167
|
LoanCreator::Timetable.new(
|
155
|
-
|
156
|
-
period: period,
|
168
|
+
loan: self,
|
157
169
|
interests_start_date: interests_start_date,
|
158
170
|
starting_index: @starting_index
|
159
171
|
)
|
@@ -163,6 +175,10 @@ module LoanCreator
|
|
163
175
|
@index ? (@starting_index + @index - 1) : nil
|
164
176
|
end
|
165
177
|
|
178
|
+
def last_period?(idx)
|
179
|
+
idx == (duration_in_periods - 1)
|
180
|
+
end
|
181
|
+
|
166
182
|
def compute_term_zero
|
167
183
|
@crd_beginning_of_period = @crd_end_of_period
|
168
184
|
@period_theoric_interests = term_zero_interests
|
@@ -172,6 +188,7 @@ module LoanCreator
|
|
172
188
|
@total_paid_interests_end_of_period += @period_interests
|
173
189
|
@period_amount_to_pay = @period_interests
|
174
190
|
@index = 0
|
191
|
+
@due_on = timetable_term_dates[0]
|
175
192
|
end
|
176
193
|
|
177
194
|
def term_zero_interests
|
@@ -194,5 +211,17 @@ module LoanCreator
|
|
194
211
|
def term_zero?
|
195
212
|
interests_start_date && interests_start_date < term_zero_date
|
196
213
|
end
|
214
|
+
|
215
|
+
def compute_realistic_periodic_interests_rate_percentage_for(date, relative_to_date:)
|
216
|
+
realistic_days = 365
|
217
|
+
realistic_days += 1 if date.leap?
|
218
|
+
realistic_days_in_period = (date - relative_to_date).to_i
|
219
|
+
|
220
|
+
annual_interests_rate.div(bigd(realistic_days) / bigd(realistic_days_in_period), BIG_DECIMAL_DIGITS)
|
221
|
+
end
|
222
|
+
|
223
|
+
def realistic_durations?
|
224
|
+
!!@realistic_durations
|
225
|
+
end
|
197
226
|
end
|
198
227
|
end
|
data/lib/loan_creator/linear.rb
CHANGED
@@ -11,26 +11,23 @@ module LoanCreator
|
|
11
11
|
timetable << current_term
|
12
12
|
end
|
13
13
|
|
14
|
-
duration_in_periods.times
|
15
|
-
@last_period = last_period?(idx)
|
16
|
-
@deferred_period = idx < deferred_in_periods
|
17
|
-
compute_current_term(idx)
|
18
|
-
timetable << current_term
|
19
|
-
end
|
14
|
+
duration_in_periods.times { |idx| timetable << compute_current_term(idx, timetable) }
|
20
15
|
|
21
16
|
timetable
|
22
17
|
end
|
23
18
|
|
24
19
|
private
|
25
20
|
|
26
|
-
def
|
27
|
-
|
28
|
-
|
21
|
+
def compute_current_term(idx, timetable)
|
22
|
+
@index = idx + 1
|
23
|
+
@last_period = last_period?(idx)
|
24
|
+
@deferred_period = @index <= deferred_in_periods
|
25
|
+
@due_on = timetable_term_dates[timetable.next_index]
|
26
|
+
computed_periodic_interests_rate = periodic_interests_rate(@due_on, relative_to_date: timetable_term_dates[timetable.next_index - 1])
|
29
27
|
|
30
|
-
def compute_current_term(idx)
|
31
28
|
# Reminder: CRD beginning of period = CRD end of period **of previous period**
|
32
29
|
@crd_beginning_of_period = @crd_end_of_period
|
33
|
-
@period_theoric_interests = @crd_beginning_of_period *
|
30
|
+
@period_theoric_interests = @crd_beginning_of_period * computed_periodic_interests_rate
|
34
31
|
@delta_interests = @period_theoric_interests - @period_theoric_interests.round(2)
|
35
32
|
@accrued_delta_interests += @delta_interests
|
36
33
|
@amount_to_add = bigd(
|
@@ -49,8 +46,8 @@ module LoanCreator
|
|
49
46
|
@total_paid_interests_end_of_period += @period_interests
|
50
47
|
@period_amount_to_pay = @period_interests + @period_capital
|
51
48
|
@crd_end_of_period -= @period_capital
|
52
|
-
|
53
|
-
|
49
|
+
|
50
|
+
current_term
|
54
51
|
end
|
55
52
|
|
56
53
|
def period_capital
|
@@ -12,24 +12,22 @@ module LoanCreator
|
|
12
12
|
timetable << current_term
|
13
13
|
end
|
14
14
|
|
15
|
-
duration_in_periods.times
|
16
|
-
|
17
|
-
@deferred_period = idx < deferred_in_periods
|
18
|
-
compute_current_term(idx)
|
19
|
-
timetable << current_term
|
20
|
-
end
|
15
|
+
duration_in_periods.times { |idx| timetable << compute_current_term(idx, timetable) }
|
16
|
+
|
21
17
|
timetable
|
22
18
|
end
|
23
19
|
|
24
20
|
private
|
25
21
|
|
26
|
-
def
|
27
|
-
|
28
|
-
|
22
|
+
def compute_current_term(idx, timetable)
|
23
|
+
@index = idx + 1
|
24
|
+
@last_period = last_period?(idx)
|
25
|
+
@deferred_period = @index <= deferred_in_periods
|
26
|
+
@due_on = timetable_term_dates[timetable.next_index]
|
27
|
+
computed_periodic_interests_rate = periodic_interests_rate(@due_on, relative_to_date: timetable_term_dates[timetable.next_index - 1])
|
29
28
|
|
30
|
-
def compute_current_term(idx)
|
31
29
|
@crd_beginning_of_period = @crd_end_of_period
|
32
|
-
@period_theoric_interests = period_theoric_interests(
|
30
|
+
@period_theoric_interests = period_theoric_interests(@index, computed_periodic_interests_rate)
|
33
31
|
@delta_interests = @period_theoric_interests - @period_theoric_interests.round(2)
|
34
32
|
@accrued_delta_interests += @delta_interests
|
35
33
|
@amount_to_add = bigd(
|
@@ -43,37 +41,37 @@ module LoanCreator
|
|
43
41
|
)
|
44
42
|
@accrued_delta_interests -= @amount_to_add
|
45
43
|
@period_interests = @period_theoric_interests.round(2) + @amount_to_add
|
46
|
-
@period_capital = period_capital(
|
44
|
+
@period_capital = period_capital(@index, computed_periodic_interests_rate)
|
47
45
|
@total_paid_capital_end_of_period += @period_capital
|
48
46
|
@total_paid_interests_end_of_period += @period_interests
|
49
47
|
@period_amount_to_pay = @period_interests + @period_capital
|
50
48
|
@crd_end_of_period -= @period_capital
|
51
|
-
|
52
|
-
|
49
|
+
|
50
|
+
current_term
|
53
51
|
end
|
54
52
|
|
55
|
-
def period_theoric_interests(idx)
|
53
|
+
def period_theoric_interests(idx, computed_periodic_interests_rate)
|
56
54
|
if @deferred_period
|
57
|
-
@crd_beginning_of_period *
|
55
|
+
@crd_beginning_of_period * computed_periodic_interests_rate
|
58
56
|
else
|
59
57
|
-ipmt(
|
60
|
-
|
61
|
-
|
58
|
+
computed_periodic_interests_rate,
|
59
|
+
idx - deferred_in_periods,
|
62
60
|
duration_in_periods - deferred_in_periods,
|
63
61
|
amount
|
64
62
|
)
|
65
63
|
end
|
66
64
|
end
|
67
65
|
|
68
|
-
def period_capital(idx)
|
66
|
+
def period_capital(idx, computed_periodic_interests_rate)
|
69
67
|
if @last_period
|
70
68
|
@crd_beginning_of_period
|
71
69
|
elsif @deferred_period
|
72
70
|
bigd(0)
|
73
71
|
else
|
74
72
|
-ppmt(
|
75
|
-
|
76
|
-
|
73
|
+
computed_periodic_interests_rate,
|
74
|
+
idx - deferred_in_periods,
|
77
75
|
duration_in_periods - deferred_in_periods,
|
78
76
|
amount
|
79
77
|
).round(2)
|
@@ -1,22 +1,13 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
module LoanCreator
|
3
3
|
class Timetable
|
4
|
-
|
5
|
-
PERIODS = {
|
6
|
-
month: {months: 1},
|
7
|
-
quarter: {months: 3},
|
8
|
-
semester: {months: 6},
|
9
|
-
year: {years: 1}
|
10
|
-
}
|
4
|
+
attr_reader :loan, :terms, :starting_index #, :interests_start_date
|
11
5
|
|
12
|
-
|
13
|
-
|
14
|
-
def initialize(starts_on:, period:, interests_start_date: nil, starting_index: 1)
|
15
|
-
raise ArgumentError.new(:period) unless PERIODS.keys.include?(period)
|
6
|
+
delegate :starts_on, :period, to: :loan
|
16
7
|
|
8
|
+
def initialize(loan:, interests_start_date: nil, starting_index: 1)
|
17
9
|
@terms = []
|
18
|
-
@
|
19
|
-
@period = period
|
10
|
+
@loan = loan
|
20
11
|
@starting_index = starting_index
|
21
12
|
|
22
13
|
if interests_start_date
|
@@ -26,8 +17,11 @@ module LoanCreator
|
|
26
17
|
|
27
18
|
def <<(term)
|
28
19
|
raise ArgumentError.new('LoanCreator::Term expected') unless term.is_a?(LoanCreator::Term)
|
29
|
-
|
30
|
-
|
20
|
+
|
21
|
+
@current_index = term.index || next_index
|
22
|
+
|
23
|
+
term.index = @current_index
|
24
|
+
term.due_on ||= loan.timetable_term_dates[term.index]
|
31
25
|
@terms << term
|
32
26
|
self
|
33
27
|
end
|
@@ -43,29 +37,8 @@ module LoanCreator
|
|
43
37
|
@terms.find { |term| term.index == index }
|
44
38
|
end
|
45
39
|
|
46
|
-
|
47
|
-
|
48
|
-
def autoincrement_index
|
49
|
-
@current_index = (@current_index.nil? ? @starting_index : @current_index + 1)
|
50
|
-
end
|
51
|
-
|
52
|
-
def date_for(index)
|
53
|
-
@_dates ||= Hash.new do |dates, index|
|
54
|
-
dates[index] =
|
55
|
-
if index < 1
|
56
|
-
dates[index + 1].advance(PERIODS.fetch(period).transform_values {|n| -n})
|
57
|
-
elsif index == 1
|
58
|
-
starts_on
|
59
|
-
else
|
60
|
-
dates[index - 1].advance(PERIODS.fetch(period))
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
@_dates[index]
|
65
|
-
end
|
66
|
-
|
67
|
-
def reset_dates
|
68
|
-
@_dates = nil
|
40
|
+
def next_index
|
41
|
+
@current_index.nil? ? @starting_index : @current_index + 1
|
69
42
|
end
|
70
43
|
end
|
71
44
|
end
|
@@ -7,18 +7,24 @@ module LoanCreator
|
|
7
7
|
reset_current_term
|
8
8
|
@crd_beginning_of_period = amount
|
9
9
|
@crd_end_of_period = amount
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
|
11
|
+
duration_in_periods.times { |idx| timetable << compute_current_term(idx, timetable) }
|
12
|
+
|
13
13
|
timetable
|
14
14
|
end
|
15
15
|
|
16
16
|
private
|
17
17
|
|
18
|
-
def
|
18
|
+
def compute_current_term(idx, timetable)
|
19
|
+
@due_on = timetable_term_dates[timetable.next_index]
|
20
|
+
last_period?(idx) ? compute_last_term(timetable) : compute_term(timetable)
|
21
|
+
current_term
|
22
|
+
end
|
23
|
+
|
24
|
+
def compute_last_term(timetable)
|
19
25
|
@crd_end_of_period = bigd('0')
|
20
26
|
@due_interests_beginning_of_period = @due_interests_end_of_period
|
21
|
-
@period_interests = @due_interests_end_of_period + compute_interests
|
27
|
+
@period_interests = @due_interests_end_of_period + compute_interests(@due_on, timetable)
|
22
28
|
@due_interests_end_of_period = 0
|
23
29
|
@period_capital = @crd_beginning_of_period
|
24
30
|
@total_paid_capital_end_of_period += @period_capital
|
@@ -26,14 +32,14 @@ module LoanCreator
|
|
26
32
|
@period_amount_to_pay = @period_capital + @period_interests
|
27
33
|
end
|
28
34
|
|
29
|
-
def compute_interests
|
30
|
-
|
35
|
+
def compute_interests(due_date, timetable)
|
36
|
+
computed_periodic_interests_rate = periodic_interests_rate(due_date, relative_to_date: timetable_term_dates[timetable.next_index - 1])
|
37
|
+
amount.mult(bigd(computed_periodic_interests_rate), BIG_DECIMAL_DIGITS)
|
31
38
|
end
|
32
39
|
|
33
40
|
def compute_term(timetable)
|
34
41
|
@due_interests_beginning_of_period = @due_interests_end_of_period
|
35
|
-
@due_interests_end_of_period += compute_interests
|
36
|
-
timetable << current_term
|
42
|
+
@due_interests_end_of_period += compute_interests(@due_on, timetable)
|
37
43
|
end
|
38
44
|
end
|
39
45
|
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.8.
|
4
|
+
version: 0.8.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- thibaulth
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: exe
|
14
14
|
cert_chain: []
|
15
|
-
date: 2021-02
|
15
|
+
date: 2021-04-02 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: bundler
|