xirr 0.7.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,61 +1,35 @@
1
1
  # frozen_string_literal: true
2
- require 'bigdecimal/newton'
3
2
 
4
3
  module Xirr
5
- # Class to calculate IRR using Newton Method
4
+ # Plain Newton-Raphson: step from the guess by -xnpv/xnpv' until the step is
5
+ # smaller than the tolerance. Fast when it converges, but with no bracketing it
6
+ # can walk off to a non-root or below -100%; {RtSafe} is the safeguarded default
7
+ # that avoids that. Kept for `xirr(method: :newton_method)`.
6
8
  class NewtonMethod
7
9
  include Base
8
- include Newton
9
10
 
10
-
11
- # Base class for working with Newton's Method.
12
- # @api private
13
- class Function
14
- values = {
15
- eps: Xirr.config.eps,
16
- one: '1.0',
17
- two: '2.0',
18
- ten: '10.0',
19
- zero: '0.0'
20
- }
21
-
22
- # define default values
23
- values.each do |key, value|
24
- define_method key do
25
- BigDecimal(value, Xirr.config.precision)
26
- end
11
+ # @param guess [Float, nil] initial rate
12
+ # @param options [Hash] reads +:iteration_limit+
13
+ # @return [Float, nil] the rate rounded to +Xirr.config.precision+, or nil
14
+ def xirr(guess, options)
15
+ limit = (options && options[:iteration_limit]) || Xirr.config.iteration_limit
16
+ rate = (guess || cf.irr_guess).to_f
17
+
18
+ limit.times do
19
+ derivative = xnpv_derivative(rate)
20
+ return nil if derivative.zero?
21
+
22
+ step = xnpv(rate).to_f / derivative
23
+ rate -= step
24
+ # Below -100% the discount base (1 + rate) turns negative and the next
25
+ # xnpv raises it to a fractional power, producing a Complex. Bail first.
26
+ return nil if rate.nan? || rate.infinite? || rate <= -1
27
+ break if step.abs < Xirr.config.eps
27
28
  end
28
29
 
29
- # @param transactions [Cashflow]
30
- # @param function [Symbol]
31
- # Initializes the Function with the Cashflow it will use as data source and the function to reduce it.
32
- def initialize(transactions, function)
33
- @transactions = transactions
34
- @function = function
35
- end
36
-
37
- # Necessary for #nlsolve
38
- # @param x [BigDecimal]
39
- def values(x)
40
- value = @transactions.send(@function, BigDecimal(x[0].to_s, Xirr.config.precision))
41
- [BigDecimal(value.to_s, Xirr.config.precision)]
42
- end
43
- end
44
-
45
- # Calculates XIRR using Newton method
46
- # @return [BigDecimal]
47
- # @param guess [Float]
48
- def xirr(guess, _options)
49
- func = Function.new(self, :xnpv)
50
- rate = [guess || cf.irr_guess]
51
- begin
52
- nlsolve(func, rate)
53
- (rate[0] <= -1 || rate[0].nan?) ? nil : rate[0].round(Xirr.config.precision)
54
-
55
- # rate[0].round(Xirr.config.precision)
56
- rescue
57
- nil
58
- end
30
+ rate.nan? ? nil : rate.round(Xirr.config.precision)
31
+ rescue FloatDomainError, Math::DomainError, RangeError
32
+ nil
59
33
  end
60
34
  end
