| 1 | <?php |
||
| 2 | |||
| 3 | namespace Common\Tool; |
||
| 4 | |||
| 5 | class Financial |
||
| 6 | { |
||
| 7 | private const FINANCIAL_ACCURACY = 1.0e-6; |
||
| 8 | private const FINANCIAL_MAX_ITERATIONS = 100; |
||
| 9 | |||
| 10 | private static $isInitialized = false; |
||
| 11 | |||
| 12 | private static function init() |
||
| 13 | { |
||
| 14 | if (!self::$isInitialized) { |
||
| 15 | // forces the precision for calculations |
||
| 16 | ini_set('precision', '14'); |
||
| 17 | |||
| 18 | self::$isInitialized = true; |
||
| 19 | } |
||
| 20 | } |
||
| 21 | |||
| 22 | |||
| 23 | /** |
||
| 24 | * Present value interest factor |
||
| 25 | * |
||
| 26 | * nper |
||
| 27 | * PVIF = (1 + rate) |
||
| 28 | * |
||
| 29 | * @param float $rate is the interest rate per period. |
||
| 30 | * @param integer $nper is the total number of periods. |
||
| 31 | * |
||
| 32 | * @return float the present value interest factor |
||
| 33 | */ |
||
| 34 | private static function PVIF($rate, $nper) |
||
| 35 | { |
||
| 36 | self::init(); |
||
| 37 | |||
| 38 | return ((1 + $rate) ** $nper); |
||
| 39 | } |
||
| 40 | |||
| 41 | /** |
||
| 42 | * Future value interest factor of annuities |
||
| 43 | * |
||
| 44 | * nper |
||
| 45 | * (1 + rate) - 1 |
||
| 46 | * FVIFA = ------------------- |
||
| 47 | * rate |
||
| 48 | * |
||
| 49 | * @param float $rate is the interest rate per period. |
||
| 50 | * @param integer $nper is the total number of periods. |
||
| 51 | * |
||
| 52 | * @return float the present value interest factor of annuities |
||
| 53 | */ |
||
| 54 | private static function FVIFA($rate, $nper) |
||
| 55 | { |
||
| 56 | self::init(); |
||
| 57 | |||
| 58 | // Removable singularity at rate == 0 |
||
| 59 | if ($rate == 0) { |
||
| 60 | return $nper; |
||
| 61 | } |
||
| 62 | |||
| 63 | return (((1 + $rate) ** $nper) - 1) / $rate; |
||
| 64 | } |
||
| 65 | |||
| 66 | /** |
||
| 67 | * @param $pv |
||
| 68 | * @param $pmt |
||
| 69 | * @param $rate |
||
| 70 | * @param $period |
||
| 71 | * |
||
| 72 | * @return mixed |
||
| 73 | */ |
||
| 74 | private static function interestPart($pv, $pmt, $rate, $period) |
||
| 75 | { |
||
| 76 | self::init(); |
||
| 77 | |||
| 78 | return -($pv * ((1 + $rate) ** $period) * $rate + $pmt * (((1 + $rate) ** $period) - 1)); |
||
| 79 | } |
||
| 80 | |||
| 81 | public static function PMT($rate, $nper, $pv, $fv, $type = 0) |
||
| 82 | { |
||
| 83 | self::init(); |
||
| 84 | |||
| 85 | // Calculate the PVIF and FVIFA |
||
| 86 | $pvif = self::PVIF($rate, $nper); |
||
| 87 | $fvifa = self::FVIFA($rate, $nper); |
||
| 88 | |||
| 89 | return ((-$pv * $pvif - $fv) / ((1.0 + $rate * $type) * $fvifa)); |
||
| 90 | } |
||
| 91 | |||
| 92 | /** |
||
| 93 | * IPMT |
||
| 94 | * Returns the interest payment for a given period for an investment based |
||
| 95 | * on periodic, constant payments and a constant interest rate. |
||
| 96 | * |
||
| 97 | * For a more complete description of the arguments in IPMT, see the PV function. |
||
| 98 | * |
||
| 99 | * |
||
| 100 | * Make sure that you are consistent about the units you use for specifying rate and nper. If you make monthly payments on a four-year loan at 12 percent annual interest, use 12%/12 for rate and 4*12 for nper. If you make annual payments on the same loan, use 12% for rate and 4 for nper. |
||
| 101 | * For all the arguments, cash you pay out, such as deposits to savings, is represented by negative numbers; cash you receive, such as dividend checks, is represented by positive numbers. |
||
| 102 | * |
||
| 103 | * @param $rate // The interest rate per period. |
||
|
0 ignored issues
–
show
Documentation
Bug
introduced
by
Loading history...
|
|||
| 104 | * @param $per // The period for which you want to find the interest and must be in the range 1 to nper. |
||
| 105 | * @param $nper // The total number of payment periods in an annuity. |
||
| 106 | * @param $pv // The present value, or the lump-sum amount that a series of future payments is worth right now. |
||
| 107 | * @param float $fv // The future value, or a cash balance you want to attain after the last payment is made. If fv is omitted, it is assumed to be 0 (the future value of a loan, for example, is 0). |
||
| 108 | * @param int $type // The number 0 or 1 and indicates when payments are due. If type is omitted, it is assumed to be 0. |
||
| 109 | * |
||
| 110 | * @return mixed|null |
||
| 111 | */ |
||
| 112 | public static function IPMT($rate, $per, $nper, $pv, $fv = 0.0, $type = 0) |
||
| 113 | { |
||
| 114 | self::init(); |
||
| 115 | |||
| 116 | if (($per < 1) || ($per >= ($nper + 1))) { |
||
| 117 | return null; |
||
| 118 | } |
||
| 119 | |||
| 120 | $pmt = self::PMT($rate, $nper, $pv, $fv, $type); |
||
| 121 | |||
| 122 | $ipmt = self::interestPart($pv, $pmt, $rate, $per - 1); |
||
| 123 | |||
| 124 | if (!is_finite($ipmt)) { |
||
| 125 | return null; |
||
| 126 | } |
||
| 127 | |||
| 128 | return $ipmt; |
||
| 129 | } |
||
| 130 | |||
| 131 | /** |
||
| 132 | * PPMT |
||
| 133 | * Returns the payment on the principal for a given period for an |
||
| 134 | * investment based on periodic, constant payments and a constant |
||
| 135 | * interest rate. |
||
| 136 | * |
||
| 137 | * @param $rate //Required. The interest rate per period. |
||
|
0 ignored issues
–
show
|
|||
| 138 | * @param $per //Required. The period for which you want to find the interest and must be in the range 1 to nper. |
||
| 139 | * @param $nper //Required. The total number of payment periods in an annuity. |
||
| 140 | * @param $pv //Required. The present value, or the lump-sum amount that a series of future payments is worth right now. |
||
| 141 | * @param float $fv //Optional. The future value, or a cash balance you want to attain after the last payment is made. If fv is omitted, it is assumed to be 0 (the future value of a loan, for example, is 0). |
||
| 142 | * @param int $type //Optional. The number 0 or 1 and indicates when payments are due. If type is omitted, it is assumed to be 0. |
||
| 143 | * |
||
| 144 | * @return float|null |
||
| 145 | */ |
||
| 146 | public static function PPMT($rate, $per, $nper, $pv, $fv = 0.0, $type = 0) |
||
| 147 | { |
||
| 148 | self::init(); |
||
| 149 | |||
| 150 | if (($per < 1) || ($per >= ($nper + 1))) { |
||
| 151 | return null; |
||
| 152 | } |
||
| 153 | |||
| 154 | $pmt = self::PMT($rate, $nper, $pv, $fv, $type); |
||
| 155 | $ipmt = self::interestPart($pv, $pmt, $rate, $per - 1); |
||
| 156 | |||
| 157 | return ((is_finite($pmt) && is_finite($ipmt)) ? $pmt - $ipmt : null); |
||
| 158 | } |
||
| 159 | |||
| 160 | |||
| 161 | /** |
||
| 162 | * NPV |
||
| 163 | * Calculates the net present value of an investment by using a |
||
| 164 | * discount rate and a series of future payments (negative values) |
||
| 165 | * and income (positive values). |
||
| 166 | * |
||
| 167 | * n / values(i) \ |
||
| 168 | * NPV = SUM | -------------- | |
||
| 169 | * i=1 | i | |
||
| 170 | * \ (1 + rate) / |
||
| 171 | * |
||
| 172 | * @param float $rate |
||
| 173 | * @param float[] $values |
||
| 174 | * |
||
| 175 | * @return float|int|null |
||
| 176 | */ |
||
| 177 | private static function NPV($rate, $values) |
||
| 178 | { |
||
| 179 | self::init(); |
||
| 180 | |||
| 181 | if (!is_array($values)) { |
||
|
0 ignored issues
–
show
|
|||
| 182 | return null; |
||
| 183 | } |
||
| 184 | |||
| 185 | $npv = 0.0; |
||
| 186 | foreach ($values as $i => $iValue) { |
||
| 187 | $npv += $iValue / ((1 + $rate) ** ($i + 1)); |
||
| 188 | } |
||
| 189 | |||
| 190 | return (is_finite($npv) ? $npv : null); |
||
| 191 | } |
||
| 192 | |||
| 193 | /** |
||
| 194 | * IRR |
||
| 195 | * Returns the internal rate of return for a series of cash flows |
||
| 196 | * represented by the numbers in values. These cash flows do not |
||
| 197 | * have to be even, as they would be for an annuity. However, the |
||
| 198 | * cash flows must occur at regular intervals, such as monthly or |
||
| 199 | * annually. The internal rate of return is the interest rate |
||
| 200 | * received for an investment consisting of payments (negative |
||
| 201 | * values) and income (positive values) that occur at regular periods. |
||
| 202 | * |
||
| 203 | * @param float[] $values |
||
| 204 | * @param float $guess |
||
| 205 | * |
||
| 206 | * @return float|null |
||
| 207 | */ |
||
| 208 | public static function IRR($values, $guess = 0.1) |
||
| 209 | { |
||
| 210 | self::init(); |
||
| 211 | |||
| 212 | if (!is_array($values)) { |
||
|
0 ignored issues
–
show
|
|||
| 213 | return null; |
||
| 214 | } |
||
| 215 | |||
| 216 | // create an initial bracket, with a root somewhere between bot and top |
||
| 217 | $x1 = 0.0; |
||
| 218 | $x2 = $guess; |
||
| 219 | $f1 = self::NPV($x1, $values); |
||
| 220 | $f2 = self::NPV($x2, $values); |
||
| 221 | for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; $i++) { |
||
| 222 | if (($f1 * $f2) < 0.0) { |
||
| 223 | break; |
||
| 224 | } |
||
| 225 | if (abs($f1) < abs($f2)) { |
||
| 226 | $f1 = self::NPV($x1 += 1.6 * ($x1 - $x2), $values); |
||
| 227 | } else { |
||
| 228 | $f2 = self::NPV($x2 += 1.6 * ($x2 - $x1), $values); |
||
| 229 | } |
||
| 230 | } |
||
| 231 | if (($f1 * $f2) > 0.0) { |
||
| 232 | return null; |
||
| 233 | } |
||
| 234 | |||
| 235 | $f = self::NPV($x1, $values); |
||
| 236 | if ($f < 0.0) { |
||
| 237 | $rtb = $x1; |
||
| 238 | $dx = $x2 - $x1; |
||
| 239 | } else { |
||
| 240 | $rtb = $x2; |
||
| 241 | $dx = $x1 - $x2; |
||
| 242 | } |
||
| 243 | |||
| 244 | for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; $i++) { |
||
| 245 | $dx *= 0.5; |
||
| 246 | $x_mid = $rtb + $dx; |
||
| 247 | $f_mid = self::NPV($x_mid, $values); |
||
| 248 | if ($f_mid <= 0.0) { |
||
| 249 | $rtb = $x_mid; |
||
| 250 | } |
||
| 251 | if ((abs($f_mid) < self::FINANCIAL_ACCURACY) || (abs($dx) < self::FINANCIAL_ACCURACY)) { |
||
| 252 | return $x_mid; |
||
| 253 | } |
||
| 254 | } |
||
| 255 | |||
| 256 | return null; |
||
| 257 | } |
||
| 258 | |||
| 259 | /** |
||
| 260 | * @param float[] $values |
||
| 261 | * @param int[] $timestamps |
||
| 262 | * @param float $guess |
||
| 263 | * |
||
| 264 | * @return float|null |
||
| 265 | */ |
||
| 266 | public static function XIRR($values, $timestamps, $guess = 0.1) |
||
| 267 | { |
||
| 268 | self::init(); |
||
| 269 | |||
| 270 | // Initialize dates and check that values contains at least one positive value and one negative value |
||
| 271 | $positive = false; |
||
| 272 | $negative = false; |
||
| 273 | |||
| 274 | //XIRR Return error if number of values does not equal to number of timestamps |
||
| 275 | if (count($values) != count($timestamps)) { |
||
| 276 | return null; |
||
| 277 | } |
||
| 278 | |||
| 279 | //XIRR sort array on key (not sure whether it is needed, but just to be sure :-)) |
||
| 280 | array_multisort($timestamps, $values); |
||
| 281 | |||
| 282 | //XIRR determine first timestamp |
||
| 283 | $lowestTimestamp = new \DateTime('@' . min($timestamps)); |
||
| 284 | |||
| 285 | $dates = []; |
||
| 286 | foreach ($values as $key => $value) { |
||
| 287 | //XIRR Calculate the number of days between the given timestamp and the lowest timestamp |
||
| 288 | $dates[] = date_diff($lowestTimestamp, date_create('@' . $timestamps[$key]))->days; |
||
|
0 ignored issues
–
show
It seems like
date_diff($lowestTimesta...imestamps[$key]))->days can also be of type boolean. However, the property $days is declared as type false|integer. Maybe add an additional type check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly. For example, imagine you have a variable Either this assignment is in error or a type check should be added for that assignment. class Id
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class Account
{
/** @var Id $id */
public $id;
}
$account_id = false;
if (starsAreRight()) {
$account_id = new Id(42);
}
$account = new Account();
if ($account instanceof Id)
{
$account->id = $account_id;
}
Loading history...
|
|||
| 289 | |||
| 290 | if ($value > 0) { |
||
| 291 | $positive = true; |
||
| 292 | } |
||
| 293 | if ($value < 0) { |
||
| 294 | $negative = true; |
||
| 295 | } |
||
| 296 | } |
||
| 297 | //XIRR remove all keys from the input array (which are the timestamps) |
||
| 298 | $values = array_values($values); |
||
| 299 | |||
| 300 | // Return error if values does not contain at least one positive value and one negative value |
||
| 301 | if (!$positive || !$negative) { |
||
| 302 | return null; |
||
| 303 | } |
||
| 304 | |||
| 305 | // Initialize guess and resultRate |
||
| 306 | $resultRate = $guess; |
||
| 307 | |||
| 308 | // Implement Newton's method |
||
| 309 | |||
| 310 | $iteration = 0; |
||
| 311 | $contLoop = true; |
||
| 312 | while ($contLoop && (++$iteration < self::FINANCIAL_MAX_ITERATIONS)) { |
||
| 313 | $resultValue = self::irrResult($values, $dates, $resultRate); |
||
| 314 | $newRate = $resultRate - $resultValue / self::irrResultDeriv($values, $dates, $resultRate); |
||
| 315 | $epsRate = abs($newRate - $resultRate); |
||
| 316 | $resultRate = $newRate; |
||
| 317 | $contLoop = ($epsRate > self::FINANCIAL_ACCURACY) && (abs($resultValue) > self::FINANCIAL_ACCURACY); |
||
| 318 | } |
||
| 319 | |||
| 320 | if ($contLoop) { |
||
| 321 | return null; |
||
| 322 | } |
||
| 323 | |||
| 324 | // Return internal rate of return |
||
| 325 | return $resultRate; |
||
| 326 | } |
||
| 327 | |||
| 328 | // Calculates the resulting amount |
||
| 329 | private static function irrResult($values, $dates, $rate) |
||
| 330 | { |
||
| 331 | self::init(); |
||
| 332 | |||
| 333 | $r = $rate + 1; |
||
| 334 | $result = $values[0]; |
||
| 335 | for ($i = 1, $iMax = count($values); $i < $iMax; $i++) { |
||
| 336 | $result += $values[$i] / ($r ** (($dates[$i] - $dates[0]) / 365)); |
||
| 337 | } |
||
| 338 | |||
| 339 | return $result; |
||
| 340 | } |
||
| 341 | |||
| 342 | // Calculates the first derivation |
||
| 343 | private static function irrResultDeriv($values, $dates, $rate) |
||
| 344 | { |
||
| 345 | self::init(); |
||
| 346 | |||
| 347 | $r = $rate + 1; |
||
| 348 | $result = 0; |
||
| 349 | for ($i = 1, $iMax = count($values); $i < $iMax; $i++) { |
||
| 350 | $frac = ($dates[$i] - $dates[0]) / 365; |
||
| 351 | $result -= $frac * $values[$i] / ($r ** ($frac + 1)); |
||
| 352 | } |
||
| 353 | |||
| 354 | return $result; |
||
| 355 | } |
||
| 356 | |||
| 357 | |||
| 358 | /** |
||
| 359 | * @param float $apr Interest rate. |
||
| 360 | * @param integer $term Loan length in years. |
||
| 361 | * @param float $loan The loan amount. |
||
| 362 | * |
||
| 363 | * @return float |
||
| 364 | */ |
||
| 365 | public function calculatePMT($apr, int $term, $loan): float |
||
| 366 | { |
||
| 367 | $term = $term * 12; |
||
| 368 | $apr = $apr / 1200; |
||
| 369 | $amount = $apr * -$loan * ((1 + $apr) ** $term) / (1 - ((1 + $apr) ** $term)); |
||
| 370 | |||
| 371 | return round($amount); |
||
| 372 | |||
| 373 | } |
||
| 374 | } |
||
| 375 |