Anexen / pyxirr

Rust-powered collection of financial functions.
https://anexen.github.io/pyxirr/
The Unlicense
172 stars 16 forks source link

Payment scenarios where IRR returns None #25

Closed JoshCleaverEmpire closed 2 years ago

JoshCleaverEmpire commented 2 years ago

First off, I wanted to say that I LOVE this library. I tested moving from 0.6.4 -> 0.7.2 and it seems like the IRR calcs are somehow twice as fast as they were. So thats incredible. I was hoping that it would reduce None results as well, but it does not. I was able to come up with ~1700 situations where pyxirr returns None and numpy financial returns a value. (There were 1707 in 0.6.4 and the same 1707 fail in 0.7.2) NPF is rather slow as you know and it'd be awesome to not have to fallback to NPF at all.

I wanted to get in and see if I could figure out what the issue is but I don't have any r experience - yet.

Here is a file containing the payment info of the ~1700 scenarios failed-irr.txt

I just had a quick script like this:

import json
from pyxirr import irr

with open("failed-irr.txt", "r") as file_object:
    bad_payments_list = json.loads(file_object.read())

fail_count = 0
success_count = 0

for bad_payment in bad_payments_list:
    monthly_irr = irr(bad_payment)
    if monthly_irr is None:
        fail_count += 1
    else:
        success_count += 1

print('Successful: ', success_count)
print('Failed: ', fail_count)

It takes a bit to run through all 1700 calcs. I realize this is open source and you likely have limited time to look at this. Let me know if theres anything else I can provide to help.

Anexen commented 2 years ago

@CliveCleaves, first of all, thank you so much for preparing so many examples. It's a lot of work.

By definition, the Internal Rate of Return is the interest rate that makes the Net Present Value zero. So, the result of npv(irr(payments), payments) must be close to zero.

I checked the first 100 cases from the file and as a result did not get a value close to zero (the results are in the table below).

Here is the code snippet I used:

import numpy_financial as npf

for i, bad_payment in enumerate(bad_payments_list[:100], start=1):
    monthly_irr = irr(bad_payment)
    if monthly_irr is None:
        rate = npf.irr(bad_payment)
        print(i, " ", rate, "\t", npf.npv(rate, bad_payment))

As you can see, npf calculates the wrong rate in most cases. In some cases, the rate is close to 0.01, but the default tolerance in pyxirr is 0.001.

My suggestion: add a parameter to control the tolerance. This will allow for rate close to 0.01 (as in lines 1, 31, 61) instead of None.

