$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 $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 $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 $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 $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 $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 $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; } }