mirror of
https://github.com/Dolibarr/dolibarr.git
synced 2026-01-12 20:12:43 +01:00
781 lines
28 KiB
PHP
781 lines
28 KiB
PHP
<?php
|
|
|
|
namespace MathPHP;
|
|
|
|
use MathPHP\Exception\OutOfBoundsException;
|
|
|
|
/**
|
|
* General references on financial functions and formulas:
|
|
* - Open Document Format for Office Applications (OpenDocument) Version 1.2 Part 2:
|
|
* Recalculated Formula (OpenFormula) Format. 29 September 2011. OASIS Standard.
|
|
* http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part2.html#__RefHeading__1018228_715980110
|
|
* - https://wiki.openoffice.org/wiki/Documentation/How_Tos/Calc:_Derivation_of_Financial_Formulas#Loans_and_Annuities
|
|
*/
|
|
class Finance
|
|
{
|
|
/**
|
|
* Floating-point range near zero to consider insignificant.
|
|
*/
|
|
public const EPSILON = 1e-6;
|
|
|
|
/**
|
|
* Consider any floating-point value less than epsilon from zero as zero,
|
|
* ie any value in the range [-epsilon < 0 < epsilon] is considered zero.
|
|
* Also used to convert -0.0 to 0.0.
|
|
*
|
|
* @param float $value
|
|
* @param float $epsilon
|
|
*
|
|
* @return float
|
|
*/
|
|
private static function checkZero(float $value, float $epsilon = self::EPSILON): float
|
|
{
|
|
return \abs($value) < $epsilon ? 0.0 : $value;
|
|
}
|
|
|
|
/**
|
|
* Financial payment for a loan or annuity with compound interest.
|
|
* Determines the periodic payment amount for a given interest rate,
|
|
* principal, targeted payment goal, life of the annuity as number
|
|
* of payments, and whether the payments are made at the start or end
|
|
* of each payment period.
|
|
*
|
|
* Same as the =PMT() function in most spreadsheet software.
|
|
*
|
|
* The basic monthly payment formula derivation:
|
|
* https://en.wikipedia.org/wiki/Mortgage_calculator#Monthly_payment_formula
|
|
*
|
|
* rP(1+r)ᴺ
|
|
* PMT = --------
|
|
* (1+r)ᴺ-1
|
|
*
|
|
* The formula is adjusted to allow targeting any future value rather than 0.
|
|
* The 1/(1+r*when) factor adjusts the payment to the beginning or end
|
|
* of the period. In the common case of a payment at the end of a period,
|
|
* the factor is 1 and reduces to the formula above. Setting when=1 computes
|
|
* an "annuity due" with an immediate payment.
|
|
*
|
|
* Examples:
|
|
* The payment on a 30-year fixed mortgage note of $265000 at 3.5% interest
|
|
* paid at the end of every month.
|
|
* pmt(0.035/12, 30*12, 265000, 0, false)
|
|
*
|
|
* The payment on a 30-year fixed mortgage note of $265000 at 3.5% interest
|
|
* needed to half the principal in half in 5 years:
|
|
* pmt(0.035/12, 5*12, 265000, 265000/2, false)
|
|
*
|
|
* The weekly payment into a savings account with 1% interest rate and current
|
|
* balance of $1500 needed to reach $10000 after 3 years:
|
|
* pmt(0.01/52, 3*52, -1500, 10000, false)
|
|
* The present_value is negative indicating money put into the savings account,
|
|
* whereas future_value is positive, indicating money that will be withdrawn from
|
|
* the account. Similarly, the payment value is negative
|
|
*
|
|
* How much money can be withdrawn at the end of every quarter from an account
|
|
* with $1000000 earning 4% so the money lasts 20 years:
|
|
* pmt(0.04/4, 20*4, 1000000, 0, false)
|
|
*
|
|
* @param float $rate
|
|
* @param int $periods
|
|
* @param float $present_value
|
|
* @param float $future_value
|
|
* @param bool $beginning adjust the payment to the beginning or end of the period
|
|
*
|
|
* @return float
|
|
*/
|
|
public static function pmt(float $rate, int $periods, float $present_value, float $future_value = 0.0, bool $beginning = false): float
|
|
{
|
|
$when = $beginning ? 1 : 0;
|
|
|
|
if ($rate == 0) {
|
|
return - ($future_value + $present_value) / $periods;
|
|
}
|
|
|
|
return - ($future_value + ($present_value * \pow(1 + $rate, $periods)))
|
|
/
|
|
((1 + $rate * $when) / $rate * (\pow(1 + $rate, $periods) - 1));
|
|
}
|
|
|
|
/**
|
|
* Interest on a financial payment for a loan or annuity with compound interest.
|
|
* Determines the interest payment at a particular period of the annuity. For
|
|
* a typical loan paid down to zero, the amount of interest and principle paid
|
|
* throughout the lifetime of the loan will change, with the interest portion
|
|
* of the payment decreasing over time as the loan principle decreases.
|
|
*
|
|
* Same as the =IPMT() function in most spreadsheet software.
|
|
*
|
|
* See the PMT function for derivation of the formula. For IPMT, we have
|
|
* the payment equal to the interest portion and principle portion of the payment:
|
|
*
|
|
* PMT = IPMT + PPMT
|
|
*
|
|
* The interest portion IPMT on a regular annuity can be calculated by computing
|
|
* the future value of the annuity for the prior period and computing the compound
|
|
* interest for one period:
|
|
*
|
|
* IPMT = FV(p=n-1) * rate
|
|
*
|
|
* For an "annuity due" where payment is at the start of the period, period=1 has
|
|
* no interest portion of the payment because no time has elapsed for compounding.
|
|
* To compute the interest portion of the payment, the future value of 2 periods
|
|
* back needs to be computed, as the definition of a period is different, giving:
|
|
*
|
|
* IPMT = (FV(p=n-2) - PMT) * rate
|
|
*
|
|
* By thinking of the future value at period 0 instead of the present value, the
|
|
* given formulas are computed.
|
|
*
|
|
* Example of regular annuity and annuity due for a loan of $10.00 paid back in 3 periods.
|
|
* Although the principle payments are equal, the total payment and interest portion are
|
|
* lower with the annuity due because a principle payment is made immediately.
|
|
*
|
|
* Regular Annuity | Annuity Due
|
|
* Period FV PMT IPMT PPMT | PMT IPMT PPMT
|
|
* 0 -10.00 |
|
|
* 1 -6.83 -3.67 -0.50 -3.17 | -3.50 0.00 -3.50
|
|
* 2 -3.50 -3.67 -0.34 -3.33 | -3.50 -0.33 -3.17
|
|
* 3 0.00 -3.67 -0.17 -3.50 | -3.50 -0.17 -3.33
|
|
* -----------------------|----------------------
|
|
* SUM -11.01 -1.01 -10.00 | -10.50 -0.50 -10.00
|
|
*
|
|
* Examples:
|
|
* The interest on a payment on a 30-year fixed mortgage note of $265000 at 3.5% interest
|
|
* paid at the end of every month, looking at the first payment:
|
|
* ipmt(0.035/12, 1, 30*12, 265000, 0, false)
|
|
*
|
|
* @param float $rate
|
|
* @param int $period
|
|
* @param int $periods
|
|
* @param float $present_value
|
|
* @param float $future_value
|
|
* @param bool $beginning adjust the payment to the beginning or end of the period
|
|
*
|
|
* @return float
|
|
*/
|
|
public static function ipmt(float $rate, int $period, int $periods, float $present_value, float $future_value = 0.0, bool $beginning = false): float
|
|
{
|
|
if ($period < 1 || $period > $periods) {
|
|
return \NAN;
|
|
}
|
|
|
|
if ($rate == 0) {
|
|
return 0;
|
|
}
|
|
|
|
if ($beginning && $period == 1) {
|
|
return 0.0;
|
|
}
|
|
|
|
$payment = self::pmt($rate, $periods, $present_value, $future_value, $beginning);
|
|
if ($beginning) {
|
|
$interest = (self::fv($rate, $period - 2, $payment, $present_value, $beginning) - $payment) * $rate;
|
|
} else {
|
|
$interest = self::fv($rate, $period - 1, $payment, $present_value, $beginning) * $rate;
|
|
}
|
|
|
|
return self::checkZero($interest);
|
|
}
|
|
|
|
/**
|
|
* Principle on a financial payment for a loan or annuity with compound interest.
|
|
* Determines the principle payment at a particular period of the annuity. For
|
|
* a typical loan paid down to zero, the amount of interest and principle paid
|
|
* throughout the lifetime of the loan will change, with the principle portion
|
|
* of the payment increasing over time as the loan principle decreases.
|
|
*
|
|
* Same as the =PPMT() function in most spreadsheet software.
|
|
*
|
|
* See the PMT function for derivation of the formula.
|
|
* See the IPMT function for derivation and use of PMT, IPMT, and PPMT.
|
|
*
|
|
* With derivations for PMT and IPMT, we simply compute:
|
|
*
|
|
* PPMT = PMT - IPMT
|
|
*
|
|
* Examples:
|
|
* The principle on a payment on a 30-year fixed mortgage note of $265000 at 3.5% interest
|
|
* paid at the end of every month, looking at the first payment:
|
|
* ppmt(0.035/12, 1, 30*12, 265000, 0, false)
|
|
*
|
|
* @param float $rate
|
|
* @param int $period
|
|
* @param int $periods
|
|
* @param float $present_value
|
|
* @param float $future_value
|
|
* @param bool $beginning adjust the payment to the beginning or end of the period
|
|
*
|
|
* @return float
|
|
*/
|
|
public static function ppmt(float $rate, int $period, int $periods, float $present_value, float $future_value = 0.0, bool $beginning = false): float
|
|
{
|
|
$payment = self::pmt($rate, $periods, $present_value, $future_value, $beginning);
|
|
$ipmt = self::ipmt($rate, $period, $periods, $present_value, $future_value, $beginning);
|
|
|
|
return $payment - $ipmt;
|
|
}
|
|
|
|
/**
|
|
* Number of payment periods of an annuity.
|
|
* Solves for the number of periods in the annuity formula.
|
|
*
|
|
* Same as the =NPER() function in most spreadsheet software.
|
|
*
|
|
* Solving the basic annuity formula for number of periods:
|
|
* log(PMT - FV*r)
|
|
* ---------------
|
|
* log(PMT + PV*r)
|
|
* n = --------------------
|
|
* log(1 + r)
|
|
*
|
|
* The (1+r*when) factor adjusts the payment to the beginning or end
|
|
* of the period. In the common case of a payment at the end of a period,
|
|
* the factor is 1 and reduces to the formula above. Setting when=1 computes
|
|
* an "annuity due" with an immediate payment.
|
|
*
|
|
* Examples:
|
|
* The number of periods of a $475000 mortgage with interest rate 3.5% and monthly
|
|
* payment of $2132.96 paid in full:
|
|
* nper(0.035/12, -2132.96, 475000, 0)
|
|
*
|
|
* @param float $rate
|
|
* @param float $payment
|
|
* @param float $present_value
|
|
* @param float $future_value
|
|
* @param bool $beginning adjust the payment to the beginning or end of the period
|
|
*
|
|
* @return float
|
|
*/
|
|
public static function periods(float $rate, float $payment, float $present_value, float $future_value, bool $beginning = false): float
|
|
{
|
|
$when = $beginning ? 1 : 0;
|
|
|
|
if ($rate == 0) {
|
|
return - ($present_value + $future_value) / $payment;
|
|
}
|
|
|
|
$initial = $payment * (1.0 + $rate * $when);
|
|
return \log(($initial - $future_value * $rate) / ($initial + $present_value * $rate)) / \log(1.0 + $rate);
|
|
}
|
|
|
|
/**
|
|
* Annual Equivalent Rate (AER) of an annual percentage rate (APR).
|
|
* The effective yearly rate of an annual percentage rate when the
|
|
* annual percentage rate is compounded periodically within the year.
|
|
*
|
|
* Same as the =EFFECT() function in most spreadsheet software.
|
|
*
|
|
* The formula:
|
|
* https://en.wikipedia.org/wiki/Effective_interest_rate
|
|
*
|
|
* / i \ ᴺ
|
|
* AER = | 1 + - | - 1
|
|
* \ n /
|
|
*
|
|
* Examples:
|
|
* The AER of APR 3.5% interest compounded monthly.
|
|
* aer(0.035, 12)
|
|
*
|
|
* @param float $nominal
|
|
* @param int $periods
|
|
*
|
|
* @return float
|
|
*/
|
|
public static function aer(float $nominal, int $periods): float
|
|
{
|
|
if ($periods == 1) {
|
|
return $nominal;
|
|
}
|
|
|
|
return \pow(1 + ($nominal / $periods), $periods) - 1;
|
|
}
|
|
|
|
/**
|
|
* Annual Nominal Rate of an annual effective rate (AER).
|
|
* The nominal yearly rate of an annual effective rate when the
|
|
* annual effective rate is compounded periodically within the year.
|
|
*
|
|
* Same as the =NOMINAL() function in most spreadsheet software.
|
|
*
|
|
* See:
|
|
* https://en.wikipedia.org/wiki/Nominal_interest_rate
|
|
*
|
|
* / 1/N \
|
|
* NOMINAL = | (AER + 1) -1 | * N
|
|
* \ /
|
|
*
|
|
* Examples:
|
|
* The nominal rate of AER 3.557% interest compounded monthly.
|
|
* nominal(0.03557, 12)
|
|
*
|
|
* @param float $aer
|
|
* @param int $periods
|
|
*
|
|
* @return float
|
|
*/
|
|
public static function nominal(float $aer, int $periods): float
|
|
{
|
|
if ($periods == 1) {
|
|
return $aer;
|
|
}
|
|
|
|
return (\pow($aer + 1, 1 / $periods) - 1) * $periods;
|
|
}
|
|
|
|
/**
|
|
* Future value for a loan or annuity with compound interest.
|
|
*
|
|
* Same as the =FV() function in most spreadsheet software.
|
|
*
|
|
* The basic future-value formula derivation:
|
|
* https://en.wikipedia.org/wiki/Future_value
|
|
*
|
|
* PMT*((1+r)ᴺ - 1)
|
|
* FV = -PV*(1+r)ᴺ - ----------------
|
|
* r
|
|
*
|
|
* The (1+r*when) factor adjusts the payment to the beginning or end
|
|
* of the period. In the common case of a payment at the end of a period,
|
|
* the factor is 1 and reduces to the formula above. Setting when=1 computes
|
|
* an "annuity due" with an immediate payment.
|
|
*
|
|
* Examples:
|
|
* The future value in 5 years on a 30-year fixed mortgage note of $265000
|
|
* at 3.5% interest paid at the end of every month. This is how much loan
|
|
* principle would be outstanding:
|
|
* fv(0.035/12, 5*12, 1189.97, -265000, false)
|
|
*
|
|
* The present_value is negative indicating money borrowed for the mortgage,
|
|
* whereas payment is positive, indicating money that will be paid to the
|
|
* mortgage.
|
|
*
|
|
* @param float $rate
|
|
* @param int $periods
|
|
* @param float $payment
|
|
* @param float $present_value
|
|
* @param bool $beginning adjust the payment to the beginning or end of the period
|
|
*
|
|
* @return float
|
|
*/
|
|
public static function fv(float $rate, int $periods, float $payment, float $present_value, bool $beginning = false): float
|
|
{
|
|
$when = $beginning ? 1 : 0;
|
|
|
|
if ($rate == 0) {
|
|
$fv = -($present_value + ($payment * $periods));
|
|
return self::checkZero($fv);
|
|
}
|
|
|
|
$initial = 1 + ($rate * $when);
|
|
$compound = \pow(1 + $rate, $periods);
|
|
$fv = - (($present_value * $compound) + (($payment * $initial * ($compound - 1)) / $rate));
|
|
|
|
return self::checkZero($fv);
|
|
}
|
|
|
|
/**
|
|
* Present value for a loan or annuity with compound interest.
|
|
*
|
|
* Same as the =PV() function in most spreadsheet software.
|
|
*
|
|
* The basic present-value formula derivation:
|
|
* https://en.wikipedia.org/wiki/Present_value
|
|
*
|
|
* PMT*((1+r)ᴺ - 1)
|
|
* PV = -FV - ----------------
|
|
* r
|
|
* ---------------------
|
|
* (1 + r)ᴺ
|
|
*
|
|
* The (1+r*when) factor adjusts the payment to the beginning or end
|
|
* of the period. In the common case of a payment at the end of a period,
|
|
* the factor is 1 and reduces to the formula above. Setting when=1 computes
|
|
* an "annuity due" with an immediate payment.
|
|
*
|
|
* Examples:
|
|
* The present value of a bond's $1000 face value paid in 5 year's time
|
|
* with a constant discount rate of 3.5% compounded monthly:
|
|
* pv(0.035/12, 5*12, 0, -1000, false)
|
|
*
|
|
* The present value of a $1000 5-year bond that pays a fixed 7% ($70)
|
|
* coupon at the end of each year with a discount rate of 5%:
|
|
* pv(0.5, 5, -70, -1000, false)
|
|
*
|
|
* The payment and future_value is negative indicating money paid out.
|
|
*
|
|
* @param float $rate
|
|
* @param int $periods
|
|
* @param float $payment
|
|
* @param float $future_value
|
|
* @param bool $beginning adjust the payment to the beginning or end of the period
|
|
*
|
|
* @return float
|
|
*/
|
|
public static function pv(float $rate, int $periods, float $payment, float $future_value, bool $beginning = false): float
|
|
{
|
|
$when = $beginning ? 1 : 0;
|
|
|
|
if ($rate == 0) {
|
|
$pv = -$future_value - ($payment * $periods);
|
|
return self::checkZero($pv);
|
|
}
|
|
|
|
$initial = 1 + ($rate * $when);
|
|
$compound = \pow(1 + $rate, $periods);
|
|
$pv = (-$future_value - (($payment * $initial * ($compound - 1)) / $rate)) / $compound;
|
|
|
|
return self::checkZero($pv);
|
|
}
|
|
|
|
/**
|
|
* Net present value of cash flows. Cash flows are periodic starting
|
|
* from an initial time and with a uniform discount rate.
|
|
*
|
|
* Similar to the =NPV() function in most spreadsheet software, except
|
|
* the initial (usually negative) cash flow at time 0 is given as the
|
|
* first element of the array rather than subtracted. For example,
|
|
* spreadsheet: =NPV(0.01, 100, 200, 300, 400) - 1000
|
|
* is done as
|
|
* MathPHP::npv(0.01, [-1000, 100, 200, 300, 400])
|
|
*
|
|
* The basic net-present-value formula derivation:
|
|
* https://en.wikipedia.org/wiki/Net_present_value
|
|
*
|
|
* n Rt
|
|
* Σ --------
|
|
* t=0 (1 / r)ᵗ
|
|
*
|
|
* Examples:
|
|
* The net present value of 5 yearly cash flows after an initial $1000
|
|
* investment with a 3% discount rate:
|
|
* npv(0.03, [-1000, 100, 500, 300, 700, 700])
|
|
*
|
|
* @param float $rate
|
|
* @param array<float> $values
|
|
*
|
|
* @return float
|
|
*/
|
|
public static function npv(float $rate, array $values): float
|
|
{
|
|
$result = 0.0;
|
|
|
|
for ($i = 0; $i < \count($values); ++$i) {
|
|
$result += $values[$i] / (1 + $rate) ** $i;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Interest rate per period of an Annuity.
|
|
*
|
|
* Same as the =RATE() formula in most spreadsheet software.
|
|
*
|
|
* The basic rate formula derivation is to solve for the future value
|
|
* taking into account the present value:
|
|
* https://en.wikipedia.org/wiki/Future_value
|
|
*
|
|
* ((1+r)ᴺ - 1)
|
|
* FV + PV*(1+r)ᴺ + PMT * ------------ = 0
|
|
* r
|
|
* The (1+r*when) factor adjusts the payment to the beginning or end
|
|
* of the period. In the common case of a payment at the end of a period,
|
|
* the factor is 1 and reduces to the formula above. Setting when=1 computes
|
|
* an "annuity due" with an immediate payment.
|
|
*
|
|
* Not all solutions for the rate have real-value solutions or converge.
|
|
* In these cases, NAN is returned.
|
|
*
|
|
* @param float $periods
|
|
* @param float $payment
|
|
* @param float $present_value
|
|
* @param float $future_value
|
|
* @param bool $beginning
|
|
* @param float $initial_guess
|
|
*
|
|
* @return float
|
|
*/
|
|
public static function rate(float $periods, float $payment, float $present_value, float $future_value, bool $beginning = false, float $initial_guess = 0.1): float
|
|
{
|
|
$when = $beginning ? 1 : 0;
|
|
|
|
$func = function ($x, $periods, $payment, $present_value, $future_value, $when) {
|
|
return $future_value + $present_value * (1 + $x) ** $periods + $payment * (1 + $x * $when) / $x * ((1 + $x) ** $periods - 1);
|
|
};
|
|
|
|
return self::checkZero(NumericalAnalysis\RootFinding\NewtonsMethod::solve($func, [$initial_guess, $periods, $payment, $present_value, $future_value, $when], 0, self::EPSILON, 0));
|
|
}
|
|
|
|
/**
|
|
* Internal rate of return.
|
|
* Periodic rate of return that would provide a net-present value (NPV) of 0.
|
|
*
|
|
* Same as =IRR formula in most spreadsheet software.
|
|
*
|
|
* Reference:
|
|
* https://en.wikipedia.org/wiki/Internal_rate_of_return
|
|
*
|
|
* Examples:
|
|
* The rate of return of an initial investment of $100 with returns
|
|
* of $50, $40, and $30:
|
|
* irr([-100, 50, 40, 30])
|
|
*
|
|
* Solves for NPV=0 using Newton's Method.
|
|
* @param array<float> $values
|
|
* @param float $initial_guess
|
|
*
|
|
* @return float
|
|
*
|
|
* @throws OutOfBoundsException
|
|
*
|
|
* @todo: Use eigenvalues to find the roots of a characteristic polynomial.
|
|
* This will allow finding all solutions and eliminate the need of the initial_guess.
|
|
*/
|
|
public static function irr(array $values, float $initial_guess = 0.1): float
|
|
{
|
|
$func = function ($x, $values) {
|
|
return Finance::npv($x, $values);
|
|
};
|
|
|
|
if (\count($values) <= 1) {
|
|
return \NAN;
|
|
}
|
|
|
|
$root = NumericalAnalysis\RootFinding\NewtonsMethod::solve($func, [$initial_guess, $values], 0, self::EPSILON, 0);
|
|
if (!\is_nan($root)) {
|
|
return self::CheckZero($root);
|
|
}
|
|
return self::checkZero(self::alternateIrr($values));
|
|
}
|
|
|
|
/**
|
|
* Alternate IRR implementation.
|
|
*
|
|
* A more numerically stable implementation that converges to only one value.
|
|
*
|
|
* Based off of Better: https://github.com/better/irr
|
|
*
|
|
* @param array<float> $values
|
|
*
|
|
* @return float
|
|
*/
|
|
private static function alternateIrr(array $values): float
|
|
{
|
|
$rate = 0.0;
|
|
for ($iter = 0; $iter < 100; $iter++) {
|
|
$m = -1000;
|
|
for ($i = 0; $i < \count($values); $i++) {
|
|
$m = \max($m, -$rate * $i);
|
|
}
|
|
$f = [];
|
|
for ($i = 0; $i < \count($values); $i++) {
|
|
$f[$i] = \exp(-$rate * $i - $m);
|
|
}
|
|
$t = 0;
|
|
for ($i = 0; $i < \count($values); $i++) {
|
|
$t += $f[$i] * $values[$i];
|
|
}
|
|
if (\abs($t) < (self::EPSILON * \exp($m))) {
|
|
break;
|
|
}
|
|
$u = 0;
|
|
for ($i = 0; $i < \count($values); $i++) {
|
|
$u += $f[$i] * $i * $values[$i];
|
|
}
|
|
if ($u == 0) {
|
|
return \NAN;
|
|
}
|
|
$rate += $t / $u;
|
|
}
|
|
return \exp($rate) - 1;
|
|
}
|
|
|
|
/**
|
|
* Modified internal rate of return.
|
|
* Rate of return that discounts outflows (investments) at the financing rate,
|
|
* and reinvests inflows with an expected rate of return.
|
|
*
|
|
* Same as =MIRR formula in most spreadsheet software.
|
|
*
|
|
* The formula derivation:
|
|
* https://en.wikipedia.org/wiki/Modified_internal_rate_of_return
|
|
*
|
|
* _____________________________
|
|
* n/ FV(re-invested cash inflows)
|
|
* - / ---------------------------- - 1.0
|
|
* \/ PV(discounted cash outflows)
|
|
*
|
|
* Examples:
|
|
* The rate of return of an initial investment of $100 at 5% financing
|
|
* with returns of $50, $40, and $30 reinvested at 10%:
|
|
* mirr([-100, 50, 40, 30], 0.05, 0.10)
|
|
*
|
|
* @param array<float> $values
|
|
* @param float $finance_rate
|
|
* @param float $reinvestment_rate
|
|
*
|
|
* @return float
|
|
*/
|
|
public static function mirr(array $values, float $finance_rate, float $reinvestment_rate): float
|
|
{
|
|
$inflows = array();
|
|
$outflows = array();
|
|
|
|
for ($i = 0; $i < \count($values); $i++) {
|
|
if ($values[$i] >= 0) {
|
|
$inflows[] = $values[$i];
|
|
$outflows[] = 0;
|
|
} else {
|
|
$inflows[] = 0;
|
|
$outflows[] = $values[$i];
|
|
}
|
|
}
|
|
|
|
$nonzero = function ($x) {
|
|
return $x != 0;
|
|
};
|
|
|
|
if (\count(\array_filter($inflows, $nonzero)) == 0 || \count(\array_filter($outflows, $nonzero)) == 0) {
|
|
return \NAN;
|
|
}
|
|
|
|
$root = \count($values) - 1;
|
|
$pv_inflows = self::npv($reinvestment_rate, $inflows);
|
|
$fv_inflows = self::fv($reinvestment_rate, $root, 0, -$pv_inflows);
|
|
$pv_outflows = self::npv($finance_rate, $outflows);
|
|
|
|
return self::checkZero(\pow($fv_inflows / -$pv_outflows, 1 / $root) - 1);
|
|
}
|
|
|
|
/**
|
|
* Discounted Payback of an investment.
|
|
* The number of periods to recoup cash outlays of an investment.
|
|
*
|
|
* This is commonly used with discount rate=0 as simple payback period,
|
|
* but it is not a real financial measurement when it doesn't consider the
|
|
* discount rate. Even with a discount rate, it doesn't consider the cost
|
|
* of capital or re-investment of returns.
|
|
*
|
|
* Avoid this when possible. Consider NPV, MIRR, IRR, and other financial
|
|
* functions.
|
|
*
|
|
* Reference:
|
|
* https://en.wikipedia.org/wiki/Payback_period
|
|
*
|
|
* The result is given assuming cash flows are continous throughout a period.
|
|
* To compute payback in terms of whole periods, use ceil() on the result.
|
|
*
|
|
* An investment could reach its payback period before future cash outlays occur.
|
|
* The payback period returned is defined to be the final point at which the
|
|
* sum of returns becomes positive.
|
|
*
|
|
* Examples:
|
|
* The payback period of an investment with a $1,000 investment and future returns
|
|
* of $100, $200, $300, $400, $500:
|
|
* payback([-1000, 100, 200, 300, 400, 500])
|
|
*
|
|
* The discounted payback period of an investment with a $1,000 investment, future returns
|
|
* of $100, $200, $300, $400, $500, and a discount rate of 0.10:
|
|
* payback([-1000, 100, 200, 300, 400, 500], 0.1)
|
|
*
|
|
* @param array<float> $values
|
|
* @param float $rate
|
|
*
|
|
* @return float
|
|
*/
|
|
public static function payback(array $values, float $rate = 0.0): float
|
|
{
|
|
$last_outflow = -1;
|
|
for ($i = 0; $i < \count($values); $i++) {
|
|
if ($values[$i] < 0) {
|
|
$last_outflow = $i;
|
|
}
|
|
}
|
|
|
|
if ($last_outflow < 0) {
|
|
return 0.0;
|
|
}
|
|
|
|
$sum = $values[0];
|
|
$payback_period = -1;
|
|
|
|
for ($i = 1; $i < \count($values); $i++) {
|
|
$prevsum = $sum;
|
|
$discounted_flow = $values[$i] / (1 + $rate) ** $i;
|
|
$sum += $discounted_flow;
|
|
if ($sum >= 0) {
|
|
if ($i > $last_outflow) {
|
|
return ($i - 1) + (-$prevsum / $discounted_flow);
|
|
}
|
|
if ($payback_period == -1) {
|
|
$payback_period = ($i - 1) + (-$prevsum / $discounted_flow);
|
|
}
|
|
} else {
|
|
$payback_period = -1;
|
|
}
|
|
}
|
|
if ($sum >= 0) {
|
|
return $payback_period;
|
|
}
|
|
|
|
return \NAN;
|
|
}
|
|
|
|
/**
|
|
* Profitability Index.
|
|
* The Profitability Index, also referred to as Profit Investment
|
|
* Ratio (PIR) and Value Investment Ratio (VIR), is a comparison of
|
|
* discounted cash inflows to discounted cash outflows. It can be
|
|
* used as a decision criteria of an investment, using larger than 1
|
|
* to choose an investment, and less than 1 to pass.
|
|
*
|
|
* The formula derivation:
|
|
* https://en.wikipedia.org/wiki/Profitability_index
|
|
*
|
|
* PV(cash inflows)
|
|
* ----------------
|
|
* PV(cash outflows)
|
|
*
|
|
* The formula is usually stated in terms of the initial investmest,
|
|
* but it is generalized here to discount all future outflows.
|
|
*
|
|
* Examples:
|
|
* The profitability index of an initial $100 investment with future
|
|
* returns of $50, $50, $50 with a 10% discount rate:
|
|
* profitabilityIndex([-100, 50, 50, 50], 0.10)
|
|
*
|
|
* @param array<float> $values
|
|
* @param float $rate
|
|
*
|
|
* @return float
|
|
*/
|
|
public static function profitabilityIndex(array $values, float $rate): float
|
|
{
|
|
$inflows = array();
|
|
$outflows = array();
|
|
|
|
for ($i = 0; $i < \count($values); $i++) {
|
|
if ($values[$i] >= 0) {
|
|
$inflows[] = $values[$i];
|
|
$outflows[] = 0;
|
|
} else {
|
|
$inflows[] = 0;
|
|
$outflows[] = -$values[$i];
|
|
}
|
|
}
|
|
|
|
$nonzero = function ($x) {
|
|
return $x != 0;
|
|
};
|
|
|
|
if (\count(\array_filter($outflows, $nonzero)) == 0) {
|
|
return \NAN;
|
|
}
|
|
|
|
$pv_inflows = self::npv($rate, $inflows);
|
|
$pv_outflows = self::npv($rate, $outflows);
|
|
|
|
return $pv_inflows / $pv_outflows;
|
|
}
|
|
}
|