fast_xirr 1.0.2 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4836e4f3499607c058d627e0718c1415bc7bfd23f750f9df413dc6c4527ee6a7
4
- data.tar.gz: 9331bdb84b707c270c5e29d914dd9e6eeb814d102a1e0b3a25322e3e67435c73
3
+ metadata.gz: f240b5f7c4ffe72a091fea47502e7bf16a206502eb7b498131f68489698353a4
4
+ data.tar.gz: 541df15f67a6e62dc1d8c2a07db6673bd8b806b98a22dc7832a743824add6078
5
5
  SHA512:
6
- metadata.gz: e130e92d55d75e71b706ece17b6318c36924c69aae09b1b34320369a912b2ead4dc76caded2446ebe93389a3f6db9a55f65979a108a68feefadfe1511b87b39f
7
- data.tar.gz: fed653b1893283b35d72846300995bee86e7fb452fcb66cda23d2285bcf97fda3b99c7c2a83719ed275336944e7aed712051b1d6f2031db64624705ef35256b7
6
+ metadata.gz: 15e8c80e533662528318fa0bd4a488c27c1fe30532d6b483acaf05e7407a67a17d899cad206d455d4d13a7ddcec929f7a265ba926222fe07881c9034f6718c89
7
+ data.tar.gz: deb52dffd457cab0bf5a730d75b26ed61cd0e87c6dc27708f2a515b61fd08edaeece7e09d365b3e08748cfab53f9bff87a404e7d8aa92ff091ed328769316336
data/README.md CHANGED
@@ -66,7 +66,7 @@ result.nan?
66
66
  # => true
67
67
  ```
68
68
 
69
- Tolerance can be set to a custom value (default is 1e-7), as well as the maximum number of iterations (default is 1e10).
69
+ Tolerance can be set to a custom value (default is 1e-7), as well as the maximum number of iterations (default is 1e10). You can also specify the initial bracket interval for Brent's method (default is [-0.3, 10.0]) to help the algorithm converge in specific ranges. If the algorith doesn't converge, it will expand the brackets to [0.999, 100.0].
70
70
 
71
71
 
72
72
  ```ruby
@@ -86,6 +86,14 @@ puts "XIRR: #{result}"
86
86
  result = FastXirr.calculate(cashflows: cashflows, tol: 1e-8, max_iter: 2)
87
87
  puts "XIRR: #{result}"
88
88
  # => XIRR: NaN
