Class: Xirr::Cashflow
- Inherits:
-
Array
- Object
- Array
- Xirr::Cashflow
- Defined in:
- lib/xirr/cashflow.rb
Overview
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
-
#fallback ⇒ Object
readonly
Returns the value of attribute fallback.
-
#iteration_limit ⇒ Object
readonly
Returns the value of attribute iteration_limit.
-
#options ⇒ Object
readonly
Returns the value of attribute options.
-
#raise_exception ⇒ Object
readonly
Returns the value of attribute raise_exception.
Instance Method Summary collapse
- #<<(arg) ⇒ Object
-
#compact_cf ⇒ Cashflow
A copy of the cashflow with same-date transactions merged into one.
-
#initialize(flow: [], period: Xirr.config.period, **options) ⇒ Cashflow
constructor
A new instance of Cashflow.
-
#invalid? ⇒ Boolean
Check if Cashflow is invalid.
-
#invalid_message ⇒ String
Error message depending on the missing transaction.
-
#irr_guess ⇒ Float
A rough starting rate for the solver: the cash-on-cash multiple annualized over the investment horizon,
multiple^(1 / years) - 1. -
#max_date ⇒ Time
Last investment date.
-
#min_date ⇒ Time
First investment date.
-
#mirr(finance_rate, reinvest_rate) ⇒ Float
Modified IRR of the dated flows.
- #period ⇒ Object
-
#sum ⇒ Float
Sums all amounts in a cashflow.
-
#valid? ⇒ Boolean
Inverse of #invalid?.
-
#xirr(guess: nil, method: nil, **options) ⇒ Float
The rate, or
Xirr.config.replace_for_nilwhen it can't converge. -
#xirr!(guess: nil, method: nil, **options) ⇒ Float
Same as #xirr, but raises
ArgumentErrorwhen the cashflow is invalid or the rate can't be found, instead of returningXirr.config.replace_for_nil. -
#xnpv(rate) ⇒ Float
Net present value of the dated flows discounted at
rateon the same Actual/period basis #xirr uses.
Constructor Details
#initialize(flow: [], period: Xirr.config.period, **options) ⇒ Cashflow
Returns a new instance of Cashflow.
16 17 18 19 20 21 22 |
# File 'lib/xirr/cashflow.rb', line 16 def initialize(flow: [], period: Xirr.config.period, ** ) @period = period @fallback = [:fallback] || Xirr.config.fallback @options = self << flow flatten! end |
Instance Attribute Details
#fallback ⇒ Object (readonly)
Returns the value of attribute fallback.
7 8 9 |
# File 'lib/xirr/cashflow.rb', line 7 def fallback @fallback end |
#iteration_limit ⇒ Object (readonly)
Returns the value of attribute iteration_limit.
7 8 9 |
# File 'lib/xirr/cashflow.rb', line 7 def iteration_limit @iteration_limit end |
#options ⇒ Object (readonly)
Returns the value of attribute options.
7 8 9 |
# File 'lib/xirr/cashflow.rb', line 7 def @options end |
#raise_exception ⇒ Object (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_cf ⇒ Cashflow
A copy of the cashflow with same-date transactions merged into one.
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, **) end |
#invalid? ⇒ Boolean
Check if Cashflow is invalid
26 27 28 |
# File 'lib/xirr/cashflow.rb', line 26 def invalid? inflow.empty? || outflows.empty? end |
#invalid_message ⇒ String
Error message depending on the missing transaction
181 182 183 184 |
# File 'lib/xirr/cashflow.rb', line 181 def return 'No positive transaction' if inflow.empty? return 'No negative transaction' if outflows.empty? end |
#irr_guess ⇒ Float
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.
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_date ⇒ Time
Last investment date
44 45 46 |
# File 'lib/xirr/cashflow.rb', line 44 def max_date @max_date ||= map(&:date).max end |
#min_date ⇒ Time
First investment date
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.
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, 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 |
#period ⇒ Object
186 187 188 |
# File 'lib/xirr/cashflow.rb', line 186 def period @temporary_period || @period end |
#sum ⇒ Float
Sums all amounts in a cashflow
38 39 40 |
# File 'lib/xirr/cashflow.rb', line 38 def sum map(&:amount).sum end |
#valid? ⇒ Boolean
Inverse of #invalid?
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.
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, ** ) method, = (method, ) if invalid? raise ArgumentError, if [:raise_exception] return Xirr.config.replace_for_nil end result = choose_(method).send(:xirr, guess, ) # 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, ) end if unconverged?(result) raise ArgumentError, 'XIRR did not converge' if [: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.
95 96 97 |
# File 'lib/xirr/cashflow.rb', line 95 def xirr!(guess: nil, method: nil, ** ) xirr(guess: guess, method: method, **.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.
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 |