61
35
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xirr
4
+ # Periodic (dateless) cash-flow functions. Where {Cashflow} works with dated
5
+ # flows, these take a plain list of amounts landing at equally spaced periods
6
+ # 0, 1, 2, …, for when the exact dates don't matter. The rate they return is
7
+ # per period.
8
+ module_function
9
+
10
+ # Internal rate of return of +amounts+ at periods 0, 1, 2, …
11
+ #
12
+ # Xirr.irr([-1000, 1100]) # => 0.1
13
+ # Xirr.irr([-1000, 500, 500, 300]) # => 0.156579
14
+ #
15
+ # @param amounts [Array<Numeric>]
16
+ # @param guess [Float] initial rate for the solver
17
+ # @return [Float] the rate per period
18
+ # @raise [ArgumentError] when there aren't at least one inflow and one outflow
19
+ def irr(amounts, guess: 0.1)
20
+ flows = validated_flows(amounts)
21
+ rate = RtSafe.find(flows, guess: guess)
22
+ raise ArgumentError, 'IRR did not converge' if rate.nil?
23
+ rate
24
+ end
25
+
26
+ # Net present value of +amounts+ at periods 0, 1, 2, … discounted at +rate+.
27
+ # The first amount sits at period 0 and is left undiscounted, so
28
+ # +Xirr.npv(Xirr.irr(a), a)+ comes out to roughly zero. This differs from a
29
+ # spreadsheet +NPV+, which places the first amount at period 1.
30
+ #
31
+ # Xirr.npv(0.1, [-1000, 1100]) # => 0.0
32
+ # Xirr.npv(0.1, [-1000, 600, 600]) # => 41.322314
33
+ #
34
+ # @param rate [Numeric]
35
+ # @param amounts [Array<Numeric>]
36
+ # @return [Float]
37
+ def npv(rate, amounts)
38
+ amounts.each_with_index.inject(0.0) do |sum, (amount, i)|
39
+ sum + amount.to_f / (1.0 + rate) ** i
40
+ end.round(Xirr.config.precision)
41
+ end
42
+
43
+ # Modified internal rate of return of periodic +amounts+. Positive flows are
44
+ # assumed reinvested at +reinvest_rate+, negative flows financed at
45
+ # +finance_rate+.
46
+ #
47
+ # Xirr.mirr([-120_000, 39_000, 30_000, 21_000, 37_000, 46_000], 0.10, 0.12)
48
+ # # => 0.126094
49
+ #
50
+ # @param amounts [Array<Numeric>]
51
+ # @param finance_rate [Numeric]
52
+ # @param reinvest_rate [Numeric]
53
+ # @return [Float]
54
+ def mirr(amounts, finance_rate, reinvest_rate)
55
+ values = validated_amounts(amounts)
56
+ periods = values.length - 1
57
+
58
+ future_of_inflows = values.each_with_index.inject(0.0) do |acc, (value, i)|
59
+ value > 0 ? acc + value * (1.0 + reinvest_rate) ** (periods - i) : acc
60
+ end
61
+ present_of_outflows = values.each_with_index.inject(0.0) do |acc, (value, i)|
62
+ value < 0 ? acc + value / (1.0 + finance_rate) ** i : acc
63
+ end
64
+
65
+ ((future_of_inflows / -present_of_outflows) ** (1.0 / periods) - 1).round(Xirr.config.precision)
66
+ end
67
+
68
+ # @api private
69
+ def validated_amounts(amounts)
70
+ values = amounts.map(&:to_f)
71
+ raise ArgumentError, 'Need at least two amounts' if values.length < 2
72
+ unless values.any?(&:positive?) && values.any?(&:negative?)
73
+ raise ArgumentError, 'Need at least one positive and one negative amount'
74
+ end
75
+ values
76
+ end
77
+
78
+ # @api private
79
+ def validated_flows(amounts)
80
+ validated_amounts(amounts).each_with_index.map { |amount, i| [i.to_f, amount] }
81
+ end
82
+
83
+ private_class_method :validated_amounts, :validated_flows
84
+ end
data/lib/xirr/rates.rb ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xirr
4
+ # Converting between the ways an interest rate can be quoted. A nominal rate
5
+ # paired with a compounding frequency, the effective annual rate it actually
6
+ # earns, and a continuously-compounded rate all describe the same return in
7
+ # different terms; these move between them.
8
+ module Rates
9
+ module_function
10
+
11
+ # Effective annual rate earned by a +nominal+ rate compounded +m+ times a
12
+ # year: +(1 + nominal/m)^m − 1+.
13
+ # @return [Float]
14
+ def effective_annual_rate(nominal, m)
15
+ raise ArgumentError, 'm must be positive' if m <= 0
16
+
17
+ (1 + nominal / m)**m - 1
18
+ end
19
+
20
+ # Nominal rate that, compounded +m+ times a year, produces the +effective+
21
+ # annual rate. The inverse of {effective_annual_rate}.
22
+ # @return [Float]
23
+ def nominal_rate(effective, m)
24
+ raise ArgumentError, 'm must be positive' if m <= 0
25
+ raise ArgumentError, 'effective must be above -100%' if 1 + effective <= 0
26
+
27
+ m * ((1 + effective)**(1.0 / m) - 1)
28
+ end
29
+
30
+ # Per-period rate equivalent to a continuously-compounded annual +rate+, for
31
+ # +periods+ periods a year: +e^(rate/periods) − 1+.
32
+ # @return [Float]
33
+ def continuous_to_periodic(rate, periods)
34
+ raise ArgumentError, 'periods must be positive' if periods <= 0
35
+
36
+ Math.exp(rate / periods) - 1
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xirr
4
+ # Performance and risk metrics: {volatility}, {cagr}, {payback_period},
5
+ # {discounted_payback_period}, {profitability_index}, and {twr}.
6
+ #
7
+ # The cash-flow functions follow the same convention as {Xirr.npv}: the initial
8
+ # outlay sits at index 0 (undiscounted) and later flows fall at periods 1, 2, ….
9
+ module Returns
10
+ module_function
11
+
12
+ # Annualised volatility of a price series — the sample standard deviation of
13
+ # its period-over-period returns, scaled up by +√periods_per_year+. Needs at
14
+ # least three positive prices, in time order.
15
+ # @param returns [:simple, :log] how to measure each return: +(b-a)/a+ or +ln(b/a)+
16
+ # @return [Float]
17
+ def volatility(prices, periods_per_year: 252, returns: :simple, precision: Xirr.config.precision)
18
+ rets = period_returns(prices, returns)
19
+ raise ArgumentError, 'need at least three prices' if rets.length < 2
20
+
21
+ round_value(annualise(rets, periods_per_year), precision)
22
+ end
23
+
24
+ # Compound annual growth rate — the constant yearly rate that grows
25
+ # +begin_value+ into +end_value+ over +years+.
26
+ # @return [Float]
27
+ def cagr(begin_value, end_value, years, precision: Xirr.config.precision)
28
+ if begin_value <= 0 || years <= 0 || end_value.to_f / begin_value < 0
29
+ raise ArgumentError, 'undefined'
30
+ end
31
+
32
+ round_value((end_value.to_f / begin_value)**(1.0 / years) - 1, precision)
33
+ end
34
+
35
+ # Payback period — how many periods of cash flow it takes to recover the
36
+ # initial outlay, interpolating within the recovering period. The first
37
+ # amount is the outlay (negative), the rest are inflows.
38
+ # @return [Float]
39
+ def payback_period(cash_flows, precision: Xirr.config.precision)
40
+ recovery(cash_flows, precision)
41
+ end
42
+
43
+ # Like {payback_period}, but recovers the outlay from cash flows discounted
44
+ # at +rate+, so it accounts for the time value of money.
45
+ # @return [Float]
46
+ def discounted_payback_period(cash_flows, rate, precision: Xirr.config.precision)
47
+ recovery(discount_flows(cash_flows, rate), precision)
48
+ end
49
+
50
+ # Profitability index — the present value of future inflows per unit of
51
+ # initial investment, discounted at +rate+. Above 1 means the project adds
52
+ # value. Equivalent to +1 + NPV / initial investment+.
53
+ # @return [Float]
54
+ def profitability_index(cash_flows, rate, precision: Xirr.config.precision)
55
+ raise ArgumentError, 'need at least one flow' if cash_flows.empty?
56
+ raise ArgumentError, 'the first flow must be an outlay (negative)' if cash_flows.first >= 0
57
+
58
+ npv = Xirr.npv(rate, cash_flows)
59
+ round_value(1 + npv / -cash_flows.first, precision)
60
+ end
61
+
62
+ # Time-weighted return — period returns linked geometrically,
63
+ # +∏(1 + rᵢ) − 1+. Immune to the timing of cash flows. Pass
64
+ # +periods_per_year+ to annualise.
65
+ # @return [Float]
66
+ def twr(period_returns, periods_per_year: nil, precision: Xirr.config.precision)
67
+ raise ArgumentError, 'need at least one return' if period_returns.empty?
68
+ raise ArgumentError, 'returns must be numbers' unless period_returns.all? { |r| r.is_a?(Numeric) }
69
+
70
+ round_value(time_weighted(period_returns, periods_per_year), precision)
71
+ end
72
+
73
+ # --- helpers ------------------------------------------------------------
74
+
75
+ def round_value(value, precision)
76
+ value.round(precision) + 0.0
77
+ end
78
+
79
+ # Consecutive-pair returns. Raises if a price is non-numeric or non-positive.
80
+ def period_returns(prices, kind)
81
+ prices.each_cons(2).map do |a, b|
82
+ raise ArgumentError, 'prices must be numbers' unless a.is_a?(Numeric) && b.is_a?(Numeric)
83
+ raise ArgumentError, 'every price must be positive' unless a.positive? && b.positive?
84
+
85
+ kind == :log ? Math.log(b.to_f / a) : (b - a).to_f / a
86
+ end
87
+ end
88
+
89
+ def annualise(returns, periods_per_year)
90
+ n = returns.length
91
+ mean = returns.sum / n.to_f
92
+ sum_of_squares = returns.sum { |r| (r - mean)**2 }
93
+ Math.sqrt(sum_of_squares / (n - 1)) * Math.sqrt(periods_per_year)
94
+ end
95
+
96
+ def recovery(flows, precision)
97
+ whole, shortfall, recovering_flow = cumulative_recovery(flows)
98
+ round_value(whole + shortfall / recovering_flow, precision)
99
+ end
100
+
101
+ # Find the first period where the running cumulative crosses from negative to
102
+ # non-negative. Raises when it never crosses or there are no flows.
103
+ def cumulative_recovery(flows)
104
+ raise ArgumentError, 'need at least one flow' if flows.empty?
105
+
106
+ cumulative = 0.0
107
+ flows.each_with_index do |flow, i|
108
+ nxt = cumulative + flow
109
+ return [i - 1, -cumulative, flow] if i.positive? && cumulative.negative? && nxt >= 0
110
+
111
+ cumulative = nxt
112
+ end
113
+ raise ArgumentError, 'the outlay is never recovered'
114
+ end
115
+
116
+ def discount_flows(flows, rate)
117
+ flows.each_with_index.map { |flow, i| flow / (1 + rate)**i }
118
+ end
119
+
120
+ def time_weighted(returns, periods_per_year)
121
+ cumulative = returns.reduce(1.0) { |acc, r| acc * (1 + r) } - 1
122
+ return cumulative if periods_per_year.nil?
123
+
124
+ (1 + cumulative)**(periods_per_year.to_f / returns.length) - 1
125
+ end
126
+
127
+ private_class_method :round_value, :period_returns, :annualise, :recovery,
128
+ :cumulative_recovery, :discount_flows, :time_weighted
129
+ end
130
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xirr
4
+ # Safeguarded Newton-Raphson root finder — the classic +rtsafe+.
5
+ #
6
+ # It brackets a sign change of the net present value first, then each iteration
7
+ # takes a Newton step when that step lands inside the bracket and is shrinking
8
+ # the interval fast enough, and a bisection step otherwise. This keeps Newton's
9
+ # speed on well-behaved flows while retaining bisection's guaranteed
10
+ # convergence, in a single pass rather than running Newton to exhaustion and
11
+ # then bisecting separately.
12
+ #
13
+ # The maintained bracket always encloses a sign change, so the result is a
14
+ # genuine root rather than a stalled non-root, and a long-dated flow whose raw
15
+ # Newton step would overflow takes a bisection step instead.
16
+ class RtSafe
17
+ include Base
18
+
19
+ # Stop expanding the upper bound once it passes this — the flow has no root
20
+ # in a sane rate range.
21
+ BRACKET_CEILING = 1.0e7
22
+
23
+ # Solves the compacted {Cashflow} the instance was built with.
24
+ # @param guess [Float, nil] initial rate; used when it lands inside the bracket
25
+ # @param options [Hash] reads +:iteration_limit+
26
+ # @return [Float, nil] the rate rounded to +Xirr.config.precision+, or nil when it can't converge
27
+ def xirr(guess, options)
28
+ limit = (options && options[:iteration_limit]) || Xirr.config.iteration_limit
29
+ start = guess || cf.irr_guess
30
+ RtSafe.find(flows, guess: start.to_f, iteration_limit: limit)
31
+ end
32
+
33
+ # Pure solver over normalized +[time, amount]+ flows (time in years/periods).
34
+ # Shared by the dated {Cashflow} path and the periodic {Xirr} module helpers.
35
+ # @param flows [Array<Array(Float, Float)>]
36
+ # @return [Float, nil]
37
+ def self.find(flows, guess: 0.1, tolerance: Xirr.config.eps, iteration_limit: Xirr.config.iteration_limit, precision: Xirr.config.precision)
38
+ rate = rtsafe(flows, guess.to_f, tolerance.to_f, iteration_limit)
39
+ return nil if rate.nil? || rate.nan? || rate.infinite?
40
+
41
+ # Round before the floor check: a rate just above -1 can round down to it.
42
+ rounded = rate.round(precision)
43
+ rounded <= -1.0 ? nil : rounded
44
+ rescue FloatDomainError, Math::DomainError
45
+ nil
46
+ end
47
+
48
+ # Net present value of +flows+ at +rate+: Σ amount / (1 + rate)^t
49
+ def self.present_value(flows, rate)
50
+ flows.inject(0.0) { |sum, (t, amount)| sum + amount / (1.0 + rate) ** t }
51
+ end
52
+
53
+ # Derivative of the NPV with respect to rate: Σ -t · amount / (1 + rate)^(t+1)
54
+ def self.present_value_derivative(flows, rate)
55
+ flows.inject(0.0) { |sum, (t, amount)| sum + (-t * amount / (1.0 + rate) ** (t + 1)) }
56
+ end
57
+
58
+ # Bracket a sign change, then run the safeguarded iteration from +guess+
59
+ # (when it falls inside the bracket) or the midpoint.
60
+ def self.rtsafe(flows, guess, tol, iteration_limit)
61
+ low = safe_low(flows)
62
+ f_low = present_value(flows, low)
63
+ bounds = bracket(flows, low, f_low, 1.0)
64
+ return nil if bounds.nil?
65
+
66
+ a, b = bounds
67
+ # a == low, so the NPV at a is f_low. Orient so it is negative at xlo and
68
+ # positive at xhi — the invariant the step selection relies on.
69
+ xlo, xhi = f_low < 0.0 ? [a, b] : [b, a]
70
+ x = (guess > a && guess < b) ? guess : (a + b) / 2.0
71
+ f = present_value(flows, x)
72
+ df = present_value_derivative(flows, x)
73
+ search(flows, x, xlo, xhi, f, df, (b - a).abs, tol, iteration_limit)
74
+ end
75
+
76
+ # Iterate to the root, taking a Newton or bisection step each time. Looped
77
+ # (not recursed) so a large +iteration_limit+ can't overflow the stack.
78
+ # @return [Float, nil] the root, or nil if it doesn't converge within +iters+
79
+ def self.search(flows, x, xlo, xhi, f, df, dxold, tol, iters)
80
+ iters.times do
81
+ nxt, dx = move(x, xlo, xhi, f, df, dxold)
82
+ return nxt if dx.abs < tol
83
+
84
+ f = present_value(flows, nxt)
85
+ df = present_value_derivative(flows, nxt)
86
+ f < 0.0 ? xlo = nxt : xhi = nxt
87
+ x = nxt
88
+ dxold = dx
89
+ end
90
+ nil
91
+ end
92
+
93
+ # A Newton step when it's usable, a bisection step otherwise.
94
+ # @return [Array(Float, Float)] +[next_x, step]+
95
+ def self.move(x, xlo, xhi, f, df, dxold)
96
+ if newton_usable?(x, xlo, xhi, f, df, dxold)
97
+ dx = f / df
98
+ [x - dx, dx]
99
+ else
100
+ dx = (xhi - xlo) / 2.0
101
+ [xlo + dx, dx]
102
+ end
103
+ end
104
+
105
+ # Prefer Newton when the derivative isn't flat, the step lands inside the
106
+ # bracket, and it shrinks the interval by at least half. Comparing the Newton
107
+ # point against the bracket — rather than the classic product form — avoids an
108
+ # overflow in the steep zone near the bracket's floor. +df != 0+ short-circuits
109
+ # before +x - f / df+.
110
+ def self.newton_usable?(x, xlo, xhi, f, df, dxold)
111
+ df != 0.0 && inside?(x - f / df, xlo, xhi) && (2.0 * f).abs <= (dxold * df).abs
112
+ end
113
+
114
+ def self.inside?(point, xlo, xhi)
115
+ point >= [xlo, xhi].min && point <= [xlo, xhi].max
116
+ end
117
+
118
+ # The bracket's floor. As +rate+ nears -1, +(1 + rate)^t+ underflows to zero
119
+ # (then divides by zero) for large +t+, so raise the floor just enough that the
120
+ # longest-dated flow's discount factor stays finite. For short-dated flows this
121
+ # is the familiar -0.999999; for a 30-year monthly schedule it sits higher.
122
+ def self.safe_low(flows)
123
+ max_t = flows.map { |t, _amount| t }.max
124
+ max_t = 1.0 if max_t.nil? || max_t < 1.0
125
+ [1.0e-290 ** (1.0 / max_t), 1.0e-6].max - 1.0
126
+ end
127
+
128
+ # Expand the upper bound until the NPV changes sign, giving a bracket.
129
+ def self.bracket(flows, low, f_low, high)
130
+ return nil if high > BRACKET_CEILING
131
+
132
+ if straddles_zero?(f_low, present_value(flows, high))
133
+ [low, high]
134
+ else
135
+ bracket(flows, low, f_low, high * 2 + 1)
136
+ end
137
+ end
138
+
139
+ # Whether +a+ and +b+ sit on opposite sides of zero. Comparing signs rather
140
+ # than the product +a * b+ avoids overflow when the NPV is astronomically large
141
+ # near the bracket's floor for long-dated flows.
142
+ def self.straddles_zero?(a, b)
143
+ (a <= 0 && b >= 0) || (a >= 0 && b <= 0)
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xirr
4
+ # True when the native rtsafe extension compiled and loaded.
5
+ begin
6
+ require 'xirr/xirr_native'
7
+ NATIVE = true
8
+ rescue LoadError
9
+ NATIVE = false
10
+ end
11
+
12
+ # C-backed rtsafe. Same algorithm and results as {RtSafe}, run in a native
13
+ # extension. Only usable when {NATIVE} is true (the extension was compiled);
14
+ # otherwise the gem runs the pure-Ruby {RtSafe} instead.
15
+ class RtSafeC
16
+ include Base
17
+
18
+ # @param guess [Float, nil]
19
+ # @param options [Hash] reads +:iteration_limit+
20
+ # @return [Float, nil]
21
+ def xirr(guess, options)
22
+ limit = (options && options[:iteration_limit]) || Xirr.config.iteration_limit
23
+ start = (guess || cf.irr_guess).to_f
24
+ rate = Xirr::Native.rtsafe(flows, start, Xirr.config.eps.to_f, limit)
25
+ return nil if rate.nil?
26
+
27
+ # Round before the floor check: a rate just above -1 can round down to it.
28
+ rounded = rate.round(Xirr.config.precision)
29
+ rounded <= -1.0 ? nil : rounded
30
+ end
31
+ end
32
+ end
@@ -6,17 +6,12 @@ module Xirr
6
6
  attr_reader :amount, :date
7
7
 
8
8
  # @example
9
- # Transaction.new -1000, date: Date.now
9
+ # Transaction.new(-1000, date: Date.new(2013, 1, 1))
10
10
  # @param amount [Numeric]
11
- # @param opts [Hash]
12
- # @note Don't forget to add date: [Date] in the opts hash.
11
+ # @param opts [Hash] must include +:date+, the date the amount falls on
13
12
  def initialize(amount, opts = {})
14
13
  self.amount = amount
15
-
16
- # Set optional attributes..
17
- opts.each do |key, value|
18
- send("#{key}=", value)
19
- end
14
+ opts.each { |key, value| send("#{key}=", value) }
20
15
  end
21
16
 
22
17
  # Sets the date
@@ -30,7 +25,7 @@ module Xirr
30
25
  # @param value [Numeric]
31
26
  # @return [Float]
32
27
  def amount=(value)
33
- @amount = value.to_f || 0.0
28
+ @amount = value.to_f
34
29
  end
35
30
 
36
31
  # @return [String]