Module: Finrb::Cashflow

Included in:
Array
Defined in:
lib/finrb/cashflows.rb,
sig/finrb.rbs

Overview

Provides methods for working with cash flows (collections of transactions)

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.irr(cashflows, guess = nil) ⇒ decimal

Parameters:

  • (Enumerable[number])
  • (number)

Returns:

  • (decimal)


17
18
19
# File 'lib/finrb/cashflows.rb', line 17

def irr(cashflows, guess = nil)
  sequence(cashflows).irr(guess)
end

.mirr(cashflows, finance_rate:, reinvestment_rate:) ⇒ decimal

Parameters:

  • (Enumerable[number])
  • finance_rate: (number)
  • reinvestment_rate: (number)

Returns:

  • (decimal)


25
26
27
# File 'lib/finrb/cashflows.rb', line 25

def mirr(cashflows, finance_rate:, reinvestment_rate:)
  sequence(cashflows).mirr(finance_rate:, reinvestment_rate:)
end

.npv(cashflows, rate) ⇒ decimal

Parameters:

  • (Enumerable[number])
  • (number)

Returns:

  • (decimal)


21
22
23
# File 'lib/finrb/cashflows.rb', line 21

def npv(cashflows, rate)
  sequence(cashflows).npv(rate)
end

.xirr(transactions, guess = nil) ⇒ Rate

Parameters:

Returns:



29
30
31
# File 'lib/finrb/cashflows.rb', line 29

def xirr(transactions, guess = nil)
  sequence(transactions).xirr(guess)
end

.xnpv(transactions, rate) ⇒ decimal

Parameters:

Returns:

  • (decimal)


33
34
35
# File 'lib/finrb/cashflows.rb', line 33

def xnpv(transactions, rate)
  sequence(transactions).xnpv(rate)
end

Instance Method Details

#irr(guess = nil) ⇒ Flt::DecNum

Calculate the per-period internal rate of return for an ordered sequence of equally spaced cashflows.

For cashflows with multiple sign-changing roots, the guess determines which nearby root is selected. Rates must be greater than -1.

Examples:

Finrb::Cashflow.irr([-4000,1200,1410,1875,1050]) #=> 0.143

Parameters:

  • guess (Numeric, nil) (defaults to: nil)

    initial rate used for root selection; defaults to Finrb.config.guess

  • (number)

Returns:

  • (Flt::DecNum)

    the per-period internal rate of return

Raises:

  • (InvalidCashflowError)

    if the sequence lacks both cashflow signs

  • (ArgumentError)

    if the guess is not numeric

  • (DomainError)

    if the rate domain or function evaluation is invalid

  • (ConvergenceError)

    if no root can be bracketed or solved

See Also:



61
62
63
64
65
66
67
68
# File 'lib/finrb/cashflows.rb', line 61

def irr(guess = nil)
  validate_numeric_cashflows!

  # Make sure we have a valid sequence of cash flows.
  raise(InvalidCashflowError, 'Cashflow needs at least one positive and one negative value.') if none?(&:positive?) || none?(&:negative?)

  solve(:npv, valid(guess))
end

#mirr(finance_rate:, reinvestment_rate:) ⇒ Flt::DecNum

Calculate the modified internal rate of return for equally spaced cashflows using separate financing and reinvestment assumptions.

Parameters:

  • finance_rate: (number)
  • reinvestment_rate: (number)

Returns:

  • (Flt::DecNum)

    modified per-period internal rate of return

Raises:



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/finrb/cashflows.rb', line 93

def mirr(finance_rate:, reinvestment_rate:)
  validate_numeric_cashflows!
  raise(InvalidCashflowError, 'MIRR requires at least two cashflows.') if size < 2

  cashflows = map { |entry| Validation.decimal(entry, name: 'cashflow amount') }
  raise(InvalidCashflowError, 'Cashflow needs at least one positive and one negative value.') if cashflows.none?(&:positive?) || cashflows.none?(&:negative?)

  finance_rate = Validation.decimal_greater_than(finance_rate, minimum: -1, name: 'finance rate', error: DomainError)
  reinvestment_rate = Validation.decimal_greater_than(reinvestment_rate, minimum: -1, name: 'reinvestment rate', error: DomainError)

  last_period = cashflows.size - 1
  future_positive =
    cashflows.each_with_index.sum do |amount, index|
      amount.positive? ? amount * ((reinvestment_rate + 1)**(last_period - index)) : Flt::DecNum(0)
    end
  present_negative =
    cashflows.each_with_index.sum do |amount, index|
      amount.negative? ? amount / ((finance_rate + 1)**index) : Flt::DecNum(0)
    end

  ((future_positive / -present_negative)**(Flt::DecNum(1) / last_period)) - 1
