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
![]() |
|||
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;
}
![]() |
|||
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 |