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.
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;
}