Class: Xirr::Cashflow

Inherits:
Array
  • Object
show all
Defined in:
lib/xirr/cashflow.rb

Overview

Note:

A Cashflow should consist of at least two transactions, one positive and one negative.

Expands [Array] to store a set of transactions which will be used to calculate the XIRR

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(flow: [], period: Xirr.config.period, **options) ⇒ Cashflow

Returns a new instance of Cashflow.

Examples:

Creating a Cashflow

cf = Cashflow.new
cf << Transaction.new( 1000, date: '2013-01-01'.to_date)
cf << Transaction.new(-1234, date: '2013-03-31'.to_date)
Or
cf = Cashflow.new Transaction.new( 1000, date: '2013-01-01'.to_date), Transaction.new(-1234, date: '2013-03-31'.to_date)

Parameters:



16
17
18
19
20
21
22
# File 'lib/xirr/cashflow.rb', line 16

def initialize(flow: [], period: Xirr.config.period, ** options)
  @period   = period
  @fallback = options[:fallback] || Xirr.config.fallback
  @options  = options
  self << flow
  flatten!
end

Instance Attribute Details

#fallbackObject (readonly)

Returns the value of attribute fallback.



7
8
9
# File 'lib/xirr/cashflow.rb', line 7

def fallback
  @fallback
end

#iteration_limitObject (readonly)

Returns the value of attribute iteration_limit.



7
8
9
# File 'lib/xirr/cashflow.rb', line 7

def iteration_limit
  @iteration_limit
end

#optionsObject (readonly)

Returns the value of attribute options.



7
8
9
# File 'lib/xirr/cashflow.rb', line 7

def options
  @options
end

#raise_exceptionObject (readonly)

Returns the value of attribute raise_exception.



7
8
9
# File 'lib/xirr/cashflow.rb', line 7

def raise_exception
  @raise_exception
end

Instance Method Details

#<<(arg) ⇒ Object



190
191
192
193
194
195
196
197
# File 'lib/xirr/cashflow.rb', line 190

def <<(arg)
  super arg
  sort! { |x, y| x.date <=> y.date }
  # Adding a transaction can change the date range and leading sign, so drop
  # the values memoized from the old contents.
  @min_date = @max_date = @first_transaction_direction = nil
  self
end

#compact_cfCashflow

A copy of the cashflow with same-date transactions merged into one.

Returns:



167
168
169
170
171
# File 'lib/xirr/cashflow.rb', line 167

def compact_cf
  compact = Hash.new 0
  each { |flow| compact[flow.date] += flow.amount }
  Cashflow.new(flow: compact.map { |date, amount| Transaction.new(amount, date: date) }, period: period, **options)
end

#invalid?Boolean

Check if Cashflow is invalid

Returns:

  • (Boolean)


26
27
28
# File 'lib/xirr/cashflow.rb', line 26

def invalid?
  inflow.empty? || outflows.empty?
end

#invalid_messageString

Error message depending on the missing transaction

Returns:

  • (String)


181
182
183
184
# File 'lib/xirr/cashflow.rb', line 181

def invalid_message
  return 'No positive transaction' if inflow.empty?
  return 'No negative transaction' if outflows.empty?
end

#irr_guessFloat

A rough starting rate for the solver: the cash-on-cash multiple annualized over the investment horizon, multiple^(1 / years) - 1.

