rewrite xirr, xnpv formulas

This commit is contained in:
Chika 2020-11-30 18:22:33 +03:00
parent 8e655816d2
commit 066f75484d
9 changed files with 184 additions and 69 deletions

View File

@ -152,9 +152,7 @@
<e p="EvoCalculator.Core.Models" t="IncludeRecursive">
<e p="bin" t="ExcludeRecursive" />
<e p="Calculation" t="Include">
<e p="Interfaces" t="Include">
<e p="IFinanceFormula.cs" t="Include" />
</e>
<e p="Interfaces" t="Include" />
<e p="Models" t="Include">
<e p="AdditionalData.cs" t="Include" />
<e p="Flow.cs" t="Include" />

View File

@ -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
));
}

View File

@ -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<double>
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<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()
{
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;

View File

@ -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<double>
public class XNPV
{
private readonly Flow[] _flows;
private readonly double _rate;
private readonly IEnumerable<Flow> _flows;
private double _rate;
public XNPV(Flow[] flows, double rate)
public XNPV(IEnumerable<Flow> 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();
}
}
}

View File

@ -1,7 +0,0 @@
namespace EvoCalculator.Core.Models.Calculation.Interfaces
{
public interface IFinanceFormula<out T>
{
public T GetResult();
}
}

View File

@ -5,6 +5,7 @@
</PropertyGroup>
<ItemGroup>
<Folder Include="Calculation\Interfaces" />
<Folder Include="Calculation\Models\Response" />
</ItemGroup>

View File

@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=90125dce_002D8b20_002D4c11_002D807a_002Da58620eb7b69/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;Solution /&gt;&#xD;
&lt;/SessionState&gt;</s:String>

View File

@ -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[]
{

View File

@ -21,7 +21,10 @@ namespace EvoCalculator.Core
{
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();