rewrite xirr, xnpv formulas
This commit is contained in:
parent
8e655816d2
commit
066f75484d
@ -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" />
|
||||||
|
|||||||
@ -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
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +0,0 @@
|
|||||||
namespace EvoCalculator.Core.Models.Calculation.Interfaces
|
|
||||||
{
|
|
||||||
public interface IFinanceFormula<out T>
|
|
||||||
{
|
|
||||||
public T GetResult();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,6 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="Calculation\Interfaces" />
|
||||||
<Folder Include="Calculation\Models\Response" />
|
<Folder Include="Calculation\Models\Response" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@ -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"><SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=90125dce_002D8b20_002D4c11_002D807a_002Da58620eb7b69/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||||
<Solution />
|
<Solution />
|
||||||
</SessionState></s:String>
|
</SessionState></s:String>
|
||||||
|
|||||||
@ -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[]
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user