diff --git a/lib/xirr.rb b/lib/xirr.rb index 1810041..0beaf05 100644 --- a/lib/xirr.rb +++ b/lib/xirr.rb @@ -3,6 +3,8 @@ require 'xirr/config' require 'xirr/main' +# @abstract adds a {Xirr::Cashflow} and {Xirr::Transaction} classes to calculate IRR of irregular transactions. +# Calculates Xirr module Xirr autoload :Transaction, 'xirr/transaction' diff --git a/lib/xirr/cashflow.rb b/lib/xirr/cashflow.rb index b0e8ffd..6e824f6 100644 --- a/lib/xirr/cashflow.rb +++ b/lib/xirr/cashflow.rb @@ -1,14 +1,25 @@ module Xirr + + # @abstract Expands [Array] to store a set of transactions which will be used to calculate the XIRR + # @note A Cashflow should consist of at least two transactions, one positive and one negative. class Cashflow < Array include Xirr::Main + # @api public + # @param args [Transaction] + # @example Creating a Cashflow + # cf = Cashflow.new + # cf << Transaction.new( 1000, date: '2013-01-01'.to_time(:utc)) + # cf << Transaction.new(-1234, date: '2013-03-31'.to_time(:utc)) + # Or + # cf = Cashflow.new Transaction.new( 1000, date: '2013-01-01'.to_time(:utc)), Transaction.new(-1234, date: '2013-03-31'.to_time(:utc)) def initialize(*args) # :nodoc: args.each { |a| self << a } self.flatten! end # Check if Cashflow is invalid and raises ArgumentError - # retuns [Boolean] + # @return [Boolean] def invalid? if positives.empty? || negatives.empty? raise ArgumentError, invalid_message @@ -18,64 +29,96 @@ def invalid? end # Inverse of #invalid? - # returns [Boolean] + # @return [Boolean] def valid? !invalid? end + # @return [Float] + # Sums all amounts in a cashflow + def sum # :nodoc: + self.map(&:amount).sum + end - # calculates a simple IRR guess based on period of investment and multiples - # returns [Float] - def irr_guess - ((multiple ** (1 / years_of_investment)) - 1).round(3) + # Last investment date + # @return [Time] + def max_date # :nodoc: + @max_date ||= self.map(&:date).max end - def sum # :nodoc: - self.map(&:amount).sum + # Calculates a simple IRR guess based on period of investment and multiples. + # @return [Float] + def irr_guess + ((multiple ** (1 / years_of_investment)) - 1).round(3) end private - def first_transaction_direction # :nodoc: + # @api private + # Sorts the {Cashflow} by date ascending + # and finds the signal of the first transaction. + # This implies the first transaction is a disembursement + # @return [Integer] + def first_transaction_direction self.sort! { |x,y| x.date <=> y.date } self.first.amount / self.first.amount.abs end - # Based on the direction of the first investment we create the multiple + # Based on the direction of the first investment finds the multiple cash-on-cash + # @example + # [100,100,-300] and [-100,-100,300] returns 1.5 + # @api private + # @return [Float] def multiple # :nodoc: - if first_transaction_direction > 0 - positives.sum(&:amount) / -negatives.sum(&:amount) - else - -negatives.sum(&:amount) / positives.sum(&:amount) - end + result = positives.sum(&:amount) / -negatives.sum(&:amount) + first_transaction_direction > 0 ? result : 1 / result end + # @api private + # Counts how many years from first to last transaction in the cashflow + # @return def years_of_investment # :nodoc: (max_date - min_date) / (365 * 24 * 60 * 60).to_f end - def max_date # :nodoc: - @max_date ||= self.map(&:date).max - end - + # @api private + # First investment date + # @return [Time] def min_date # :nodoc: @min_date ||= self.map(&:date).min end + # @api private + # @return [Array] + # @see #negatives + # @see #split_transactions + # Finds all transactions income from Cashflow def positives # :nodoc: split_transactions @positives end + # @api private + # @return [Array] + # @see #positives + # @see #split_transactions + # Finds all transactions investments from Cashflow def negatives # :nodoc: split_transactions @negatives end + # @api private + # @see #positives + # @see #negatives + # Uses partition to separate the investment transactions Negatives and the income transactions (Positives) def split_transactions # :nodoc: @negatives, @positives = self.partition { |x| x.amount >= 0 } # Inverted as negative amount is good end + # @api private + # @return [String] + # Error message depending on the missing transaction def invalid_message # :nodoc: return 'No positive transaction' if positives.empty? return 'No negatives transaction' if negatives.empty? diff --git a/lib/xirr/config.rb b/lib/xirr/config.rb index a3ff170..bfd3ac0 100644 --- a/lib/xirr/config.rb +++ b/lib/xirr/config.rb @@ -1,11 +1,13 @@ module Xirr include ActiveSupport::Configurable + # Default values default_values = { eps: '1.0e-12', days_in_year: 365, } + # Iterates trhough default values and sets in config default_values.each do |key, value| self.config.send("#{key.to_sym}=", value) end diff --git a/lib/xirr/main.rb b/lib/xirr/main.rb index 46bd68a..2389d19 100644 --- a/lib/xirr/main.rb +++ b/lib/xirr/main.rb @@ -2,70 +2,77 @@ module Xirr + # Methods that will be included in Cashflow to calculate XIRR module Main extend ActiveSupport::Concern # Calculates yearly Internal Rate of Return - # returns [Float] + # @return [Float] + # @param guess [Float] an initial guess rate that will override the {Cashflow#irr_guess} def xirr(guess = nil) + # Raises error if Cashflow is not valid self.valid? # Bisection method finding the rate to zero nfv + # Initial values days_in_year = Xirr.config.days_in_year.to_f - left = -0.99/days_in_year right = 9.99/days_in_year epsilon = Xirr.config.eps.to_f - guess = self.irr_guess.to_f + # Loops until difference is within error margin while ((right-left).abs > 2 * epsilon) do midpoint = guess || (right + left)/2 guess = nil + nfv_positive?(left, midpoint) ? left = midpoint : right = midpoint - if (nfv(left) * nfv(midpoint) > 0) - - left = midpoint + end - else + return format_irr(left, right) - right = midpoint + end - end + private - end + # @param left [Float] + # @param midpoint [Float] + # @return [Bolean] + # Returns true if result is to the right ot the range + def nfv_positive?(left, midpoint) + (nfv(left) * nfv(midpoint) > 0) + end + # @param left [Float] + # @param right [Float] + # @return [Float] IRR of the Cashflow + def format_irr(left, right) + days_in_year = Xirr.config.days_in_year.to_f # Irr for daily cashflow (not in percentage format) irr = (right+left) / 2 # Irr for daily cashflow multiplied by 365 to get yearly return irr = irr * days_in_year # Annualized yield (return) reflecting compounding effect of daily returns irr = (1 + irr / days_in_year) ** days_in_year - 1 - - return irr - end - private - + # Returns the Net future value of the flow given a Rate + # @param rate [Float] + # @return [Float] def nfv(rate) # :nodoc: - - today = self.map(&:date).max.to_date - nfv = 0 - self.each do |t| - cf, date = t.amount, t.date - - datestring = date.to_s - formatteddate = Date.parse(datestring).to_date - t_in_days = (today - formatteddate).numerator / (today - formatteddate).denominator - nfv = nfv + cf * ((1 + rate) ** t_in_days) - + self.inject(0) do |nfv,t| + nfv = nfv + t.amount * ((1 + rate) ** t_in_days(t.date)) end - return nfv + end + # Calculates days until last transaction + # @return [Rational] + # @param date [Time] + def t_in_days(date) + Date.parse(max_date.to_s) - Date.parse(date.to_s) end end diff --git a/lib/xirr/transaction.rb b/lib/xirr/transaction.rb index 226bfbc..e6ecbea 100644 --- a/lib/xirr/transaction.rb +++ b/lib/xirr/transaction.rb @@ -1,9 +1,12 @@ module Xirr + # @abstract A unit of the Cashflow. class Transaction attr_reader :amount attr_accessor :date + # @example + # Transaction.new -1000, date: Time.now def initialize(amount, opts={}) @amount = amount @original = amount @@ -14,30 +17,18 @@ def initialize(amount, opts={}) end end + # Sets the amount + # @param value [Float, Integer] + # @return [Float] def amount=(value) @amount = value.to_f || 0 end + # @return [String] def inspect "T(#{@amount},#{@date})" end - def description - investment ? "#{self.investment.transaction_type_name}: #{round.description}" : @description - end - - def round - @investment.nil? ? nil : investment.round - end - - def company - @company || investment.company - end - - def shareholder - @shareholder || investment.shareholder - end - end end \ No newline at end of file diff --git a/lib/xirr/version.rb b/lib/xirr/version.rb index 8f3d094..2ada3f2 100644 --- a/lib/xirr/version.rb +++ b/lib/xirr/version.rb @@ -1,3 +1,4 @@ module Xirr - VERSION = "0.1.0" + # Version of the Gem + VERSION = "0.1.1" end