fast_xirr 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 164d5fa4cee3f5843f73e98ebb07c1b07b35c3bd76b2cb203729e4de0bbf117b
4
+ data.tar.gz: 8e39e9dd7b740f4bc5a50161f13b4022eccac626faf02e4d05148393ff1d67c5
5
+ SHA512:
6
+ metadata.gz: 1afc7e0a0c46fffb68036501eda4a431d935924b4e0c4cc32befaeb67ec5520acaf3221c87ff90ef2523936b13df061e914b68469d537ef1524b2ab63b0d30fc
7
+ data.tar.gz: 22f0d3670038bdabeacb097d17f52227dd6a79f8d5cf01ada768fde793ee3605f3c99bcd93fad3eec4688c4fd52dda1b46e4038d9509be5f24b6ab38e7d9237e
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # FastXirr
2
+
3
+ FastXirr is a high-performance Ruby gem for calculating the Extended Internal Rate of Return (XIRR). It leverages C under the hood for rapid calculations, making it suitable for performance-critical applications.
4
+
5
+ ## Features
6
+
7
+ - Fast XIRR calculations using efficient algorithms
8
+ - Implemented in C for high performance
9
+ - Easy to use Ruby interface
10
+ - Includes both Brent's method and Bisection method for robust root-finding
11
+
12
+ ## Installation
13
+
14
+ ### From RubyGems
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'fast_xirr', git: 'https://github.com/fintual-oss/fast-xirr.git'
20
+ ```
21
+
22
+ And then execute:
23
+ ```bash
24
+ bundle install
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Calculate XIRR
30
+
31
+ To calculate the XIRR for a series of cash flows, use the calculate method:
32
+
33
+ ```ruby
34
+ require 'fast_xirr'
35
+ require 'date'
36
+
37
+ cashflows = [
38
+ [1000, Date.new(1985, 1, 1)],
39
+ [-600, Date.new(1990, 1, 1)],
40
+ [-6000, Date.new(1995, 1, 1)]
41
+ ]
42
+
43
+ result = FastXirr.calculate(cashflows: cashflows)
44
+ puts "XIRR: #{result}"
45
+ # => XIRR: 0.22568401743016633
46
+ ```
47
+
48
+ If it is not possible to find a solution, the method will return `nan`.
49
+
50
+ ```ruby
51
+ require 'fast_xirr'
52
+ require 'date'
53
+
54
+ result = FastXirr.calculate(cashflows: [[1000, Date.new(1985, 1, 1)]])
55
+
56
+ puts "XIRR: #{result}"
57
+ # => XIRR: NaN
58
+
59
+ result.nan?
60
+ # => true
61
+ ```
62
+
63
+ Tolerance can be set to a custom value (default is 1e-10), as well as the maximum number of iterations (default is 100000000000000).
64
+
65
+
66
+ ```ruby
67
+ require 'fast_xirr'
68
+ require 'date'
69
+
70
+ cashflows = [
71
+ [1000, Date.new(1985, 1, 1)],
72
+ [-600, Date.new(1990, 1, 1)],
73
+ [-6000, Date.new(1995, 1, 1)]
74
+ ]
75
+
76
+ result = FastXirr.calculate(cashflows: cashflows, tol: 1e-2, max_iter: 100)
77
+ puts "XIRR: #{result}"
78
+ # => XIRR: 0.22305878076614422
79
+
80
+ result = FastXirr.calculate(cashflows: cashflows, tol: 1e-8, max_iter: 2)
81
+ puts "XIRR: #{result}"
82
+ # => XIRR: NaN
83
+ ```
84
+
85
+ ## Build and test
86
+
87
+ ### Building the Gem
88
+
89
+ To build the gem from the source code, follow these steps:
90
+
91
+ 1. **Clone the Repository**:
92
+
93
+ ```bash
94
+ git clone https://github.com/fintual-oss/fast-xirr.git
95
+ cd fast_xirr
96
+ ```
97
+
98
+ 2. **Build the Gem**:
99
+
100
+ ```bash
101
+ gem build fast_xirr.gemspec
102
+ ```
103
+
104
+ This will create a `.gem` file in the directory, such as `fast_xirr-0.1.0.gem`.
105
+
106
+ 3. **Install the Gem Locally**:
107
+
108
+ ```bash
109
+ gem install ./fast_xirr-0.1.0.gem
110
+ ```
111
+
112
+ ### Testing the Gem
113
+
114
+ To run the tests, follow these steps:
115
+
116
+ 1. **Install Development Dependencies**:
117
+
118
+ ```bash
119
+ bundle install
120
+ ```
121
+
122
+ 2. **Run the Tests**:
123
+
124
+ ```bash
125
+ rake test
126
+ ```
127
+
128
+ This will run the test suite using RSpec.
129
+
130
+
131
+ ## Contributing
132
+
133
+ Bug reports and pull requests are welcome on GitHub at https://github.com/fintual-oss/fast-xirr. This project is intended to be a safe, welcoming space for collaboration.
134
+
@@ -0,0 +1,100 @@
1
+ #include <ruby.h>
2
+ #include <time.h>
3
+ #include <math.h>
4
+ #include "bisection.h"
5
+ #include "common.h"
6
+
7
+ /**
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.
10
+ *
11
+ * @param cashflows Array of CashFlow structures containing amount and date.
12
+ * @param count Number of elements in the cashflows array.
13
+ * @param tol Tolerance for convergence.
14
+ * @param max_iter Maximum number of iterations.
15
+ * @param low Lower bound of the initial interval.
16
+ * @param high Upper bound of the initial interval.
17
+ *
18
+ * @return The estimated root (XIRR) or NAN if it fails to converge.
19
+ */
20
+ double bisection_method(CashFlow *cashflows, long count, double tol, long max_iter, double low, double high) {
21
+ double mid, f_low, f_mid, f_high;
22
+
23
+ // Calculate the NPV at the boundaries of the interval
24
+ f_low = npv(low, cashflows, count, cashflows[0].date);
25
+ f_high = npv(high, cashflows, count, cashflows[0].date);
26
+
27
+ // Ensure the root is bracketed
28
+ if (f_low * f_high > 0) {
29
+ return NAN; // Root is not bracketed
30
+ }
31
+
32
+ // Iteratively apply the bisection method
33
+ for (long iter = 0; iter < max_iter; iter++) {
34
+ mid = (low + high) / 2.0;
35
+ f_mid = npv(mid, cashflows, count, cashflows[0].date);
36
+
37
+ // Check for convergence
38
+ if (fabs(f_mid) < tol || fabs(high - low) < tol) {
39
+ return mid;
40
+ }
41
+
42
+ // Narrow the interval
43
+ if (f_low * f_mid < 0) {
44
+ high = mid;
45
+ f_high = f_mid;
46
+ } else {
47
+ low = mid;
48
+ f_low = f_mid;
49
+ }
50
+ }
51
+
52
+ // If we reach here, it means we failed to converge
53
+ return NAN;
54
+ }
55
+
56
+ /**
57
+ * Ruby wrapper to calculate XIRR using the Bisection method.
58
+ *
59
+ * @param self Ruby object (self).
60
+ * @param rb_cashflows Ruby array of cash flows.
61
+ * @param rb_tol Ruby float for tolerance.
62
+ * @param rb_max_iter Ruby integer for maximum iterations.
63
+ *
64
+ * @return Ruby float with the calculated XIRR.
65
+ */
66
+ VALUE calculate_xirr_with_bisection(VALUE self, VALUE rb_cashflows, VALUE rb_tol, VALUE rb_max_iter) {
67
+ // Get the number of cash flows
68
+ long count = RARRAY_LEN(rb_cashflows);
69
+ CashFlow cashflows[count];
70
+
71
+ // Convert Ruby cash flows array to C array
72
+ for (long i = 0; i < count; i++) {
73
+ VALUE rb_cashflow = rb_ary_entry(rb_cashflows, i);
74
+ cashflows[i].amount = NUM2DBL(rb_ary_entry(rb_cashflow, 0));
75
+ cashflows[i].date = (time_t)NUM2LONG(rb_ary_entry(rb_cashflow, 1));
76
+ }
77
+
78
+ // Convert tolerance and max iterations to C types
79
+ double tol = NUM2DBL(rb_tol);
80
+ long max_iter = NUM2LONG(rb_max_iter);
81
+
82
+ // Initial standard bracketing interval
83
+ double low = -0.999999, high = 100.0;
84
+
85
+ // Try Bisection method with the standard interval
86
+ double result = bisection_method(cashflows, count, tol, max_iter, low, high);
87
+ if (!isnan(result)) {
88
+ return rb_float_new(result);
89
+ }
90
+
91
+ // If the standard interval fails, try to find a better bracketing interval
92
+ low = -0.9999999; high = 1000.0;
93
+ if (find_bracketing_interval(cashflows, count, &low, &high)) {
94
+ result = bisection_method(cashflows, count, tol, max_iter, low, high);
95
+ return rb_float_new(result);
96
+ }
97
+
98
+ // If no interval is found, return NAN
99
+ return rb_float_new(NAN);
100
+ }
@@ -0,0 +1,8 @@
1
+ #ifndef BISECTION_H
2
+ #define BISECTION_H
3
+
4
+ #include <ruby.h>
5
+
6
+ VALUE calculate_xirr_with_bisection(VALUE self, VALUE rb_cashflows, VALUE rb_tol, VALUE rb_max_iter);
7
+
8
+ #endif
@@ -0,0 +1,152 @@
1
+ #include <ruby.h>
2
+ #include <time.h>
3
+ #include <math.h>
4
+ #include "brent.h"
5
+ #include "common.h"
6
+
7
+ /**
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.
10
+ *
11
+ * @param cashflows Array of CashFlow structures containing amount and date.
12
+ * @param count Number of elements in the cashflows array.
13
+ * @param tol Tolerance for convergence.
14
+ * @param max_iter Maximum number of iterations.
15
+ * @param low Lower bound of the initial interval.
16
+ * @param high Upper bound of the initial interval.
17
+ *
18
+ * @return The estimated root (XIRR) or NAN if it fails to converge.
19
+ */
20
+ double brent_method(CashFlow *cashflows, long count, double tol, long max_iter, double low, double high) {
21
+ // Calculate the NPV at the boundaries of the interval
22
+ double fa = npv(low, cashflows, count, cashflows[0].date);
23
+ double fb = npv(high, cashflows, count, cashflows[0].date);
24
+
25
+ // Ensure the root is bracketed
26
+ if (fa * fb > 0) {
27
+ return NAN; // Root is not bracketed
28
+ }
29
+
30
+ double c = low, fc = fa, s, d = 0.0, e = 0.0;
31
+
32
+ // Iteratively apply Brent's method
33
+ for (long iter = 0; iter < max_iter; iter++) {
34
+ if (fb * fc > 0) {
35
+ // Adjust c to ensure that f(b) and f(c) have opposite signs
36
+ c = low;
37
+ fc = fa;
38
+ d = e = high - low;
39
+ }
40
+ if (fabs(fc) < fabs(fb)) {
41
+ // Swap low and high to make f(b) smaller in magnitude
42
+ low = high;
43
+ high = c;
44
+ c = low;
45
+ fa = fb;
46
+ fb = fc;
47
+ fc = fa;
48
+ }
49
+
50
+ // Tolerance calculation for convergence check
51
+ double tol1 = 2 * tol * fabs(high) + 0.5 * tol;
52
+ double m = 0.5 * (c - high);
53
+
54
+ // Check for convergence
55
+ if (fabs(m) <= tol1 || fb == 0.0) {
56
+ return high;
57
+ }
58
+
59
+ // Use inverse quadratic interpolation if conditions are met
60
+ if (fabs(e) >= tol1 && fabs(fa) > fabs(fb)) {
61
+ double p, q, r;
62
+ s = fb / fa;
63
+ if (low == c) {
64
+ // Use the secant method
65
+ p = 2 * m * s;
66
+ q = 1 - s;
67
+ } else {
68
+ // Use inverse quadratic interpolation
69
+ q = fa / fc;
70
+ r = fb / fc;
71
+ p = s * (2 * m * q * (q - r) - (high - low) * (r - 1));
72
+ q = (q - 1) * (r - 1) * (s - 1);
73
+ }
74
+ if (p > 0) q = -q;
75
+ p = fabs(p);
76
+ if (2 * p < fmin(3 * m * q - fabs(tol1 * q), fabs(e * q))) {
77
+ // Accept interpolation
78
+ e = d;
79
+ d = p / q;
80
+ } else {
81
+ // Use bisection
82
+ d = m;
83
+ e = m;
84
+ }
85
+ } else {
86
+ // Use bisection
87
+ d = m;
88
+ e = m;
89
+ }
90
+
91
+ // Update bounds
92
+ low = high;
93
+ fa = fb;
94
+
95
+ // Update the new high value
96
+ if (fabs(d) > tol1) {
97
+ high += d;
98
+ } else {
99
+ high += copysign(tol1, m);
100
+ }
101
+ fb = npv(high, cashflows, count, cashflows[0].date);
102
+ }
103
+
104
+ return NAN; // Failed to converge
105
+ }
106
+
107
+ /**
108
+ * Ruby wrapping to calculate XIRR using Brent's method.
109
+ *
110
+ * @param self Ruby object (self).
111
+ * @param rb_cashflows Ruby array of cash flows.
112
+ * @param rb_tol Ruby float for tolerance.
113
+ * @param rb_max_iter Ruby integer for maximum iterations.
114
+ *
115
+ * @return Ruby float with the calculated XIRR.
116
+ */
117
+ VALUE calculate_xirr_with_brent(VALUE self, VALUE rb_cashflows, VALUE rb_tol, VALUE rb_max_iter) {
118
+ // Get the number of cash flows
119
+ long count = RARRAY_LEN(rb_cashflows);
120
+ CashFlow cashflows[count];
121
+
122
+ // Convert Ruby cash flows array to C array
123
+ for (long i = 0; i < count; i++) {
124
+ VALUE rb_cashflow = rb_ary_entry(rb_cashflows, i);
125
+ cashflows[i].amount = NUM2DBL(rb_ary_entry(rb_cashflow, 0));
126
+ cashflows[i].date = (time_t)NUM2LONG(rb_ary_entry(rb_cashflow, 1));
127
+ }
128
+
129
+ // Convert tolerance and max iterations to C types
130
+ double tol = NUM2DBL(rb_tol);
131
+ long max_iter = NUM2LONG(rb_max_iter);
132
+
133
+ double result;
134
+
135
+ // Try Brent's method with a standard bracketing interval
136
+ double low = -0.9999, high = 10.0;
137
+ result = brent_method(cashflows, count, tol, max_iter, low, high);
138
+ if (!isnan(result)) {
139
+ return rb_float_new(result);
140
+ }
141
+
142
+ // If the standard interval fails, try to find a better bracketing interval
143
+ low = -0.9999999; high = 10.0;
144
+ if (find_bracketing_interval(cashflows, count, &low, &high)) {
145
+ result = brent_method(cashflows, count, tol, max_iter, low, high);
146
+ return rb_float_new(result);
147
+ }
148
+
149
+ // If no interval is found, return NAN
150
+ return rb_float_new(NAN);
151
+ }
152
+
@@ -0,0 +1,9 @@
1
+ #ifndef BRENT_H
2
+ #define BRENT_H
3
+
4
+ #include <ruby.h>
5
+
6
+ VALUE calculate_xirr_with_brent(VALUE self, VALUE rb_cashflows, VALUE rb_tol, VALUE rb_max_iter);
7
+
8
+ #endif
9
+
@@ -0,0 +1,85 @@
1
+ #include "common.h"
2
+ #include <math.h>
3
+
4
+ /**
5
+ * Calculate the Net Present Value (NPV) for a given rate.
6
+ *
7
+ * @param rate The discount rate.
8
+ * @param cashflows Array of CashFlow structures containing amount and date.
9
+ * @param count Number of elements in the cashflows array.
10
+ * @param min_date The earliest date in the cashflows array.
11
+ *
12
+ * @return The calculated NPV.
13
+ */
14
+ double npv(double rate, CashFlow *cashflows, long count, time_t min_date) {
15
+ double npv_value = 0.0;
16
+
17
+ for (long i = 0; i < count; i++) {
18
+ // Calculate the number of days from the minimum date to the cash flow date
19
+ double days = difftime(cashflows[i].date, min_date) / (60 * 60 * 24);
20
+
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
+ }
24
+
25
+ return npv_value;
26
+ }
27
+
28
+
29
+
30
+ /**
31
+ * Find a bracketing interval for the root using the NPV function.
32
+ *
33
+ * @param cashflows Array of CashFlow structures containing amount and date.
34
+ * @param count Number of elements in the cashflows array.
35
+ * @param low Pointer to store the lower bound of the bracketing interval.
36
+ * @param high Pointer to store the upper bound of the bracketing interval.
37
+ *
38
+ * @return 1 if a bracketing interval is found, 0 otherwise.
39
+ */
40
+ int find_bracketing_interval(CashFlow *cashflows, long count, double *low, double *high) {
41
+ double min_rate = -0.99999999, max_rate = 10.0;
42
+ double step = 0.0001;
43
+ time_t min_date = cashflows[0].date;
44
+
45
+ // Find the earliest date in the cashflows array
46
+ for (long i = 1; i < count; i++) {
47
+ if (cashflows[i].date < min_date) {
48
+ min_date = cashflows[i].date;
49
+ }
50
+ }
51
+
52
+ // Calculate NPV at the minimum rate
53
+ double npv_min_rate = npv(min_rate, cashflows, count, min_date);
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);
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;
66
+ }
67
+
68
+ // Extend the search range if no interval is found within the initial range
69
+ step = 10.0;
70
+ for (double rate = max_rate + step; rate <= 10000.0; rate += step) {
71
+ double npv_rate = npv(rate, cashflows, count, min_date);
72
+
73
+ // Check if the function values at consecutive rates have opposite signs
74
+ if (npv_min_rate * npv_rate < 0) {
75
+ *low = rate - step;
76
+ *high = rate;
77
+ return 1;
78
+ }
79
+ npv_min_rate = npv_rate;
80
+ }
81
+
82
+ // If no bracketing interval is found, return 0
83
+ return 0;
84
+ }
85
+
@@ -0,0 +1,15 @@
1
+ #ifndef COMMON_H
2
+ #define COMMON_H
3
+
4
+ #include <time.h>
5
+
6
+ typedef struct {
7
+ double amount;
8
+ time_t date;
9
+ } CashFlow;
10
+
11
+ double npv(double rate, CashFlow *cashflows, long count, time_t min_date);
12
+
13
+ int find_bracketing_interval(CashFlow *cashflows, long count, double *low, double *high);
14
+
15
+ #endif
@@ -0,0 +1,6 @@
1
+ require 'mkmf'
2
+
3
+ $srcs = ['xirr.c', 'brent.c', 'bisection.c', 'common.c']
4
+
5
+ create_makefile('fast_xirr/fast_xirr')
6
+
@@ -0,0 +1,12 @@
1
+ #include <ruby.h>
2
+ #include "brent.h"
3
+ #include "bisection.h"
4
+
5
+ /**
6
+ * Initialize the FastXirr module and define its methods.
7
+ */
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);
12
+ }
@@ -0,0 +1,4 @@
1
+ module FastXirr
2
+ VERSION = "0.1.0"
3
+ end
4
+
data/lib/fast_xirr.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'fast_xirr/fast_xirr'
2
+
3
+ module FastXirr
4
+ def self.calculate(cashflows:, tol: 1e-7, max_iter: 1e10)
5
+ cashflows_with_timestamps = cashflows.map do |amount, date|
6
+ [amount, date.to_time.to_i]
7
+ end
8
+ result = _calculate_with_brent(cashflows_with_timestamps, tol, max_iter)
9
+
10
+ if result.nan?
11
+ result = _calculate_with_bisection(cashflows_with_timestamps, tol, max_iter)
12
+ puts "Brent failed, trying bisection" unless result.nan?
13
+ end
14
+
15
+ return result
16
+ end
17
+ end
18
+
19
+
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fast_xirr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matias Martini
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-06-28 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A gem to calculate the XIRR using a C extension for performance.
14
+ email:
15
+ - martini@fintual.com
16
+ executables: []
17
+ extensions:
18
+ - ext/fast_xirr/extconf.rb
19
+ extra_rdoc_files: []
20
+ files:
21
+ - README.md
22
+ - ext/fast_xirr/bisection.c
23
+ - ext/fast_xirr/bisection.h
24
+ - ext/fast_xirr/brent.c
25
+ - ext/fast_xirr/brent.h
26
+ - ext/fast_xirr/common.c
27
+ - ext/fast_xirr/common.h
28
+ - ext/fast_xirr/extconf.rb
29
+ - ext/fast_xirr/xirr.c
30
+ - lib/fast_xirr.rb
31
+ - lib/fast_xirr/version.rb
32
+ homepage: https://github.com/fintual-oss/fast-xirr
33
+ licenses:
34
+ - MIT
35
+ metadata:
36
+ homepage_uri: https://github.com/fintual-oss/fast-xirr
37
+ source_code_uri: https://github.com/fintual-oss/fast-xirr
38
+ changelog_uri: https://github.com/fintual-oss/fast-xirr/CHANGELOG.md
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 2.3.0
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 3.4.19
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: XIRR calculation in Ruby with C extension
58
+ test_files: []