123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780 |
- <?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;
- }
- }
|