xirr 0.1.0 → 0.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/lib/xirr.rb +2 -0
- data/lib/xirr/cashflow.rb +64 -21
- data/lib/xirr/config.rb +2 -0
- data/lib/xirr/main.rb +34 -27
- data/lib/xirr/transaction.rb +7 -17
- data/lib/xirr/version.rb +2 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac4ff922b29d910ccb51a3238c6602d0309bc685
|
4
|
+
data.tar.gz: 912024facef6c25c236f99f8e86fa0ee75efa496
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 68610190f330d76c57f1b638d92210002400a4011387816b1cb369c502055a7e2922545cfd2dfc997abcd0c560b0db4e48e4248342d0a09ee1eb6206b7eb8be3
|
7
|
+
data.tar.gz: b31417b22db2501bf5547610340c38031d8d83d372832335196d286ec51893968cb62e94f03b73cb61dca813e469bca4310e671fd5e51bf7967a7347cceece36
|
data/lib/xirr.rb
CHANGED
@@ -3,6 +3,8 @@ require 'active_support/configurable'
|
|
3
3
|
require 'xirr/config'
|
4
4
|
require 'xirr/main'
|
5
5
|
|
6
|
+
# @abstract adds a {Xirr::Cashflow} and {Xirr::Transaction} classes to calculate IRR of irregular transactions.
|
7
|
+
# Calculates Xirr
|
6
8
|
module Xirr
|
7
9
|
|
8
10
|
autoload :Transaction, 'xirr/transaction'
|
data/lib/xirr/cashflow.rb
CHANGED
@@ -1,14 +1,25 @@
|
|
1
1
|
module Xirr
|
2
|
+
|
3
|
+
# Expands [Array] to store a set of transactions which will be used to calculate the XIRR
|
4
|
+
# @note A Cashflow should consist of at least two transactions, one positive and one negative.
|
2
5
|
class Cashflow < Array
|
3
6
|
include Xirr::Main
|
4
7
|
|
8
|
+
# @api public
|
9
|
+
# @param args [Transaction]
|
10
|
+
# @example Creating a Cashflow
|
11
|
+
# cf = Cashflow.new
|
12
|
+
# cf << Transaction.new( 1000, date: '2013-01-01'.to_time(:utc))
|
13
|
+
# cf << Transaction.new(-1234, date: '2013-03-31'.to_time(:utc))
|
14
|
+
# Or
|
15
|
+
# cf = Cashflow.new Transaction.new( 1000, date: '2013-01-01'.to_time(:utc)), Transaction.new(-1234, date: '2013-03-31'.to_time(:utc))
|
5
16
|
def initialize(*args) # :nodoc:
|
6
17
|
args.each { |a| self << a }
|
7
18
|
self.flatten!
|
8
19
|
end
|
9
20
|
|
10
21
|
# Check if Cashflow is invalid and raises ArgumentError
|
11
|
-
#
|
22
|
+
# @return [Boolean]
|
12
23
|
def invalid?
|
13
24
|
if positives.empty? || negatives.empty?
|
14
25
|
raise ArgumentError, invalid_message
|
@@ -18,64 +29,96 @@ module Xirr
|
|
18
29
|
end
|
19
30
|
|
20
31
|
# Inverse of #invalid?
|
21
|
-
#
|
32
|
+
# @return [Boolean]
|
22
33
|
def valid?
|
23
34
|
!invalid?
|
24
35
|
end
|
25
36
|
|
37
|
+
# @return [Float]
|
38
|
+
# Sums all amounts in a cashflow
|
39
|
+
def sum # :nodoc:
|
40
|
+
self.map(&:amount).sum
|
41
|
+
end
|
26
42
|
|
27
|
-
#
|
28
|
-
#
|
29
|
-
def
|
30
|
-
|
43
|
+
# Last investment date
|
44
|
+
# @return [Time]
|
45
|
+
def max_date # :nodoc:
|
46
|
+
@max_date ||= self.map(&:date).max
|
31
47
|
end
|
32
48
|
|
33
|
-
|
34
|
-
|
49
|
+
# Calculates a simple IRR guess based on period of investment and multiples.
|
50
|
+
# @return [Float]
|
51
|
+
def irr_guess
|
52
|
+
((multiple ** (1 / years_of_investment)) - 1).round(3)
|
35
53
|
end
|
36
54
|
|
37
55
|
private
|
38
56
|
|
39
|
-
|
40
|
-
|
57
|
+
# @api private
|
58
|
+
# Sorts the {Cashflow} by date ascending
|
59
|
+
# and finds the signal of the first transaction.
|
60
|
+
# This implies the first transaction is a disembursement
|
61
|
+
# @return [Integer]
|
62
|
+
def first_transaction_direction
|
63
|
+
self.sort! { |x, y| x.date <=> y.date }
|
41
64
|
self.first.amount / self.first.amount.abs
|
42
65
|
end
|
43
66
|
|
44
|
-
# Based on the direction of the first investment
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
67
|
+
# Based on the direction of the first investment finds the multiple cash-on-cash
|
68
|
+
# @example
|
69
|
+
# [100,100,-300] and [-100,-100,300] returns 1.5
|
70
|
+
# @api private
|
71
|
+
# @return [Float]
|
72
|
+
def multiple # :nodoc:
|
73
|
+
result = positives.sum(&:amount) / -negatives.sum(&:amount)
|
74
|
+
first_transaction_direction > 0 ? result : 1 / result
|
51
75
|
end
|
52
76
|
|
77
|
+
# @api private
|
78
|
+
# Counts how many years from first to last transaction in the cashflow
|
79
|
+
# @return
|
53
80
|
def years_of_investment # :nodoc:
|
54
81
|
(max_date - min_date) / (365 * 24 * 60 * 60).to_f
|
55
82
|
end
|
56
83
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
84
|
+
# @api private
|
85
|
+
# First investment date
|
86
|
+
# @return [Time]
|
61
87
|
def min_date # :nodoc:
|
62
88
|
@min_date ||= self.map(&:date).min
|
63
89
|
end
|
64
90
|
|
91
|
+
# @api private
|
92
|
+
# @return [Array]
|
93
|
+
# @see #negatives
|
94
|
+
# @see #split_transactions
|
95
|
+
# Finds all transactions income from Cashflow
|
65
96
|
def positives # :nodoc:
|
66
97
|
split_transactions
|
67
98
|
@positives
|
68
99
|
end
|
69
100
|
|
101
|
+
# @api private
|
102
|
+
# @return [Array]
|
103
|
+
# @see #positives
|
104
|
+
# @see #split_transactions
|
105
|
+
# Finds all transactions investments from Cashflow
|
70
106
|
def negatives # :nodoc:
|
71
107
|
split_transactions
|
72
108
|
@negatives
|
73
109
|
end
|
74
110
|
|
111
|
+
# @api private
|
112
|
+
# @see #positives
|
113
|
+
# @see #negatives
|
114
|
+
# Uses partition to separate the investment transactions Negatives and the income transactions (Positives)
|
75
115
|
def split_transactions # :nodoc:
|
76
116
|
@negatives, @positives = self.partition { |x| x.amount >= 0 } # Inverted as negative amount is good
|
77
117
|
end
|
78
118
|
|
119
|
+
# @api private
|
120
|
+
# @return [String]
|
121
|
+
# Error message depending on the missing transaction
|
79
122
|
def invalid_message # :nodoc:
|
80
123
|
return 'No positive transaction' if positives.empty?
|
81
124
|
return 'No negatives transaction' if negatives.empty?
|
data/lib/xirr/config.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
module Xirr
|
2
2
|
include ActiveSupport::Configurable
|
3
3
|
|
4
|
+
# Default values
|
4
5
|
default_values = {
|
5
6
|
eps: '1.0e-12',
|
6
7
|
days_in_year: 365,
|
7
8
|
}
|
8
9
|
|
10
|
+
# Iterates trhough default values and sets in config
|
9
11
|
default_values.each do |key, value|
|
10
12
|
self.config.send("#{key.to_sym}=", value)
|
11
13
|
end
|
data/lib/xirr/main.rb
CHANGED
@@ -2,70 +2,77 @@ require 'active_support/concern'
|
|
2
2
|
|
3
3
|
module Xirr
|
4
4
|
|
5
|
+
# Methods that will be included in Cashflow to calculate XIRR
|
5
6
|
module Main
|
6
7
|
extend ActiveSupport::Concern
|
7
8
|
|
8
9
|
# Calculates yearly Internal Rate of Return
|
9
|
-
#
|
10
|
+
# @return [Float]
|
11
|
+
# @param guess [Float] an initial guess rate that will override the {Cashflow#irr_guess}
|
10
12
|
def xirr(guess = nil)
|
11
13
|
|
14
|
+
# Raises error if Cashflow is not valid
|
12
15
|
self.valid?
|
13
16
|
|
14
17
|
# Bisection method finding the rate to zero nfv
|
15
18
|
|
19
|
+
# Initial values
|
16
20
|
days_in_year = Xirr.config.days_in_year.to_f
|
17
|
-
|
18
21
|
left = -0.99/days_in_year
|
19
22
|
right = 9.99/days_in_year
|
20
23
|
epsilon = Xirr.config.eps.to_f
|
21
|
-
|
22
24
|
guess = self.irr_guess.to_f
|
23
25
|
|
26
|
+
# Loops until difference is within error margin
|
24
27
|
while ((right-left).abs > 2 * epsilon) do
|
25
28
|
|
26
29
|
midpoint = guess || (right + left)/2
|
27
30
|
guess = nil
|
31
|
+
nfv_positive?(left, midpoint) ? left = midpoint : right = midpoint
|
28
32
|
|
29
|
-
|
30
|
-
|
31
|
-
left = midpoint
|
33
|
+
end
|
32
34
|
|
33
|
-
|
35
|
+
return format_irr(left, right)
|
34
36
|
|
35
|
-
|
37
|
+
end
|
36
38
|
|
37
|
-
|
39
|
+
private
|
38
40
|
|
39
|
-
|
41
|
+
# @param left [Float]
|
42
|
+
# @param midpoint [Float]
|
43
|
+
# @return [Bolean]
|
44
|
+
# Returns true if result is to the right ot the range
|
45
|
+
def nfv_positive?(left, midpoint)
|
46
|
+
(nfv(left) * nfv(midpoint) > 0)
|
47
|
+
end
|
40
48
|
|
49
|
+
# @param left [Float]
|
50
|
+
# @param right [Float]
|
51
|
+
# @return [Float] IRR of the Cashflow
|
52
|
+
def format_irr(left, right)
|
53
|
+
days_in_year = Xirr.config.days_in_year.to_f
|
41
54
|
# Irr for daily cashflow (not in percentage format)
|
42
55
|
irr = (right+left) / 2
|
43
56
|
# Irr for daily cashflow multiplied by 365 to get yearly return
|
44
57
|
irr = irr * days_in_year
|
45
58
|
# Annualized yield (return) reflecting compounding effect of daily returns
|
46
59
|
irr = (1 + irr / days_in_year) ** days_in_year - 1
|
47
|
-
|
48
|
-
return irr
|
49
|
-
|
50
60
|
end
|
51
61
|
|
52
|
-
|
53
|
-
|
62
|
+
# Returns the Net future value of the flow given a Rate
|
63
|
+
# @param rate [Float]
|
64
|
+
# @return [Float]
|
54
65
|
def nfv(rate) # :nodoc:
|
55
|
-
|
56
|
-
|
57
|
-
nfv = 0
|
58
|
-
self.each do |t|
|
59
|
-
cf, date = t.amount, t.date
|
60
|
-
|
61
|
-
datestring = date.to_s
|
62
|
-
formatteddate = Date.parse(datestring).to_date
|
63
|
-
t_in_days = (today - formatteddate).numerator / (today - formatteddate).denominator
|
64
|
-
nfv = nfv + cf * ((1 + rate) ** t_in_days)
|
65
|
-
|
66
|
+
self.inject(0) do |nfv,t|
|
67
|
+
nfv = nfv + t.amount * ((1 + rate) ** t_in_days(t.date))
|
66
68
|
end
|
67
|
-
|
69
|
+
end
|
68
70
|
|
71
|
+
# Calculates days until last transaction
|
72
|
+
# @return [Rational]
|
73
|
+
# @param date [Time]
|
74
|
+
def t_in_days(date)
|
75
|
+
Date.parse(max_date.to_s) - Date.parse(date.to_s)
|
69
76
|
end
|
70
77
|
|
71
78
|
end
|
data/lib/xirr/transaction.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
module Xirr
|
2
2
|
|
3
|
+
# A unit of the Cashflow.
|
3
4
|
class Transaction
|
4
5
|
attr_reader :amount
|
5
6
|
attr_accessor :date
|
6
7
|
|
8
|
+
# @example
|
9
|
+
# Transaction.new -1000, date: Time.now
|
7
10
|
def initialize(amount, opts={})
|
8
11
|
@amount = amount
|
9
|
-
@original = amount
|
10
12
|
|
11
13
|
# Set optional attributes..
|
12
14
|
opts.each do |key, value|
|
@@ -14,30 +16,18 @@ module Xirr
|
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
19
|
+
# Sets the amount
|
20
|
+
# @param value [Float, Integer]
|
21
|
+
# @return [Float]
|
17
22
|
def amount=(value)
|
18
23
|
@amount = value.to_f || 0
|
19
24
|
end
|
20
25
|
|
26
|
+
# @return [String]
|
21
27
|
def inspect
|
22
28
|
"T(#{@amount},#{@date})"
|
23
29
|
end
|
24
30
|
|
25
|
-
def description
|
26
|
-
investment ? "#{self.investment.transaction_type_name}: #{round.description}" : @description
|
27
|
-
end
|
28
|
-
|
29
|
-
def round
|
30
|
-
@investment.nil? ? nil : investment.round
|
31
|
-
end
|
32
|
-
|
33
|
-
def company
|
34
|
-
@company || investment.company
|
35
|
-
end
|
36
|
-
|
37
|
-
def shareholder
|
38
|
-
@shareholder || investment.shareholder
|
39
|
-
end
|
40
|
-
|
41
31
|
end
|
42
32
|
|
43
33
|
end
|
data/lib/xirr/version.rb
CHANGED