end

#npv(rate) ⇒ Flt::DecNum

calculate the net present value of a sequence of cash flows

Examples:

Finrb::Cashflow.npv([-100.0, 60, 60, 60], 0.1) #=> 49.211

Parameters:

  • rate (Numeric)

    the discount rate to be applied

  • (number)

Returns:

See Also:



76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/finrb/cashflows.rb', line 76

def npv(rate)
  validate_numeric_cashflows!
  cashflows = map { |entry| Validation.decimal(entry, name: 'cashflow amount') }

  rate = Validation.decimal_greater_than(rate, minimum: -1, name: 'rate', error: DomainError)

  total = Flt::DecNum.new(0.to_s)
  cashflows.each_with_index do |cashflow, index|
    total += cashflow / ((rate + 1)**index)
  end

  total
end

#xirr(guess = nil) ⇒ Rate

Calculate the effective annual internal rate of return for an ordered sequence of dated transactions.

Under the default configuration, date offsets are actual calendar days from the first transaction and a 365-day year is used. Transactions should be supplied chronologically and their dates must respond to to_date. For multiple roots, the guess determines which nearby root is selected. Rates must be greater than -1.

Examples:

@transactions = []
@transactions << Transaction.new(-1000, :date => Time.new(1985,01,01))
@transactions << Transaction.new(  600, :date => Time.new(1990,01,01))
@transactions << Transaction.new(  600, :date => Time.new(1995,01,01))
Finrb::Cashflow.xirr(@transactions, 0.6) #=> Rate("0.024851", :effective, :compounds => :annually)

Parameters:

  • guess (Numeric, nil) (defaults to: nil)

    initial rate used for root selection; defaults to Finrb.config.guess

  • (number)

Returns:

  • (Rate)

    the effective annual internal rate of return

Raises:

  • (InvalidCashflowError)

    if the sequence lacks both cashflow signs

  • (ArgumentError)

    if the guess is not numeric

  • (DomainError)

    if the rate domain or function evaluation is invalid

  • (ConvergenceError)

    if no root can be bracketed or solved



137
138
139
140
141
142
143
144
145
# File 'lib/finrb/cashflows.rb', line 137

def xirr(guess = nil)
  validate_dated_cashflows!

  # Make sure we have a valid sequence of cash flows.
  raise(InvalidCashflowError, 'Cashflow needs at least one positive and one negative value.') if none? { |transaction| transaction.amount.positive? } || none? { |transaction| transaction.amount.negative? }

  rate = solve(:xnpv, valid(guess))
  Rate.new(rate, :effective, compounds: Finrb.config.periodic_compound ? :continuously : :annually)
end

#xnpv(rate) ⇒ Flt::DecNum

calculate the net present value of a sequence of cash flows

Examples:

@transactions = []
@transactions << Transaction.new(-1000, :date => Time.new(1985,01,01))
@transactions << Transaction.new(  600, :date => Time.new(1990,01,01))
@transactions << Transaction.new(  600, :date => Time.new(1995,01,01))
Finrb::Cashflow.xnpv(@transactions, 0.6).round(2) #=> -937.41

Parameters:

  • (number)

Returns:



155
156
157
158
159
160
161
162
# File 'lib/finrb/cashflows.rb', line 155

def xnpv(rate)
  validate_dated_cashflows!
  rate = Validation.decimal_greater_than(rate, minimum: -1, name: 'rate', error: DomainError)

  sum do |t|
    t.amount / ((rate + 1)**(date_diff(start, t.date) / days_in_period))
  end
end