tubedude/xirr

View on GitHub
lib/xirr/bisection.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

module Xirr
  # Methods that will be included in Cashflow to calculate XIRR
  class Bisection
    include Base

    # Calculates yearly Internal Rate of Return
    # @return [BigDecimal]
    # @param midpoint [Float]
    # An initial guess rate will override the {Cashflow#irr_guess}
    def xirr(midpoint, options)
      # Initial values
      left  = [BigDecimal(-0.99999999, Xirr.config.precision), cf.irr_guess].min
      right = [BigDecimal(9.99999999, Xirr.config.precision), cf.irr_guess + 1].max
      @original_right = right
      midpoint ||= cf.irr_guess

      midpoint, runs = loop_rates(left, midpoint, right, options[:iteration_limit])

      get_answer(midpoint, options, runs)
    end

    private

    # @param midpoint [BigDecimal]
    # @return [Boolean]
    # Checks if result is the right limit.
    def right_limit_reached?(midpoint)
      (@original_right - midpoint).abs < Xirr.config.eps
    end

    # @param left [BigDecimal]
    # @param midpoint [BigDecimal]
    # @param right [BigDecimal]
    # @return [Array]
    # Calculates the Bisections
    def bisection(left, midpoint, right)
      _left = xnpv(left).positive?
      _mid = xnpv(midpoint).positive?
      if _left && _mid
        return left, left, left, true if xnpv(right).positive? # Not Enough Precision in the left to find the IRR
      end
      if _left == _mid
        return midpoint, format_irr(midpoint, right), right, false # Result is to the Right
      else
        return left, format_irr(left, midpoint), midpoint, false # Result is to the Left
      end
    end

    # @param left [Float]
    # @param right [Float]
    # @return [Float] IRR of the Cashflow
    def format_irr(left, right)
      irr = (right + left) / 2
    end

    def get_answer(midpoint, options, runs)
      if runs >= options[:iteration_limit]
        if options[:raise_exception]
          raise ArgumentError, "Did not converge after #{runs} tries."
        end
      else
        midpoint.round Xirr.config.precision
      end
    end

    def loop_rates(left, midpoint, right, iteration_limit)
      runs = 0
      while (right - left).abs > Xirr.config.eps && runs < iteration_limit do
        runs += 1
        left, midpoint, right, should_stop = bisection(left, midpoint, right)
        break if should_stop
        if right_limit_reached?(midpoint)
          right           *= 2
          @original_right *= 2
        end
      end
      [midpoint, runs]
    end
  end
end