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="EvoCalculator.Core.Models" t="IncludeRecursive">
<e p="bin" t="ExcludeRecursive" /> <e p="bin" t="ExcludeRecursive" />
<e p="Calculation" t="Include"> <e p="Calculation" t="Include">
<e p="Interfaces" t="Include"> <e p="Interfaces" t="Include" />
<e p="IFinanceFormula.cs" t="Include" />
</e>
<e p="Models" t="Include"> <e p="Models" t="Include">
<e p="AdditionalData.cs" t="Include" /> <e p="AdditionalData.cs" t="Include" />
<e p="Flow.cs" t="Include" /> <e p="Flow.cs" t="Include" />

View File

@ -55,11 +55,11 @@ namespace EvoCalculator.Core.Calculation.Columns
new GoalSeekOptions( new GoalSeekOptions(
startingStabPoint: Convert.ToDecimal( startingStabPoint: Convert.ToDecimal(
(_postValues.BaseCost.Value - _preparedValues.FirstPaymentSum) / (_postValues.BaseCost.Value - _preparedValues.FirstPaymentSum) /
_preparedValues.Nmper) _preparedValues.Nmper - 2)
, tineExplorePercentage: 10 // , tineExplorePercentage: 10
// , maximumAttempts: 10000 // , maximumAttempts: 100000
// , initialTineSpacing: 1 // initialTineSpacing: 1
// , focusPercentage: 100 // , focusPercentage: 50
// , trimFinalInputValue: true // , trimFinalInputValue: true
)); ));
} }

View File

@ -1,14 +1,19 @@
using System; using System;
using EvoCalculator.Core.Models.Calculation.Interfaces; using System.Collections.Generic;
using System.Linq;
using EvoCalculator.Core.Models.Calculation.Models; using EvoCalculator.Core.Models.Calculation.Models;
namespace EvoCalculator.Core.FinanceFormulas namespace EvoCalculator.Core.FinanceFormulas
{ {
public class XIRR : IFinanceFormula<double> public class XIRR
{ {
private readonly Flow[] _flows; private readonly Flow[] _flows;
private readonly double _guess = 0.1; 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) public XIRR(Flow[] flows)
{ {
_flows = flows; _flows = flows;
@ -20,51 +25,154 @@ namespace EvoCalculator.Core.FinanceFormulas
_guess = guess; _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() public double GetResult()
{ {
var x1 = 0.0; try
var x2 = _guess;
var f1 = new XNPV(_flows, x1).GetResult();
var f2 = new XNPV(_flows, x2).GetResult();
for (var i = 0; i < 100; i++)
{ {
if (f1 * f2 < 0.0) break; try
if (Math.Abs(f1) < Math.Abs(f2))
{ {
x1 += 1.6 * (x1 - x2); return CalcXirr(_flows, NewthonsMethod);
f1 = new XNPV(_flows, x1).GetResult();
} }
else catch (InvalidOperationException)
{ {
x2 += 1.6 * (x2 - x1); // Failed: try another algorithm
f2 = new XNPV(_flows, x2).GetResult(); return CalcXirr(_flows, BisectionMethod);
} }
} }
catch (ArgumentException e)
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)
{ {
rtb = x1; Console.WriteLine(e.Message);
dx = x2 - x1;
} }
else catch (InvalidOperationException exception)
{ {
rtb = x2; Console.WriteLine(exception.Message);
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;
} }
return 0; return 0;

View File

@ -1,16 +1,16 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using EvoCalculator.Core.Models.Calculation.Interfaces;
using EvoCalculator.Core.Models.Calculation.Models; using EvoCalculator.Core.Models.Calculation.Models;
namespace EvoCalculator.Core.FinanceFormulas namespace EvoCalculator.Core.FinanceFormulas
{ {
public class XNPV : IFinanceFormula<double> public class XNPV
{ {
private readonly Flow[] _flows; private readonly IEnumerable<Flow> _flows;
private readonly double _rate; private double _rate;
public XNPV(Flow[] flows, double rate) public XNPV(IEnumerable<Flow> flows, double rate)
{ {
_flows = flows; _flows = flows;
_rate = rate; _rate = rate;
@ -18,9 +18,22 @@ namespace EvoCalculator.Core.FinanceFormulas
public double GetResult() public double GetResult()
{ {
var firstDate = _flows[0].Date; if (_rate <= -1)
return _flows.Sum(flow => _rate = -1 + 1E-10; // Very funky ... Better check what an IRR <= -100% means
Convert.ToDouble(flow.Value) / Math.Pow(1 + _rate, (flow.Date - firstDate).TotalDays / 365));
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> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Calculation\Interfaces" />
<Folder Include="Calculation\Models\Response" /> <Folder Include="Calculation\Models\Response" />
</ItemGroup> </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"> <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; <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;Solution /&gt;&#xD;
&lt;/SessionState&gt;</s:String> &lt;/SessionState&gt;</s:String>

View File

@ -23,7 +23,7 @@ namespace EvoCalculator.Core.Controllers.V1
} }
[HttpPost("[action]")] [HttpPost("[action]")]
public async Task Calculate([FromBody] RequestCalculation requestCalculation) public IActionResult Calculate([FromBody] RequestCalculation requestCalculation)
{ {
var preparedValues = requestCalculation.preparedValues; var preparedValues = requestCalculation.preparedValues;
var preparedPayments = requestCalculation.preparedPayments; var preparedPayments = requestCalculation.preparedPayments;
@ -33,10 +33,8 @@ namespace EvoCalculator.Core.Controllers.V1
var validationErrors = new Validation().ValidatePreparedData(requestCalculation); var validationErrors = new Validation().ValidatePreparedData(requestCalculation);
if (validationErrors != null) if (validationErrors != null)
{ {
Response.StatusCode = 500; return StatusCode(500, JsonConvert.SerializeObject(validationErrors,
await Response.WriteAsync(JsonConvert.SerializeObject(validationErrors,
new JsonSerializerSettings {Formatting = Formatting.Indented})); new JsonSerializerSettings {Formatting = Formatting.Indented}));
return;
} }
var constants = new Constants.Calculation(); 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 new JsonSerializerSettings
{ {
Formatting = Formatting.Indented, Formatting = Formatting.Indented,
@ -333,8 +330,7 @@ namespace EvoCalculator.Core.Controllers.V1
} }
catch (Exception ex) catch (Exception ex)
{ {
Response.StatusCode = 500; return StatusCode(500, JsonConvert.SerializeObject(new
await Response.WriteAsync(JsonConvert.SerializeObject(new
{ {
errors = new[] errors = new[]
{ {

View File

@ -20,8 +20,11 @@ namespace EvoCalculator.Core
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddControllers(); services.AddControllers();
services.AddApiVersioning(opt => { services.AddResponseCompression();
services.AddApiVersioning(opt =>
{
opt.DefaultApiVersion = new ApiVersion(1, 0); opt.DefaultApiVersion = new ApiVersion(1, 0);
opt.AssumeDefaultVersionWhenUnspecified = true; opt.AssumeDefaultVersionWhenUnspecified = true;
opt.ReportApiVersions = true; opt.ReportApiVersions = true;
@ -36,6 +39,8 @@ namespace EvoCalculator.Core
app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage();
} }
app.UseResponseCompression();
// app.UseHttpsRedirection(); // app.UseHttpsRedirection();
app.UseRouting(); app.UseRouting();