Search code examples
asp.net.netvb.netexceptionfinance

Microsoft.VisualBasic.Financial.Rate errors with "Cannot calculate rate using the arguments provided"


So we have a rates calculator class in our ASP.NET4 web app that uses the Microsoft.VisualBasic.Financial.Rate to calculate a nominal rate (based on input parameters).

We noticed that for high values of NPer (total number of payment periods, e.g. 50 years x monthly payments = 600) the function would throw an exception: Cannot calculate rate using the arguments provided.

Searching around we did not find any solutions to this, so I am posting the solution here. A requirement for us was to maintain a function that as closely as possible implemented the same algorithm as the above, as we needed to produce exactly the same outputs.


Solution

  • Answering my own question, for any future coders who run into this problem - we used dotPeek to decompile the module, which produced the following:

    public static double Rate(double NPer, double Pmt, double PV, double FV = 0.0, DueDate Due = DueDate.EndOfPeriod, double Guess = 0.1)
    {
      if (NPer <= 0.0)
        throw new ArgumentException(Utils.GetResourceString("Rate_NPerMustBeGTZero"));
      double Rate1 = Guess;
      double num1 = Financial.LEvalRate(Rate1, NPer, Pmt, PV, FV, Due);
      double Rate2 = num1 <= 0.0 ? Rate1 * 2.0 : Rate1 / 2.0;
      double num2 = Financial.LEvalRate(Rate2, NPer, Pmt, PV, FV, Due);
      int num3 = 0;
      do
      {
        if (num2 == num1)
        {
          if (Rate2 > Rate1)
            Rate1 -= 1E-05;
          else
            Rate1 -= -1E-05;
          num1 = Financial.LEvalRate(Rate1, NPer, Pmt, PV, FV, Due);
          if (num2 == num1)
            throw new ArgumentException(Utils.GetResourceString("Financial_CalcDivByZero"));
        }
        double Rate3 = Rate2 - (Rate2 - Rate1) * num2 / (num2 - num1);
        double num4 = Financial.LEvalRate(Rate3, NPer, Pmt, PV, FV, Due);
        if (Math.Abs(num4) < 1E-07)
          return Rate3;
        double num5 = num4;
        num1 = num2;
        num2 = num5;
        double num6 = Rate3;
        Rate1 = Rate2;
        Rate2 = num6;
        checked { ++num3; }
      }
      while (num3 <= 39);
      throw new ArgumentException(Utils.GetResourceString("Financial_CannotCalculateRate"));
    }
    
    private static double LEvalRate(double Rate, double NPer, double Pmt, double PV, double dFv, DueDate Due)
    {
      if (Rate == 0.0)
        return PV + Pmt * NPer + dFv;
      double num1 = Math.Pow(Rate + 1.0, NPer);
      double num2 = Due == DueDate.EndOfPeriod ? 1.0 : 1.0 + Rate;
      return PV * num1 + Pmt * num2 * (num1 - 1.0) / Rate + dFv;
    }
    

    We can see the error is thrown if num3 is exceeded, as it has a hard limit at 39. We tidied up the code a little, and increased the limit to 100:

    private static double CalculateUpfrontNominalRate(double numberOfPeriods, double payment, double presentValue, double futureValue = 0.0, DueDate Due = DueDate.EndOfPeriod, double Guess = 0.1)
        {
            if (numberOfPeriods <= 0.0)
            {
                throw new ArgumentException("CalculateUpfrontNominalRate: Number of periods must be greater than zero");
            }
    
            var rateUpperBoundary = Guess;
            var lEvalRate1 = LEvalRate(rateUpperBoundary, numberOfPeriods, payment, presentValue, futureValue, Due);
            var rateLowerBoundary = lEvalRate1 <= 0.0 ? rateUpperBoundary * 2.0 : rateUpperBoundary / 2.0;
            var lEvalRate2 = LEvalRate(rateLowerBoundary, numberOfPeriods, payment, presentValue, futureValue, Due);
    
            for (var i = 0; i < 100; i++)
            {
                if (lEvalRate2 == lEvalRate1)
                {
                    if (rateLowerBoundary > rateUpperBoundary)
                        rateUpperBoundary -= 1E-05;
                    else
                        rateUpperBoundary -= -1E-05;
    
                    lEvalRate1 = LEvalRate(rateUpperBoundary, numberOfPeriods, payment, presentValue, futureValue, Due);
                    if (lEvalRate2 == lEvalRate1)
                    {
                        throw new ArgumentException("CalculateUpfrontNominalRate: Inputs will cause a divsion by zero");
                    }
                }
    
                double temporaryRate = rateLowerBoundary - (rateLowerBoundary - rateUpperBoundary) * lEvalRate2 / (lEvalRate2 - lEvalRate1);
                double lEvalRate3 = LEvalRate(temporaryRate, numberOfPeriods, payment, presentValue, futureValue, Due);
    
                if (Math.Abs(lEvalRate3) < 1E-07)
                {
                    return temporaryRate;
                }
    
                lEvalRate1 = lEvalRate2;
                lEvalRate2 = lEvalRate3;
                rateUpperBoundary = rateLowerBoundary;
                rateLowerBoundary = temporaryRate;
            }
    
            throw new ArgumentException("CalculateUpfrontNominalRate: The maximum number of iterations has been exceeded, unable to calculate rate");
        }
    
        private static double LEvalRate(double Rate, double NPer, double Pmt, double PV, double dFv, DueDate Due)
        {
            if (Rate == 0.0)
                return PV + Pmt * NPer + dFv;
            double num1 = Math.Pow(Rate + 1.0, NPer);
            double num2 = Due == DueDate.EndOfPeriod ? 1.0 : 1.0 + Rate;
            return PV * num1 + Pmt * num2 * (num1 - 1.0) / Rate + dFv;
        }