row npf.irr(payments) npf.npv(npf.irr(payments), payments)
1 -0.8936246934946801 -0.015625
2 -0.7990327261002742 -2560.0
3 -0.7158083244273705 3670016.0
4 -0.6425367523140351 -738197504.0
5 -0.45306129910967397 -6912.0
6 -0.27943531109758235 -16128.0
7 -0.05894303425691705 -226.9128818511963
8 -0.062069116554579806 -791.8739314079285
9 -0.06130802108808808 345.1872525215149
10 -0.05933842038265813 -1688.7603859901428
11 -0.05461159904197255 -978.9646096229553
12 -0.052236780965806084 28.20184588432312
13 -0.04993411223184674 105.46152472496033
14 -0.04771710604071966 -659.4393312931061
15 -0.11313290374336471 -2.1179680285957706e+17
16 -0.12380011736813235 -8.303599304799997e+20
17 -0.12148983904900423 -8.399282265207042e+20
18 -0.11351942509378687 -1.0985608755409074e+19
19 -0.10971825180743133 3.970466888300542e+19
20 -0.10617590512106323 -2.591060111004934e+19
21 -0.10288417295784236 -3.5182019461365e+18
22 -0.09982373039333636 -4.272342621694098e+18
23 -0.096974859077675 -1.0566482529556257e+19
24 -0.0942605713520277 -3.554631715430676e+18
25 -0.09160441776867478 2.0985123094762243e+18
26 -0.08638409596484464 2691025437610568.0
27 -0.08375016974399507 2.1768153108064202e+17
28 -0.07813769007643134 -3.1658335976152216e+16
29 -0.07496610539028381 -6260952040811012.0
30 -0.07135299331973177 -2587482753078264.0
31 -0.8936246934946801 -0.015625
32 -0.7998321714433255 1024.0
33 -0.35834923940077545 -11264.0
34 -0.3201477883422911 -25600.0
35 -0.06178150788719028 -1188.607373714447
36 -0.06479969725043433 -3342.7869114875793
37 -0.0639511124405634 -5621.257207870483
38 -0.06193347593420073 1884.1013813018799
39 -0.059586994349779654 -2328.552409172058
40 -0.057170549931088055 -4020.973428249359
41 -0.05478806692913096 -295.99726915359497
42 -0.05247866453616756 -2035.0299735069275
43 -0.0502534326425359 -405.5948085784912
44 -0.12092730577360811 -9.852461598033881e+18
45 -0.13088050883528168 -7.42961726435428e+22
46 -0.12832189961437568 -5.703403052118493e+22
47 -0.12433446914135182 1.0904007085940357e+22
48 -0.12035878161468416 -1.0133329081812726e+22
49 -0.11662953054938607 -2.735239229801871e+20
50 -0.1131766136006368 4.9687771080854576e+20
51 -0.1070419042505083 -6.538533176600873e+20
52 -0.10173330034181949 -3.403131881079792e+20
53 -0.09919648188532759 -2.0014840980656613e+20
54 -0.09668812782631997 -7.85954229262796e+18
55 -0.09417677384893142 -1.1776497112587638e+20
56 -0.0916133564870083 7.990358787247078e+19
57 -0.0889298995123946 1.0533573271899357e+19
58 -0.08603514212123309 4.1335453872802186e+18
59 -0.08279861201389727 -1.682472576693933e+18
60 -0.07903993184061098 4.056002572715154e+17
61 -0.8936246934946801 -0.015625
62 -0.40678912033617265 13312.0
63 -0.36271295591593067 -34816.0
64 -0.29319483223699905 -67584.0
65 -0.048873335750406754 -1.586534470319748
66 -0.06423297770524161 -1867.2682104110718
67 -0.06714763445396643 -4781.858381271362
68 -0.06622229055758766 -18794.263805389404
69 -0.06416275576541774 -51622.113845825195
70 -0.061794893125661665 -30779.593170166016
71 -0.059365765722609765 7436.420125007629
72 -0.05697382260635797 2957.5482454299927
73 -0.05465487196079233 -5706.3660707473755
74 -0.12749121493063642 -1.8105029574469603e+21
75 -0.1368043670031981 -5.094417497900866e+23
76 -0.1300269706616849 5.339293325272833e+22
77 -0.12607739717808297 6.23260729465026e+23
78 -0.12239718999773586 4.815888797973269e+22
79 -0.11900718071444105 -2.393781977084619e+22
80 -0.11302986834685591 4.4430147946035843e+21
81 -0.11040106061107957 -6.636981947398056e+22
82 -0.1079109580094878 2.1342333723921393e+21
83 -0.10546226382208324 7.911211341279792e+21
84 -0.10058576940085195 -3.615519782716349e+21
85 -0.09806843841170787 -4.0271933527674276e+21
86 -0.09540414560153032 -1.5542887407623744e+21
87 -0.09249112617613864 -7.51720377758537e+20
88 -0.08918507501300055 2.073024456663685e+20
89 -0.08527891420665845 5.904417979139076e+19
90 -0.6945652270960949 -1.5
91 -0.4554753414952092 -30.0
92 -0.18407373777942015 -385.1511790752411
93 -0.16742490338378802 -363.22283458709717
94 -0.14680347997793708 -21.146135687828064
95 -0.12905888338688876 0.4194630980491638
96 -0.11416158249887243 -11.897696077823639
97 -0.10165908399892021 -1.1505871415138245
98 -0.09108732540593278 -1.263539344072342
99 -0.0820965521945235 -0.27379148453474045
100 -0.07440695784995799 -0.2943432182073593
JoshCleaverEmpire commented 2 years ago

Awesome, thank you very much for looking into it. I was certainly taking for granted the idea that NPF was correct or close to correct! It was no trouble at all preparing the examples. I'll look into tolerance locally đŸ˜„