181 lines
5.8 KiB
C#
181 lines
5.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using EvoCalculator.Core.Models.Calculation.Models;
|
|
|
|
namespace EvoCalculator.Core.FinanceFormulas
|
|
{
|
|
public class XIRR
|
|
{
|
|
private readonly Flow[] _flows;
|
|
private readonly double _guess = 0.1;
|
|
|
|
private const int MaxIterations = 100;
|
|
private const double DefaultTolerance = 1E-6;
|
|
private const double DefaultGuess = 0.1;
|
|
|
|
public XIRR(Flow[] flows)
|
|
{
|
|
_flows = flows;
|
|
}
|
|
|
|
public XIRR(Flow[] flows, double guess)
|
|
{
|
|
_flows = flows;
|
|
_guess = guess;
|
|
}
|
|
|
|
|
|
private static double NewtonsMethodImplementation(IEnumerable<Flow> flows,
|
|
double guess = DefaultGuess,
|
|
double tolerance = DefaultTolerance,
|
|
int maxIterations = MaxIterations)
|
|
{
|
|
var x0 = guess;
|
|
var i = 0;
|
|
double error;
|
|
do
|
|
{
|
|
var dfx0 = new XNPV(flows, x0).GetResultPrime();
|
|
if (Math.Abs(dfx0 - 0) < double.Epsilon)
|
|
throw new InvalidOperationException("Could not calculate: No solution found. df(x) = 0");
|
|
|
|
var fx0 = new XNPV(flows, x0).GetResultPrime();
|
|
var x1 = x0 - fx0 / dfx0;
|
|
error = Math.Abs(x1 - x0);
|
|
|
|
x0 = x1;
|
|
} while (error > tolerance && ++i < maxIterations);
|
|
|
|
if (i == maxIterations)
|
|
throw new InvalidOperationException("Could not calculate: No solution found. Max iterations reached.");
|
|
|
|
return x0;
|
|
}
|
|
|
|
public struct Brackets
|
|
{
|
|
public readonly double First;
|
|
public readonly double Second;
|
|
|
|
private Brackets(double first, double second)
|
|
{
|
|
First = first;
|
|
Second = second;
|
|
}
|
|
|
|
internal static Brackets Find(IEnumerable<Flow> flows,
|
|
double guess = DefaultGuess,
|
|
int maxIterations = MaxIterations)
|
|
{
|
|
const double bracketStep = 0.5;
|
|
var leftBracket = guess - bracketStep;
|
|
var rightBracket = guess + bracketStep;
|
|
var i = 0;
|
|
while (new XNPV(flows, leftBracket).GetResult() * new XNPV(flows, rightBracket).GetResult() > 0 &&
|
|
i++ < maxIterations)
|
|
{
|
|
leftBracket -= bracketStep;
|
|
rightBracket += bracketStep;
|
|
}
|
|
|
|
return i >= maxIterations
|
|
? new Brackets(0, 0)
|
|
: new Brackets(leftBracket, rightBracket);
|
|
}
|
|
}
|
|
|
|
|
|
private static double BisectionMethodImplementation(IEnumerable<Flow> flows,
|
|
double tolerance = DefaultTolerance,
|
|
int maxIterations = MaxIterations)
|
|
{
|
|
// From "Applied Numerical Analysis" by Gerald
|
|
var brackets = Brackets.Find(flows);
|
|
if (Math.Abs(brackets.First - brackets.Second) < double.Epsilon)
|
|
throw new ArgumentException("Could not calculate: bracket failed");
|
|
|
|
double f3;
|
|
double result;
|
|
var x1 = brackets.First;
|
|
var x2 = brackets.Second;
|
|
|
|
var i = 0;
|
|
do
|
|
{
|
|
var f1 = new XNPV(flows, x1).GetResult();
|
|
var f2 = new XNPV(flows, x2).GetResult();
|
|
|
|
if (Math.Abs(f1) < double.Epsilon && Math.Abs(f2) < double.Epsilon)
|
|
throw new InvalidOperationException("Could not calculate: No solution found");
|
|
|
|
if (f1 * f2 > 0)
|
|
throw new ArgumentException("Could not calculate: bracket failed for x1, x2");
|
|
|
|
result = (x1 + x2) / 2;
|
|
f3 = new XNPV(flows, result).GetResult();
|
|
|
|
if (f3 * f1 < 0)
|
|
x2 = result;
|
|
else
|
|
x1 = result;
|
|
} while (Math.Abs(x1 - x2) / 2 > tolerance && Math.Abs(f3) > double.Epsilon && ++i < maxIterations);
|
|
|
|
if (i == maxIterations)
|
|
throw new InvalidOperationException("Could not calculate: No solution found");
|
|
|
|
return result;
|
|
}
|
|
|
|
private Func<IEnumerable<Flow>, double> NewthonsMethod =
|
|
cf => NewtonsMethodImplementation(cf);
|
|
|
|
private Func<IEnumerable<Flow>, double> BisectionMethod =
|
|
cf => BisectionMethodImplementation(cf);
|
|
|
|
private double CalcXirr(IEnumerable<Flow> flows, Func<IEnumerable<Flow>, double> method)
|
|
{
|
|
if (flows.Count(cf => cf.Value > 0) == 0)
|
|
throw new ArgumentException("Add at least one positive item");
|
|
|
|
if (flows.Count(c => c.Value < 0) == 0)
|
|
throw new ArgumentException("Add at least one negative item");
|
|
|
|
var result = method(flows);
|
|
|
|
if (double.IsInfinity(result))
|
|
throw new InvalidOperationException("Could not calculate: Infinity");
|
|
|
|
if (double.IsNaN(result))
|
|
throw new InvalidOperationException("Could not calculate: Not a number");
|
|
|
|
return result;
|
|
}
|
|
|
|
public double GetResult()
|
|
{
|
|
try
|
|
{
|
|
try
|
|
{
|
|
return CalcXirr(_flows, NewthonsMethod);
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Failed: try another algorithm
|
|
return CalcXirr(_flows, BisectionMethod);
|
|
}
|
|
}
|
|
catch (ArgumentException e)
|
|
{
|
|
Console.WriteLine(e.Message);
|
|
}
|
|
catch (InvalidOperationException exception)
|
|
{
|
|
Console.WriteLine(exception.Message);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
} |