Issues (18)

src/Financial.php (5 issues)

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
The doc comment // at position 0 could not be parsed: Unknown type name '//' at position 0 in //.
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
Documentation Bug introduced by
The doc comment //Required. at position 0 could not be parsed: Unknown type name '//Required.' at position 0 in //Required..
Loading history...
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
The condition is_array($values) is always true.
Loading history...
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
The condition is_array($values) is always true.
Loading history...
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
Documentation Bug introduced by
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 $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

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