89
+
90
+ # You can also specify the initial bracket (search interval) for Brent's method
91
+ result = FastXirr.calculate(
92
+ cashflows: cashflows,
93
+ initial_bracket: [-0.5, 5.0]
94
+ )
95
+ puts "XIRR: #{result}"
96
+ # => XIRR: 0.22568333743016633
89
97
  ```
90
98
 
91
99
  ## Build and test
@@ -1,12 +1,13 @@
1
- #include <ruby.h>
2
- #include <stdint.h>
3
- #include <math.h>
4
1
  #include "bisection.h"
5
2
  #include "common.h"
3
+ #include <math.h>
4
+ #include <ruby.h>
5
+ #include <stdint.h>
6
6
 
7
7
  /**
8
8
  * Bisection Method for finding the root of a function within a given interval.
9
- * This method iteratively narrows the interval where the root is located by halving the interval.
9
+ * This method iteratively narrows the interval where the root is located by
10
+ * halving the interval.
10
11
  *
11
12
  * @param cashflows Array of CashFlow structures containing amount and date.
12
13
  * @param count Number of elements in the cashflows array.
@@ -17,45 +18,46 @@
17
18
  *
18
19
  * @return The estimated root (XIRR) or NAN if it fails to converge.
19
20
  */
20
- double bisection_method(CashFlow *cashflows, long long count, double tol, long long max_iter, double low, double high) {
21
- double mid, f_low, f_mid, f_high;
21
+ double bisection_method(CashFlow *cashflows, long long count, double tol,
22
+ long long max_iter, double low, double high) {
23
+ double mid, f_low, f_mid, f_high;
22
24
 
23
- //If cashflows are empty, return 0
24
- if (count == 0) {
25
- return 0.0;
26
- }
25
+ // If cashflows are empty, return 0
26
+ if (count == 0) {
27
+ return 0.0;
28
+ }
27
29
 
28
- // Calculate the NPV at the boundaries of the interval
29
- f_low = npv(low, cashflows, count, cashflows[0].date);
30
- f_high = npv(high, cashflows, count, cashflows[0].date);
30
+ // Calculate the NPV at the boundaries of the interval
31
+ f_low = npv(low, cashflows, count, cashflows[0].date);
32
+ f_high = npv(high, cashflows, count, cashflows[0].date);
31
33
 
32
- // Ensure the root is bracketed
33
- if (f_low * f_high > 0) {
34
- return NAN; // Root is not bracketed
35
- }
34
+ // Ensure the root is bracketed
35
+ if (f_low * f_high > 0) {
36
+ return NAN; // Root is not bracketed
37
+ }
36
38
 
37
- // Iteratively apply the bisection method
38
- for (long long iter = 0; iter < max_iter; iter++) {
39
- mid = (low + high) / 2.0;
40
- f_mid = npv(mid, cashflows, count, cashflows[0].date);
39
+ // Iteratively apply the bisection method
40
+ for (long long iter = 0; iter < max_iter; iter++) {
41
+ mid = (low + high) / 2.0;
42
+ f_mid = npv(mid, cashflows, count, cashflows[0].date);
41
43
 
42
- // Check for convergence
43
- if (fabs(f_mid) < tol || fabs(high - low) < tol) {
44
- return mid;
45
- }
44
+ // Check for convergence
45
+ if (fabs(f_mid) < tol || fabs(high - low) < tol) {
46
+ return mid;
47
+ }
46
48
 
47
- // Narrow the interval
48
- if (f_low * f_mid < 0) {
49
- high = mid;
50
- f_high = f_mid;
51
- } else {
52
- low = mid;
53
- f_low = f_mid;
54
- }
49
+ // Narrow the interval
50
+ if (f_low * f_mid < 0) {
51
+ high = mid;
52
+ f_high = f_mid;
53
+ } else {
54
+ low = mid;
55
+ f_low = f_mid;
55
56
  }
57
+ }
56
58
 
57
- // If we reach here, it means we failed to converge
58
- return NAN;
59
+ // If we reach here, it means we failed to converge
60
+ return NAN;
59
61
  }
60
62
 
61
63
  /**
@@ -68,38 +70,40 @@ double bisection_method(CashFlow *cashflows, long long count, double tol, long l
68
70
  *
69
71
  * @return Ruby float with the calculated XIRR.
70
72
  */
71
- VALUE calculate_xirr_with_bisection(VALUE self, VALUE rb_cashflows, VALUE rb_tol, VALUE rb_max_iter) {
72
- // Get the number of cash flows
73
- long long count = RARRAY_LEN(rb_cashflows);
74
- CashFlow cashflows[count];
73
+ VALUE calculate_xirr_with_bisection(VALUE self, VALUE rb_cashflows,
74
+ VALUE rb_tol, VALUE rb_max_iter) {
75
+ // Get the number of cash flows
76
+ long long count = RARRAY_LEN(rb_cashflows);
77
+ CashFlow cashflows[count];
75
78
 
76
- // Convert Ruby cash flows array to C array
77
- for (long long i = 0; i < count; i++) {
78
- VALUE rb_cashflow = rb_ary_entry(rb_cashflows, i);
79
- cashflows[i].amount = NUM2DBL(rb_ary_entry(rb_cashflow, 0));
80
- cashflows[i].date = (int64_t)NUM2LL(rb_ary_entry(rb_cashflow, 1));
81
- }
79
+ // Convert Ruby cash flows array to C array
80
+ for (long long i = 0; i < count; i++) {
81
+ VALUE rb_cashflow = rb_ary_entry(rb_cashflows, i);
82
+ cashflows[i].amount = NUM2DBL(rb_ary_entry(rb_cashflow, 0));
83
+ cashflows[i].date = (int64_t)NUM2LL(rb_ary_entry(rb_cashflow, 1));
84
+ }
82
85
 
83
- // Convert tolerance and max iterations to C types
84
- double tol = NUM2DBL(rb_tol);
85
- long long max_iter = NUM2LL(rb_max_iter);
86
+ // Convert tolerance and max iterations to C types
87
+ double tol = NUM2DBL(rb_tol);
88
+ long long max_iter = NUM2LL(rb_max_iter);
86
89
 
87
- // Initial standard bracketing interval
88
- double low = -0.999999, high = 100.0;
90
+ // Initial standard bracketing interval
91
+ double low = -0.999999, high = 100.0;
89
92
 
90
- // Try Bisection method with the standard interval
91
- double result = bisection_method(cashflows, count, tol, max_iter, low, high);
92
- if (!isnan(result)) {
93
- return rb_float_new(result);
94
- }
93
+ // Try Bisection method with the standard interval
94
+ double result = bisection_method(cashflows, count, tol, max_iter, low, high);
95
+ if (!isnan(result)) {
96
+ return rb_float_new(result);
97
+ }
95
98
 
96
- // If the standard interval fails, try to find a better bracketing interval
97
- low = -0.9999999; high = 1000.0;
98
- if (find_bracketing_interval(cashflows, count, &low, &high)) {
99
- result = bisection_method(cashflows, count, tol, max_iter, low, high);
100
- return rb_float_new(result);
101
- }
99
+ // If the standard interval fails, try to find a better bracketing interval
100
+ low = -0.9999999;
101
+ high = 1000.0;
102
+ if (find_bracketing_interval(cashflows, count, &low, &high)) {
103
+ result = bisection_method(cashflows, count, tol, max_iter, low, high);
104
+ return rb_float_new(result);
105
+ }
102
106
 
103
- // If no interval is found, return NAN
104
- return rb_float_new(NAN);
107
+ // If no interval is found, return NAN
108
+ return rb_float_new(NAN);
105
109
  }
@@ -3,6 +3,7 @@
3
3
 
4
4
  #include <ruby.h>
5
5
 
6
- VALUE calculate_xirr_with_bisection(VALUE self, VALUE rb_cashflows, VALUE rb_tol, VALUE rb_max_iter);
6
+ VALUE calculate_xirr_with_bisection(VALUE self, VALUE rb_cashflows,
7
+ VALUE rb_tol, VALUE rb_max_iter);
7
8
 
8
9
  #endif
@@ -1,12 +1,13 @@
1
- #include <ruby.h>
2
- #include <stdint.h>
3
- #include <math.h>
4
1
  #include "brent.h"
5
2
  #include "common.h"
3
+ #include <math.h>
4
+ #include <ruby.h>
5
+ #include <stdint.h>
6
6
 
7
7
  /**
8
8
  * Brent's Method for finding the root of a function within a given interval.
9
- * This method combines root bracketing, bisection, secant, and inverse quadratic interpolation.
9
+ * This method combines root bracketing, bisection, secant, and inverse
10
+ * quadratic interpolation.
10
11
  *
11
12
  * @param cashflows Array of CashFlow structures containing amount and date.
12
13
  * @param count Number of elements in the cashflows array.
@@ -17,96 +18,98 @@
17
18
  *
18
19
  * @return The estimated root (XIRR) or NAN if it fails to converge.
19
20
  */
20
- double brent_method(CashFlow *cashflows, long long count, double tol, long long max_iter, double low, double high) {
21
- //If cashflows are empty, return 0
22
- if (count == 0) {
23
- return 0.0;
21
+ double brent_method(CashFlow *cashflows, long long count, double tol,
22
+ long long max_iter, double low, double high) {
23
+ // If cashflows are empty, return 0
24
+ if (count == 0) {
25
+ return 0.0;
26
+ }
27
+
28
+ // Calculate the NPV at the boundaries of the interval
29
+ double fa = npv(low, cashflows, count, cashflows[0].date);
30
+ double fb = npv(high, cashflows, count, cashflows[0].date);
31
+
32
+ // Ensure the root is bracketed
33
+ if (fa * fb > 0) {
34
+ return NAN; // Root is not bracketed
35
+ }
36
+
37
+ double c = low, fc = fa, s, d = 0.0, e = 0.0;
38
+
39
+ // Iteratively apply Brent's method
40
+ for (long long iter = 0; iter < max_iter; iter++) {
41
+ if (fb * fc > 0) {
42
+ // Adjust c to ensure that f(b) and f(c) have opposite signs
43
+ c = low;
44
+ fc = fa;
45
+ d = e = high - low;
46
+ }
47
+ if (fabs(fc) < fabs(fb)) {
48
+ // Swap low and high to make f(b) smaller in magnitude
49
+ low = high;
50
+ high = c;
51
+ c = low;
52
+ fa = fb;
53
+ fb = fc;
54
+ fc = fa;
24
55
  }
25
56
 
26
- // Calculate the NPV at the boundaries of the interval
27
- double fa = npv(low, cashflows, count, cashflows[0].date);
28
- double fb = npv(high, cashflows, count, cashflows[0].date);
57
+ // Tolerance calculation for convergence check
58
+ double tol1 = 2 * tol * fabs(high) + 0.5 * tol;
59
+ double m = 0.5 * (c - high);
29
60
 
30
- // Ensure the root is bracketed
31
- if (fa * fb > 0) {
32
- return NAN; // Root is not bracketed
61
+ // Check for convergence
62
+ if (fabs(m) <= tol1 || fb == 0.0) {
63
+ return high;
33
64
  }
34
65
 
35
- double c = low, fc = fa, s, d = 0.0, e = 0.0;
36
-
37
- // Iteratively apply Brent's method
38
- for (long long iter = 0; iter < max_iter; iter++) {
39
- if (fb * fc > 0) {
40
- // Adjust c to ensure that f(b) and f(c) have opposite signs
41
- c = low;
42
- fc = fa;
43
- d = e = high - low;
44
- }
45
- if (fabs(fc) < fabs(fb)) {
46
- // Swap low and high to make f(b) smaller in magnitude
47
- low = high;
48
- high = c;
49
- c = low;
50
- fa = fb;
51
- fb = fc;
52
- fc = fa;
53
- }
54
-
55
- // Tolerance calculation for convergence check
56
- double tol1 = 2 * tol * fabs(high) + 0.5 * tol;
57
- double m = 0.5 * (c - high);
58
-
59
- // Check for convergence
60
- if (fabs(m) <= tol1 || fb == 0.0) {
61
- return high;
62
- }
63
-
64
- // Use inverse quadratic interpolation if conditions are met
65
- if (fabs(e) >= tol1 && fabs(fa) > fabs(fb)) {
66
- double p, q, r;
67
- s = fb / fa;
68
- if (low == c) {
69
- // Use the secant method
70
- p = 2 * m * s;
71
- q = 1 - s;
72
- } else {
73
- // Use inverse quadratic interpolation
74
- q = fa / fc;
75
- r = fb / fc;
76
- p = s * (2 * m * q * (q - r) - (high - low) * (r - 1));
77
- q = (q - 1) * (r - 1) * (s - 1);
78
- }
79
- if (p > 0) q = -q;
80
- p = fabs(p);
81
- if (2 * p < fmin(3 * m * q - fabs(tol1 * q), fabs(e * q))) {
82
- // Accept interpolation
83
- e = d;
84
- d = p / q;
85
- } else {
86
- // Use bisection
87
- d = m;
88
- e = m;
89
- }
90
- } else {
91
- // Use bisection
92
- d = m;
93
- e = m;
94
- }
95
-
96
- // Update bounds
97
- low = high;
98
- fa = fb;
99
-
100
- // Update the new high value
101
- if (fabs(d) > tol1) {
102
- high += d;
103
- } else {
104
- high += copysign(tol1, m);
105
- }
106
- fb = npv(high, cashflows, count, cashflows[0].date);
66
+ // Use inverse quadratic interpolation if conditions are met
67
+ if (fabs(e) >= tol1 && fabs(fa) > fabs(fb)) {
68
+ double p, q, r;
69
+ s = fb / fa;
70
+ if (low == c) {
71
+ // Use the secant method
72
+ p = 2 * m * s;
73
+ q = 1 - s;
74
+ } else {
75
+ // Use inverse quadratic interpolation
76
+ q = fa / fc;
77
+ r = fb / fc;
78
+ p = s * (2 * m * q * (q - r) - (high - low) * (r - 1));
79
+ q = (q - 1) * (r - 1) * (s - 1);
80
+ }
81
+ if (p > 0)
82
+ q = -q;
83
+ p = fabs(p);
84
+ if (2 * p < fmin(3 * m * q - fabs(tol1 * q), fabs(e * q))) {
85
+ // Accept interpolation
86
+ e = d;
87
+ d = p / q;
88
+ } else {
89
+ // Use bisection
90
+ d = m;
91
+ e = m;
92
+ }
93
+ } else {
94
+ // Use bisection
95
+ d = m;
96
+ e = m;
107
97
  }
108
-
109
- return NAN; // Failed to converge
98
+
99
+ // Update bounds
100
+ low = high;
101
+ fa = fb;
102
+
103
+ // Update the new high value
104
+ if (fabs(d) > tol1) {
105
+ high += d;
106
+ } else {
107
+ high += copysign(tol1, m);
108
+ }
109
+ fb = npv(high, cashflows, count, cashflows[0].date);
110
+ }
111
+
112
+ return NAN; // Failed to converge
110
113
  }
111
114
 
112
115
  /**
@@ -116,44 +119,59 @@ double brent_method(CashFlow *cashflows, long long count, double tol, long long
116
119
  * @param rb_cashflows Ruby array of cash flows.
117
120
  * @param rb_tol Ruby float for tolerance.
118
121
  * @param rb_max_iter Ruby integer for maximum iterations.
122
+ * @param rb_brackets Ruby array with [low, high] bounds for search interval, or
123
+ * nil for defaults. Default interval is [-0.3, 10.0].
119
124
  *
120
125
  * @return Ruby float with the calculated XIRR.
121
126
  */
122
- VALUE calculate_xirr_with_brent(VALUE self, VALUE rb_cashflows, VALUE rb_tol, VALUE rb_max_iter) {
123
- // Get the number of cash flows
124
- long long count = (long long)RARRAY_LEN(rb_cashflows);
125
- CashFlow cashflows[count];
126
-
127
- // Convert Ruby cash flows array to C array
128
- for (long long i = 0; i < count; i++) {
129
- VALUE rb_cashflow = rb_ary_entry(rb_cashflows, i);
130
- cashflows[i].amount = NUM2DBL(rb_ary_entry(rb_cashflow, 0));
131
- cashflows[i].date = (int64_t)NUM2LL(rb_ary_entry(rb_cashflow, 1));
127
+ VALUE calculate_xirr_with_brent(VALUE self, VALUE rb_cashflows, VALUE rb_tol,
128
+ VALUE rb_max_iter, VALUE rb_brackets) {
129
+ // Get the number of cash flows
130
+ long long total_count = (long long)RARRAY_LEN(rb_cashflows);
131
+ CashFlow cashflows[total_count];
132
+ long long filtered_count = 0;
133
+
134
+ // Convert Ruby cash flows array to C array, filtering out zero amounts
135
+ for (long long i = 0; i < total_count; i++) {
136
+ VALUE rb_cashflow = rb_ary_entry(rb_cashflows, i);
137
+ double amount = NUM2DBL(rb_ary_entry(rb_cashflow, 0));
138
+ if (amount != 0.0) {
139
+ cashflows[filtered_count].amount = amount;
140
+ cashflows[filtered_count].date = (int64_t)NUM2LL(rb_ary_entry(rb_cashflow, 1));
141
+ filtered_count++;
132
142
  }
143
+ }
133
144
 
145
+ // Convert tolerance and max iterations to C types
146
+ double tol = NUM2DBL(rb_tol);
147
+ long long max_iter = NUM2LL(rb_max_iter);
134
148
 
135
- // Convert tolerance and max iterations to C types
136
- double tol = NUM2DBL(rb_tol);
137
- long long max_iter = NUM2LL(rb_max_iter);
138
-
139
- double result;
140
-
141
- // Try Brent's method with a standard bracketing interval
142
- double low = -0.9999, high = 10.0;
149
+ double result;
150
+ double low = -0.3, high = 10.0;
143
151
 
144
- result = brent_method(cashflows, count, tol, max_iter, low, high);
145
- if (!isnan(result)) {
146
- return rb_float_new(result);
152
+ // Use provided brackets if they exist
153
+ if (!NIL_P(rb_brackets)) {
154
+ Check_Type(rb_brackets, T_ARRAY);
155
+ if (RARRAY_LEN(rb_brackets) != 2) {
156
+ rb_raise(rb_eArgError, "Bracket array must contain exactly 2 elements");
147
157
  }
148
-
149
- // If the standard interval fails, try to find a better bracketing interval
150
- low = -0.9999999; high = 10.0;
151
- if (find_bracketing_interval(cashflows, count, &low, &high)) {
152
- result = brent_method(cashflows, count, tol, max_iter, low, high);
153
- return rb_float_new(result);
154
- }
155
-
156
- // If no interval is found, return NAN
157
- return rb_float_new(NAN);
158
+ low = NUM2DBL(rb_ary_entry(rb_brackets, 0));
159
+ high = NUM2DBL(rb_ary_entry(rb_brackets, 1));
160
+ }
161
+
162
+ result = brent_method(cashflows, filtered_count, tol, max_iter, low, high);
163
+ if (!isnan(result)) {
164
+ return rb_float_new(result);
165
+ }
166
+
167
+ // If the standard interval fails, try to find a better bracketing interval
168
+ low = -0.9999999;
169
+ high = 100.0;
170
+ if (find_bracketing_interval(cashflows, filtered_count, &low, &high)) {
171
+ result = brent_method(cashflows, filtered_count, tol, max_iter, low, high);
172
+ return rb_float_new(result);
173
+ }
174
+
175
+ // If no interval is found, return NAN
176
+ return rb_float_new(NAN);
158
177
  }
159
-
@@ -1,9 +1,9 @@
1
- #ifndef BRENT_H
1
+ #ifndef BRENT_H
2
2
  #define BRENT_H
3
3
 
4
4
  #include <ruby.h>
5
5
 
6
- VALUE calculate_xirr_with_brent(VALUE self, VALUE rb_cashflows, VALUE rb_tol, VALUE rb_max_iter);
6
+ VALUE calculate_xirr_with_brent(VALUE self, VALUE rb_cashflows, VALUE rb_tol,
7
+ VALUE rb_max_iter, VALUE rb_brackets);
7
8
 
8
9
  #endif
9
-
@@ -11,22 +11,21 @@
11
11
  *
12
12
  * @return The calculated NPV.
13
13
  */
14
- double npv(double rate, CashFlow *cashflows, long long count, int64_t min_date) {
15
- double npv_value = 0.0;
14
+ double npv(double rate, CashFlow *cashflows, long long count,
15
+ int64_t min_date) {
16
+ double npv_value = 0.0;
16
17
 
17
- for (long long i = 0; i < count; i++) {
18
- // Calculate the number of days from the minimum date to the cash flow date
19
- double days = (double)(cashflows[i].date - min_date) / (60 * 60 * 24);
18
+ for (long long i = 0; i < count; i++) {
19
+ // Calculate the number of days from the minimum date to the cash flow date
20
+ double days = (double)(cashflows[i].date - min_date) / (60 * 60 * 24);
20
21
 
21
- // Calculate the discount factor and add the discounted amount to the NPV
22
- npv_value += cashflows[i].amount / pow(1 + rate, days / 365.0);
23
- }
22
+ // Calculate the discount factor and add the discounted amount to the NPV
23
+ npv_value += cashflows[i].amount / pow(1 + rate, days / 365.0);
24
+ }
24
25
 
25
- return npv_value;
26
+ return npv_value;
26
27
  }
27
28
 
28
-
29
-
30
29
  /**
31
30
  * Find a bracketing interval for the root using the NPV function.
32
31
  *
@@ -37,48 +36,48 @@ double npv(double rate, CashFlow *cashflows, long long count, int64_t min_date)
37
36
  *
38
37
  * @return 1 if a bracketing interval is found, 0 otherwise.
39
38
  */
40
- int find_bracketing_interval(CashFlow *cashflows, long long count, double *low, double *high) {
41
- double min_rate = -0.99999999, max_rate = 10.0;
42
- double step = 0.0001;
43
- int64_t min_date = cashflows[0].date;
39
+ int find_bracketing_interval(CashFlow *cashflows, long long count, double *low,
40
+ double *high) {
41
+ double min_rate = -0.99999999, max_rate = 10.0;
42
+ double step = 0.0001;
43
+ int64_t min_date = cashflows[0].date;
44
44
 
45
- // Find the earliest date in the cashflows array
46
- for (long long i = 1; i < count; i++) {
47
- if (cashflows[i].date < min_date) {
48
- min_date = cashflows[i].date;
49
- }
45
+ // Find the earliest date in the cashflows array
46
+ for (long long i = 1; i < count; i++) {
47
+ if (cashflows[i].date < min_date) {
48
+ min_date = cashflows[i].date;
50
49
  }
50
+ }
51
51
 
52
- // Calculate NPV at the minimum rate
53
- double npv_min_rate = npv(min_rate, cashflows, count, min_date);
52
+ // Calculate NPV at the minimum rate
53
+ double npv_min_rate = npv(min_rate, cashflows, count, min_date);
54
54
 
55
- // Search for a bracketing interval within the initial range
56
- for (double rate = min_rate + step; rate <= max_rate; rate += step) {
57
- double npv_rate = npv(rate, cashflows, count, min_date);
55
+ // Search for a bracketing interval within the initial range
56
+ for (double rate = min_rate + step; rate <= max_rate; rate += step) {
57
+ double npv_rate = npv(rate, cashflows, count, min_date);
58
58
 
59
- // Check if the function values at consecutive rates have opposite signs
60
- if (npv_min_rate * npv_rate <= 0) {
61
- *low = rate - step;
62
- *high = rate;
63
- return 1;
64
- }
65
- npv_min_rate = npv_rate;
59
+ // Check if the function values at consecutive rates have opposite signs
60
+ if (npv_min_rate * npv_rate <= 0) {
61
+ *low = rate - step;
62
+ *high = rate;
63
+ return 1;
66
64
  }
65
+ npv_min_rate = npv_rate;
66
+ }
67
67
 
68
- // Extend the search range if no interval is found within the initial range
69
- for (double rate = max_rate; rate <= 1e300; rate *= 1.5) {
70
- double npv_rate = npv(rate, cashflows, count, min_date);
68
+ // Extend the search range if no interval is found within the initial range
69
+ for (double rate = max_rate; rate <= 1e300; rate *= 1.5) {
70
+ double npv_rate = npv(rate, cashflows, count, min_date);
71
71
 
72
- // Check if the function values at consecutive rates have opposite signs
73
- if (npv_min_rate * npv_rate < 0) {
74
- *low = rate / 1.5;
75
- *high = rate;
76
- return 1;
77
- }
78
- npv_min_rate = npv_rate;
72
+ // Check if the function values at consecutive rates have opposite signs
73
+ if (npv_min_rate * npv_rate < 0) {
74
+ *low = rate / 1.5;
75
+ *high = rate;
76
+ return 1;
79
77
  }
78
+ npv_min_rate = npv_rate;
79
+ }
80
80
 
81
- // If no bracketing interval is found, return 0
82
- return 0;
81
+ // If no bracketing interval is found, return 0
82
+ return 0;
83
83
  }
84
-
@@ -4,12 +4,13 @@
4
4
  #include <stdint.h>
5
5
 
6
6
  typedef struct {
7
- double amount;
8
- int64_t date;
7
+ double amount;
8
+ int64_t date;
9
9
  } CashFlow;
10
10
 
11
11
  double npv(double rate, CashFlow *cashflows, long long count, int64_t min_date);
12
12
 
13
- int find_bracketing_interval(CashFlow *cashflows, long long count, double *low, double *high);
13
+ int find_bracketing_interval(CashFlow *cashflows, long long count, double *low,
14
+ double *high);
14
15
 
15
16
  #endif
data/ext/fast_xirr/xirr.c CHANGED
@@ -1,12 +1,14 @@
1
- #include <ruby.h>
2
- #include "brent.h"
3
1
  #include "bisection.h"
2
+ #include "brent.h"
3
+ #include <ruby.h>
4
4
 
5
5
  /**
6
6
  * Initialize the FastXirr module and define its methods.
7
7
  */
8
8
  void Init_fast_xirr(void) {
9
- VALUE XirrModule = rb_define_module("FastXirr");
10
- rb_define_singleton_method(XirrModule, "_calculate_with_bisection", calculate_xirr_with_bisection, 3);
11
- rb_define_singleton_method(XirrModule, "_calculate_with_brent", calculate_xirr_with_brent, 3);
9
+ VALUE XirrModule = rb_define_module("FastXirr");
10
+ rb_define_singleton_method(XirrModule, "_calculate_with_bisection",
11
+ calculate_xirr_with_bisection, 3);
12
+ rb_define_singleton_method(XirrModule, "_calculate_with_brent",
13
+ calculate_xirr_with_brent, 4);
12
14
  }
@@ -1,4 +1,4 @@
1
1
  module FastXirr
2
- VERSION = "1.0.2"
2
+ VERSION = "1.1.1"
3
3
  end
4
4
 
data/lib/fast_xirr.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  require 'fast_xirr/fast_xirr'
2
2
 
3
3
  module FastXirr
4
- def self.calculate(cashflows:, tol: 1e-7, max_iter: 1e10)
4
+ def self.calculate(cashflows:, tol: 1e-7, max_iter: 1e10, initial_bracket: [-0.3,10.0])
5
5
  cashflows_with_timestamps = cashflows.map do |amount, date|
6
6
  [amount, Time.utc(date.year, date.month, date.day).to_i]
7
7
  end.sort_by { |_amount, timestamp| timestamp }
8
8
 
9
- result = _calculate_with_brent(cashflows_with_timestamps, tol, max_iter)
10
-
9
+ result = _calculate_with_brent(cashflows_with_timestamps, tol, max_iter, initial_bracket)
10
+
11
11
  if result.nan?
12
12
  result = _calculate_with_bisection(cashflows_with_timestamps, tol, max_iter)
13
13
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_xirr
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matias Martini
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-21 00:00:00.000000000 Z
11
+ date: 2025-02-19 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A gem to calculate the XIRR using a C extension for performance.
14
14
  email: