From 066f75484d4d0449d71d2b7d081f96e83b548ff1 Mon Sep 17 00:00:00 2001 From: Chika Date: Mon, 30 Nov 2020 18:22:33 +0300 Subject: [PATCH] rewrite xirr, xnpv formulas --- .../.idea/contentModel.xml | 4 +- .../Columns/SumColumn.cs | 10 +- EvoCalculator.Core.FinanceFormulas/XIRR.cs | 180 ++++++++++++++---- EvoCalculator.Core.FinanceFormulas/XNPV.cs | 29 ++- .../Calculation/Interfaces/IFinanceFormula.cs | 7 - .../EvoCalculator.Core.Models.csproj | 1 + EvoCalculator.Core.sln.DotSettings.user | 1 + .../Controllers/v1/CalculationController.cs | 12 +- EvoCalculator.Core/Startup.cs | 9 +- 9 files changed, 184 insertions(+), 69 deletions(-) delete mode 100644 EvoCalculator.Core.Models/Calculation/Interfaces/IFinanceFormula.cs diff --git a/.idea/.idea.EvoCalculator.Core/.idea/contentModel.xml b/.idea/.idea.EvoCalculator.Core/.idea/contentModel.xml index 2176b7d..bba0dea 100644 --- a/.idea/.idea.EvoCalculator.Core/.idea/contentModel.xml +++ b/.idea/.idea.EvoCalculator.Core/.idea/contentModel.xml @@ -152,9 +152,7 @@ - - - + diff --git a/EvoCalculator.Core.Calculation/Columns/SumColumn.cs b/EvoCalculator.Core.Calculation/Columns/SumColumn.cs index 613e6ce..15ac3e3 100644 --- a/EvoCalculator.Core.Calculation/Columns/SumColumn.cs +++ b/EvoCalculator.Core.Calculation/Columns/SumColumn.cs @@ -55,11 +55,11 @@ namespace EvoCalculator.Core.Calculation.Columns new GoalSeekOptions( startingStabPoint: Convert.ToDecimal( (_postValues.BaseCost.Value - _preparedValues.FirstPaymentSum) / - _preparedValues.Nmper) - , tineExplorePercentage: 10 - // , maximumAttempts: 10000 - // , initialTineSpacing: 1 - // , focusPercentage: 100 + _preparedValues.Nmper - 2) + // , tineExplorePercentage: 10 + // , maximumAttempts: 100000 + // initialTineSpacing: 1 + // , focusPercentage: 50 // , trimFinalInputValue: true )); } diff --git a/EvoCalculator.Core.FinanceFormulas/XIRR.cs b/EvoCalculator.Core.FinanceFormulas/XIRR.cs index 9a90a3f..644d1f5 100644 --- a/EvoCalculator.Core.FinanceFormulas/XIRR.cs +++ b/EvoCalculator.Core.FinanceFormulas/XIRR.cs @@ -1,14 +1,19 @@ using System; -using EvoCalculator.Core.Models.Calculation.Interfaces; +using System.Collections.Generic; +using System.Linq; using EvoCalculator.Core.Models.Calculation.Models; namespace EvoCalculator.Core.FinanceFormulas { - public class XIRR : IFinanceFormula + 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; @@ -20,51 +25,154 @@ namespace EvoCalculator.Core.FinanceFormulas _guess = guess; } + + private static double NewtonsMethodImplementation(IEnumerable 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 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 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, double> NewthonsMethod = + cf => NewtonsMethodImplementation(cf); + + private Func, double> BisectionMethod = + cf => BisectionMethodImplementation(cf); + + private double CalcXirr(IEnumerable flows, Func, 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() { - var x1 = 0.0; - var x2 = _guess; - var f1 = new XNPV(_flows, x1).GetResult(); - var f2 = new XNPV(_flows, x2).GetResult(); - - for (var i = 0; i < 100; i++) + try { - if (f1 * f2 < 0.0) break; - if (Math.Abs(f1) < Math.Abs(f2)) + try { - x1 += 1.6 * (x1 - x2); - f1 = new XNPV(_flows, x1).GetResult(); + return CalcXirr(_flows, NewthonsMethod); } - else + catch (InvalidOperationException) { - x2 += 1.6 * (x2 - x1); - f2 = new XNPV(_flows, x2).GetResult(); + // Failed: try another algorithm + return CalcXirr(_flows, BisectionMethod); } } - - if (f1 * f2 > 0.0) return 0; - - var f = new XNPV(_flows, x1).GetResult(); - var dx = 0.0; - var rtb = 0.0; - if (f < 0.0) + catch (ArgumentException e) { - rtb = x1; - dx = x2 - x1; + Console.WriteLine(e.Message); } - else + catch (InvalidOperationException exception) { - rtb = x2; - dx = x1 - x2; - } - - for (var i = 0; i < 100; i++) - { - dx *= 0.5; - var xMid = rtb + dx; - var fMid = new XNPV(_flows, xMid).GetResult(); - if (fMid <= 0.0) rtb = xMid; - if (Math.Abs(fMid) < 1.0e-6 || Math.Abs(dx) < 1.0e-6) return xMid; + Console.WriteLine(exception.Message); } return 0; diff --git a/EvoCalculator.Core.FinanceFormulas/XNPV.cs b/EvoCalculator.Core.FinanceFormulas/XNPV.cs index d73e749..2916c22 100644 --- a/EvoCalculator.Core.FinanceFormulas/XNPV.cs +++ b/EvoCalculator.Core.FinanceFormulas/XNPV.cs @@ -1,16 +1,16 @@ using System; +using System.Collections.Generic; using System.Linq; -using EvoCalculator.Core.Models.Calculation.Interfaces; using EvoCalculator.Core.Models.Calculation.Models; namespace EvoCalculator.Core.FinanceFormulas { - public class XNPV : IFinanceFormula + public class XNPV { - private readonly Flow[] _flows; - private readonly double _rate; + private readonly IEnumerable _flows; + private double _rate; - public XNPV(Flow[] flows, double rate) + public XNPV(IEnumerable flows, double rate) { _flows = flows; _rate = rate; @@ -18,9 +18,22 @@ namespace EvoCalculator.Core.FinanceFormulas public double GetResult() { - var firstDate = _flows[0].Date; - return _flows.Sum(flow => - Convert.ToDouble(flow.Value) / Math.Pow(1 + _rate, (flow.Date - firstDate).TotalDays / 365)); + if (_rate <= -1) + _rate = -1 + 1E-10; // Very funky ... Better check what an IRR <= -100% means + + var startDate = _flows.OrderBy(i => i.Date).First().Date; + return + (from item in _flows + let days = -(item.Date - startDate).Days + select (double) item.Value * Math.Pow(1 + _rate, (double) days / 365)).Sum(); + } + + public double GetResultPrime() + { + var startDate = _flows.OrderBy(i => i.Date).First().Date; + return (from item in _flows + let daysRatio = -(item.Date - startDate).Days / 365 + select (double) item.Value * daysRatio * Math.Pow(1.0 + _rate, daysRatio - 1)).Sum(); } } } \ No newline at end of file diff --git a/EvoCalculator.Core.Models/Calculation/Interfaces/IFinanceFormula.cs b/EvoCalculator.Core.Models/Calculation/Interfaces/IFinanceFormula.cs deleted file mode 100644 index b4163b6..0000000 --- a/EvoCalculator.Core.Models/Calculation/Interfaces/IFinanceFormula.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace EvoCalculator.Core.Models.Calculation.Interfaces -{ - public interface IFinanceFormula - { - public T GetResult(); - } -} \ No newline at end of file diff --git a/EvoCalculator.Core.Models/EvoCalculator.Core.Models.csproj b/EvoCalculator.Core.Models/EvoCalculator.Core.Models.csproj index b57da84..ae1e96f 100644 --- a/EvoCalculator.Core.Models/EvoCalculator.Core.Models.csproj +++ b/EvoCalculator.Core.Models/EvoCalculator.Core.Models.csproj @@ -5,6 +5,7 @@ + diff --git a/EvoCalculator.Core.sln.DotSettings.user b/EvoCalculator.Core.sln.DotSettings.user index 9237d20..fb78306 100644 --- a/EvoCalculator.Core.sln.DotSettings.user +++ b/EvoCalculator.Core.sln.DotSettings.user @@ -1,4 +1,5 @@  + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Solution /> </SessionState> diff --git a/EvoCalculator.Core/Controllers/v1/CalculationController.cs b/EvoCalculator.Core/Controllers/v1/CalculationController.cs index 8beacaa..c57d8cc 100644 --- a/EvoCalculator.Core/Controllers/v1/CalculationController.cs +++ b/EvoCalculator.Core/Controllers/v1/CalculationController.cs @@ -23,7 +23,7 @@ namespace EvoCalculator.Core.Controllers.V1 } [HttpPost("[action]")] - public async Task Calculate([FromBody] RequestCalculation requestCalculation) + public IActionResult Calculate([FromBody] RequestCalculation requestCalculation) { var preparedValues = requestCalculation.preparedValues; var preparedPayments = requestCalculation.preparedPayments; @@ -33,10 +33,8 @@ namespace EvoCalculator.Core.Controllers.V1 var validationErrors = new Validation().ValidatePreparedData(requestCalculation); if (validationErrors != null) { - Response.StatusCode = 500; - await Response.WriteAsync(JsonConvert.SerializeObject(validationErrors, + return StatusCode(500, JsonConvert.SerializeObject(validationErrors, new JsonSerializerSettings {Formatting = Formatting.Indented})); - return; } var constants = new Constants.Calculation(); @@ -322,9 +320,8 @@ namespace EvoCalculator.Core.Controllers.V1 } }; - Response.ContentType = "application/json"; - await Response.WriteAsync(JsonConvert.SerializeObject(res, + return Ok(JsonConvert.SerializeObject(res, new JsonSerializerSettings { Formatting = Formatting.Indented, @@ -333,8 +330,7 @@ namespace EvoCalculator.Core.Controllers.V1 } catch (Exception ex) { - Response.StatusCode = 500; - await Response.WriteAsync(JsonConvert.SerializeObject(new + return StatusCode(500, JsonConvert.SerializeObject(new { errors = new[] { diff --git a/EvoCalculator.Core/Startup.cs b/EvoCalculator.Core/Startup.cs index c8a00c2..35df9af 100644 --- a/EvoCalculator.Core/Startup.cs +++ b/EvoCalculator.Core/Startup.cs @@ -20,8 +20,11 @@ namespace EvoCalculator.Core public void ConfigureServices(IServiceCollection services) { services.AddControllers(); - - services.AddApiVersioning(opt => { + + services.AddResponseCompression(); + + services.AddApiVersioning(opt => + { opt.DefaultApiVersion = new ApiVersion(1, 0); opt.AssumeDefaultVersionWhenUnspecified = true; opt.ReportApiVersions = true; @@ -36,6 +39,8 @@ namespace EvoCalculator.Core app.UseDeveloperExceptionPage(); } + app.UseResponseCompression(); + // app.UseHttpsRedirection(); app.UseRouting();