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="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" />
|
||||
|
||||
@ -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
|
||||
));
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
namespace EvoCalculator.Core.Models.Calculation.Interfaces
|
||||
{
|
||||
public interface IFinanceFormula<out T>
|
||||
{
|
||||
public T GetResult();
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Calculation\Interfaces" />
|
||||
<Folder Include="Calculation\Models\Response" />
|
||||
</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">
|
||||
|
||||
<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 />
|
||||
</SessionState></s:String>
|
||||
|
||||
@ -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[]
|
||||
{
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user