Skip to content

Commit

Permalink
Bumped Version; Added Documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
tubedude committed Aug 4, 2014
1 parent 2c82683 commit 2010489
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 63 deletions.
2 changes: 2 additions & 0 deletions lib/xirr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
81 changes: 62 additions & 19 deletions lib/xirr/cashflow.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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?
Expand Down
2 changes: 2 additions & 0 deletions lib/xirr/config.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
61 changes: 34 additions & 27 deletions lib/xirr/main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 7 additions & 16 deletions lib/xirr/transaction.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
3 changes: 2 additions & 1 deletion lib/xirr/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
module Xirr
VERSION = "0.1.0"
# Version of the Gem
VERSION = "0.1.1"
end

0 comments on commit 2010489

Please sign in to comment.