It falls back to 0.0 whenever that estimate is undefined or unusable — an invalid or empty cashflow, a horizon of zero, a non-positive multiple, or a rate at or below -100% (which the solver can't start from). The default rtsafe solver does not depend on this being accurate; it only needs a finite rate above -1.

Returns:

  • (Float)


57
58
59
60
61
62
63
# File 'lib/xirr/cashflow.rb', line 57

def irr_guess
  return @irr_guess = 0.0 unless valid?
  return @irr_guess = 0.0 if periods_of_investment.zero? || multiple <= 0

  guess = ((multiple**(1.0 / periods_of_investment)) - 1).round(3)
  @irr_guess = (guess.nan? || guess.infinite? || guess <= -1) ? 0.0 : guess
end

#max_dateTime

Last investment date

Returns:

  • (Time)


44
45
46
# File 'lib/xirr/cashflow.rb', line 44

def max_date
  @max_date ||= map(&:date).max
end

#min_dateTime

First investment date

Returns:

  • (Time)


175
176
177
# File 'lib/xirr/cashflow.rb', line 175

def min_date
  @min_date ||= map(&:date).min
end

#mirr(finance_rate, reinvest_rate) ⇒ Float

Modified IRR of the dated flows. Positive flows are assumed reinvested at reinvest_rate, negative flows financed at finance_rate. Unlike XIRR it has a closed form and a single answer.

Parameters:

  • finance_rate (Float)
  • reinvest_rate (Float)

Returns:

  • (Float)

Raises:

  • (ArgumentError)


113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/xirr/cashflow.rb', line 113

def mirr(finance_rate, reinvest_rate)
  raise ArgumentError, invalid_message if invalid?
  years = periods_of_investment
  raise ArgumentError, 'Flows span no time' if years.zero?

  future_of_inflows = 0.0
  present_of_outflows = 0.0
  each do |t|
    if t.amount > 0
      future_of_inflows += t.amount * (1.0 + reinvest_rate) ** ((max_date - t.date) / period)
    elsif t.amount < 0
      present_of_outflows += t.amount / (1.0 + finance_rate) ** ((t.date - min_date) / period)
    end
  end

  ((future_of_inflows / -present_of_outflows) ** (1.0 / years) - 1).round(Xirr.config.precision)
end

#periodObject



186
187
188
# File 'lib/xirr/cashflow.rb', line 186

def period
  @temporary_period || @period
end

#sumFloat

Sums all amounts in a cashflow

Returns:

  • (Float)


38
39
40
# File 'lib/xirr/cashflow.rb', line 38

def sum
  map(&:amount).sum
end

#valid?Boolean

Inverse of #invalid?

Returns:

  • (Boolean)


32
33
34
# File 'lib/xirr/cashflow.rb', line 32

def valid?
  !invalid?
end

#xirr(guess: nil, method: nil, **options) ⇒ Float

Returns the rate, or Xirr.config.replace_for_nil when it can't converge.

Parameters:

  • guess (Float) (defaults to: nil)
  • method (Symbol) (defaults to: nil)

Returns:

  • (Float)

    the rate, or Xirr.config.replace_for_nil when it can't converge



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/xirr/cashflow.rb', line 68

def xirr(guess: nil, method: nil, ** options)
  method, options = process_options(method, options)
  if invalid?
    raise ArgumentError, invalid_message if options[:raise_exception]
    return Xirr.config.replace_for_nil
  end

  result = choose_(method).send(:xirr, guess, options)
  # rtsafe already combines a Newton step with a bisection safeguard, so it is
  # the fallback for the fragile solvers; when it is itself the primary there
  # is nothing better to try, and a failure means the flow has no rate.
  if unconverged?(result) && fallback && method != :rtsafe && method != :rtsafe_c
    result = choose_(:rtsafe).send(:xirr, guess, options)
  end
  if unconverged?(result)
    raise ArgumentError, 'XIRR did not converge' if options[:raise_exception]
    return Xirr.config.replace_for_nil
  end
  result
ensure
  # A per-call +period:+ must not leak into later #xnpv / #mirr calls.
  @temporary_period = nil
end

#xirr!(guess: nil, method: nil, **options) ⇒ Float

Same as #xirr, but raises ArgumentError when the cashflow is invalid or the rate can't be found, instead of returning Xirr.config.replace_for_nil.

Returns:

  • (Float)


95
96
97
# File 'lib/xirr/cashflow.rb', line 95

def xirr!(guess: nil, method: nil, ** options)
  xirr(guess: guess, method: method, **options.merge(raise_exception: true))
end

#xnpv(rate) ⇒ Float

Net present value of the dated flows discounted at rate on the same Actual/period basis #xirr uses. Roughly zero when rate is the XIRR.

Parameters:

  • rate (Float)

Returns:

  • (Float)


103
104
105
# File 'lib/xirr/cashflow.rb', line 103

def xnpv(rate)
  inject(0.0) { |sum, t| sum + t.amount / (1.0 + rate) ** ((t.date - min_date) / period) }
end