# Bisection-based XIRR implementation in C#

Luca -

Here is a quick im­ple­men­ta­tion of XIRR (using Excel nomen­cla­ture) writ­ten in C#.

Disclaimer: this is a su­per sim­ple Bisection-based im­ple­men­ta­tion. People tend to pre­fer the Newton method, but this is sim­pler and works for the app I’m writ­ing. I de­cided to post it be­cause I could­n’t ﬁnd one on the net when I looked for it. I at­tached test­cases to show the ex­tent of my test­ing.

It is called CalculateXIRR and it is in­voked by pass­ing a list of cash ﬂows, a tol­er­ance and a max num­ber of it­er­a­tions.

``using System;using System.Linq;using Money = System.Decimal;using Rate = System.Double;using System.Collections.Generic;public struct Pair<T, Z> {public Pair(T first, Z second) { First = first; Second = second; }public readonly T First;public readonly Z Second;}public class CashFlow {public CashFlow(Money amount, DateTime date) { Amount = amount; Date = date; }public readonly Money Amount;public readonly DateTime Date;}public struct AlgorithmResult<TKindOfResult, TValue> {public AlgorithmResult(TKindOfResult kind, TValue value) {Kind = kind;Value = value;}public readonly TKindOfResult Kind;public readonly TValue Value;}public enum ApproximateResultKind {ApproximateSolution,ExactSolution,NoSolutionWithinTolerance}public static class Algorithms {internal static Money CalculateXNPV(IEnumerable<CashFlow> cfs, Rate r) {if (r <= -1)r= -0.99999999; // Very funky ... Better check what an IRR <= -100% meansreturn (from cf in cfslet startDate = cfs.OrderBy(cf1 => cf1.Date).First().Dateselect cf.Amount / (decimal) Math.Pow(1 + r, (cf.Date - startDate).Days / 365.0)).Sum();}internal static Pair<Rate, Rate> FindBrackets(Func<IEnumerable<CashFlow>, Rate, Money> func, IEnumerable<CashFlow> cfs) {// Abracadabra magic numbers ...const int maxIter = 100;const Rate bracketStep = 0.5;const Rate guess = 0.1;Rate leftBracket = guess - bracketStep;Rate rightBracket = guess + bracketStep;var iter = 0;while (func(cfs, leftBracket) * func(cfs, rightBracket) > 0 && iter++ < maxIter) {leftBracket -= bracketStep;rightBracket += bracketStep;}if (iter >= maxIter)return new Pair<double, double>(0, 0);return new Pair<Rate, Rate>(leftBracket, rightBracket);}// From "Applied Numerical Analyis" by Geraldinternal static AlgorithmResult<ApproximateResultKind, Rate> Bisection(Func<Rate, Money> func, Pair<Rate, Rate> brackets, Rate tol, int maxIters) {int iter = 1;Money f3 = 0;Rate x3 = 0;Rate x1 = brackets.First;Rate x2 = brackets.Second;do {var f1 = func(x1);var f2 = func(x2);if (f1 == 0 && f2 == 0)return new AlgorithmResult<ApproximateResultKind, Rate>(ApproximateResultKind.NoSolutionWithinTolerance, x1);if (f1 * f2 > 0)throw new ArgumentException("x1 x2 values don't bracket a root");x3 = (x1 + x2) / 2;f3 = func(x3);if (f3 * f1 < 0)x2 = x3;elsex1 = x3;iter++;} while (Math.Abs(x1 - x2)/2 > tol && f3 != 0 && iter < maxIters);if (f3 == 0)return new AlgorithmResult<ApproximateResultKind, Rate>(ApproximateResultKind.ExactSolution, x3);if (Math.Abs(x1 - x2) / 2 < tol)return new AlgorithmResult<ApproximateResultKind, Rate>(ApproximateResultKind.ApproximateSolution, x3);if (iter > maxIters)return new AlgorithmResult<ApproximateResultKind, Rate>(ApproximateResultKind.NoSolutionWithinTolerance, x3);throw new Exception("It should never get here");}public static AlgorithmResult<ApproximateResultKind, Rate> CalculateXIRR(IEnumerable<CashFlow> cfs, Rate tolerance, int maxIters) {var brackets = FindBrackets(CalculateXNPV, cfs);if (brackets.First == brackets.Second)return new AlgorithmResult<ApproximateResultKind, double>(ApproximateResultKind.NoSolutionWithinTolerance, brackets.First);return Bisection(r => CalculateXNPV(cfs,r), brackets, tolerance, maxIters);}}// TESTSusing Microsoft.VisualStudio.TestTools.UnitTesting;using System.Collections.Generic;using System;using Rate = System.Double;namespace TimeLineTest{[TestClass()]public class AlgorithmsTest {IEnumerable<CashFlow> cfs = new CashFlow[] {new CashFlow(-10000, new DateTime(2008,1,1)),new CashFlow(2750, new DateTime(2008,3,1)),new CashFlow(4250, new DateTime(2008,10,30)),new CashFlow(3250, new DateTime(2009,2,15)),new CashFlow(2750, new DateTime(2009,4,1))};IEnumerable<CashFlow> bigcfs = new CashFlow[] {new CashFlow(-10, new DateTime(2000,1,1)),new CashFlow(10, new DateTime(2002,1,2)),new CashFlow(20, new DateTime(2003,1,3))};IEnumerable<CashFlow> negcfs = new CashFlow[] {new CashFlow(-10, new DateTime(2000,1,1)),new CashFlow(-1, new DateTime(2002,1,2)),new CashFlow(1, new DateTime(2003,1,3))};IEnumerable<CashFlow> samedaysamecfs = new CashFlow[] {new CashFlow(-10, new DateTime(2000,1,1)),new CashFlow(10, new DateTime(2000,1,1)),};IEnumerable<CashFlow> samedaydifferentcfs = new CashFlow[] {new CashFlow(-10, new DateTime(2000,1,1)),new CashFlow(100, new DateTime(2000,1,1)),};IEnumerable<CashFlow> bigratecfs = new CashFlow[] {new CashFlow(-10, new DateTime(2000,1,1)),new CashFlow(20, new DateTime(2000,5,30)),};IEnumerable<CashFlow> zeroRate = new CashFlow[] {new CashFlow(-10, new DateTime(2000,1,1)),new CashFlow(10, new DateTime(2003,1,1)),};IEnumerable<CashFlow> doubleNegative = new CashFlow[] {new CashFlow(-10000, new DateTime(2008,1,1)),new CashFlow(2750, new DateTime(2008,3,1)),new CashFlow(-4250, new DateTime(2008,10,30)),new CashFlow(3250, new DateTime(2009,2,15)),new CashFlow(2750, new DateTime(2009,4,1))};IEnumerable<CashFlow> badDoubleNegative = new CashFlow[] {new CashFlow(-10000, new DateTime(2008,1,1)),new CashFlow(2750, new DateTime(2008,3,1)),new CashFlow(-4250, new DateTime(2008,10,30)),new CashFlow(3250, new DateTime(2009,2,15)),new CashFlow(-2750, new DateTime(2009,4,1))};double r = 0.09;double tolerance = 0.0001;int maxIters = 100;private TestContext testContextInstance;public TestContext TestContext {get {return testContextInstance;}set {testContextInstance = value;}}[TestMethod()]public void CalculateXNPV() {Assert.AreEqual(2086.6476020315416570634272814M, Algorithms.CalculateXNPV(cfs, r));Assert.AreEqual(-10.148147600710372651326920258M, Algorithms.CalculateXNPV(negcfs, 0.5));Assert.AreEqual(4.9923725815954514810351876895M, Algorithms.CalculateXNPV(bigcfs, 0.3));}[TestMethod]public void FindBrackets() {var brackets = Algorithms.FindBrackets(Algorithms.CalculateXNPV, cfs);Assert.IsTrue(brackets.First < 0.3733 && brackets.Second > 0.3733);brackets = Algorithms.FindBrackets(Algorithms.CalculateXNPV, bigcfs);Assert.IsTrue(brackets.First < 0.5196 && brackets.Second > 0.5196);brackets = Algorithms.FindBrackets(Algorithms.CalculateXNPV, negcfs);Assert.IsTrue(brackets.First < -0.6059 && brackets.Second > -0.6059);}[TestMethod]public void XIRRTest() {var irr = Algorithms.CalculateXIRR(cfs, tolerance, maxIters);Assert.AreEqual(0.3733, irr.Value, 0.001);Assert.AreEqual(ApproximateResultKind.ApproximateSolution, irr.Kind);irr = Algorithms.CalculateXIRR(bigcfs, tolerance, maxIters);Assert.AreEqual(0.5196, irr.Value, 0.001);Assert.AreEqual(ApproximateResultKind.ApproximateSolution, irr.Kind);irr = Algorithms.CalculateXIRR(negcfs, tolerance, maxIters);Assert.AreEqual(-0.6059, irr.Value, 0.001);Assert.AreEqual(ApproximateResultKind.ApproximateSolution, irr.Kind);irr = Algorithms.CalculateXIRR(samedaysamecfs, tolerance, maxIters);Assert.AreEqual(ApproximateResultKind.NoSolutionWithinTolerance, irr.Kind);irr = Algorithms.CalculateXIRR(samedaydifferentcfs, tolerance, maxIters);Assert.AreEqual(ApproximateResultKind.NoSolutionWithinTolerance, irr.Kind);irr = Algorithms.CalculateXIRR(bigratecfs, tolerance, maxIters);Assert.AreEqual(4.40140, irr.Value, 0.001);Assert.AreEqual(ApproximateResultKind.ApproximateSolution, irr.Kind);irr = Algorithms.CalculateXIRR(zeroRate, tolerance, maxIters);Assert.AreEqual(0, irr.Value, 0.001);Assert.AreEqual(ApproximateResultKind.ApproximateSolution, irr.Kind);irr = Algorithms.CalculateXIRR(doubleNegative, tolerance, maxIters);Assert.AreEqual(-0.537055, irr.Value, 0.001);Assert.AreEqual(ApproximateResultKind.ApproximateSolution, irr.Kind);irr = Algorithms.CalculateXIRR(badDoubleNegative, tolerance, maxIters);Assert.AreEqual(ApproximateResultKind.NoSolutionWithinTolerance, irr.Kind);}}}``
1 Comment