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 +4 -4
- data/README.md +9 -1
- data/ext/fast_xirr/bisection.c +67 -63
- data/ext/fast_xirr/bisection.h +2 -1
- data/ext/fast_xirr/brent.c +138 -120
- data/ext/fast_xirr/brent.h +3 -3
- data/ext/fast_xirr/common.c +44 -45
- data/ext/fast_xirr/common.h +4 -3
- data/ext/fast_xirr/xirr.c +7 -5
- data/lib/fast_xirr/version.rb +1 -1
- data/lib/fast_xirr.rb +3 -3
- 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: f240b5f7c4ffe72a091fea47502e7bf16a206502eb7b498131f68489698353a4
|
4
|
+
data.tar.gz: 541df15f67a6e62dc1d8c2a07db6673bd8b806b98a22dc7832a743824add6078
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/ext/fast_xirr/bisection.c
CHANGED
@@ -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
|
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,
|
21
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
25
|
+
// If cashflows are empty, return 0
|
26
|
+
if (count == 0) {
|
27
|
+
return 0.0;
|
28
|
+
}
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
// Check for convergence
|
45
|
+
if (fabs(f_mid) < tol || fabs(high - low) < tol) {
|
46
|
+
return mid;
|
47
|
+
}
|
46
48
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
58
|
-
|
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,
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
88
|
-
|
90
|
+
// Initial standard bracketing interval
|
91
|
+
double low = -0.999999, high = 100.0;
|
89
92
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
104
|
-
|
107
|
+
// If no interval is found, return NAN
|
108
|
+
return rb_float_new(NAN);
|
105
109
|
}
|
data/ext/fast_xirr/bisection.h
CHANGED
data/ext/fast_xirr/brent.c
CHANGED
@@ -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
|
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,
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
//
|
27
|
-
double
|
28
|
-
double
|
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
|
-
//
|
31
|
-
if (
|
32
|
-
|
61
|
+
// Check for convergence
|
62
|
+
if (fabs(m) <= tol1 || fb == 0.0) {
|
63
|
+
return high;
|
33
64
|
}
|
34
65
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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,
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
136
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
data/ext/fast_xirr/brent.h
CHANGED
@@ -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,
|
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
|
-
|
data/ext/fast_xirr/common.c
CHANGED
@@ -11,22 +11,21 @@
|
|
11
11
|
*
|
12
12
|
* @return The calculated NPV.
|
13
13
|
*/
|
14
|
-
double npv(double rate, CashFlow *cashflows, long long count,
|
15
|
-
|
14
|
+
double npv(double rate, CashFlow *cashflows, long long count,
|
15
|
+
int64_t min_date) {
|
16
|
+
double npv_value = 0.0;
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
22
|
-
|
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
|
-
|
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,
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
53
|
-
|
52
|
+
// Calculate NPV at the minimum rate
|
53
|
+
double npv_min_rate = npv(min_rate, cashflows, count, min_date);
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
82
|
-
|
81
|
+
// If no bracketing interval is found, return 0
|
82
|
+
return 0;
|
83
83
|
}
|
84
|
-
|
data/ext/fast_xirr/common.h
CHANGED
@@ -4,12 +4,13 @@
|
|
4
4
|
#include <stdint.h>
|
5
5
|
|
6
6
|
typedef struct {
|
7
|
-
|
8
|
-
|
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,
|
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
|
-
|
10
|
-
|
11
|
-
|
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
|
}
|
data/lib/fast_xirr/version.rb
CHANGED
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.
|
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:
|
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:
|