@@ -709,7 +709,27 @@ def rate(
709
709
return rn
710
710
711
711
712
- def irr (values , * , raise_exceptions = False ):
712
+ def _irr_default_selection (eirr ):
713
+ """ default selection logic for IRR function when there are > 1 real solutions """
714
+ # check sign of all IRR solutions
715
+ same_sign = np .all (eirr > 0 ) if eirr [0 ] > 0 else np .all (eirr < 0 )
716
+
717
+ # if the signs of IRR solutions are not the same, first filter potential IRR
718
+ # by comparing the total positive and negative cash flows.
719
+ if not same_sign :
720
+ pos = sum (eirr [eirr > 0 ])
721
+ neg = sum (eirr [eirr < 0 ])
722
+ if pos >= neg :
723
+ eirr = eirr [eirr >= 0 ]
724
+ else :
725
+ eirr = eirr [eirr < 0 ]
726
+
727
+ # pick the smallest one in magnitude and return
728
+ abs_eirr = np .abs (eirr )
729
+ return eirr [np .argmin (abs_eirr )]
730
+
731
+
732
+ def irr (values , * , raise_exceptions = False , selection_logic = _irr_default_selection ):
713
733
r"""Return the Internal Rate of Return (IRR).
714
734
715
735
This is the "average" periodically compounded rate of return
@@ -731,6 +751,12 @@ def irr(values, *, raise_exceptions=False):
731
751
having reached the maximum number of iterations (IterationsExceededException).
732
752
Set to False as default, thus returning NaNs in the two previous
733
753
cases.
754
+ selection_logic: function, optional
755
+ Function for selection logic when more than 1 real solutions is found.
756
+ User may insert their own customised function for selection
757
+ of IRR values.The function should accept a one-dimensional array
758
+ of numbers and return a number.
759
+
734
760
735
761
Returns
736
762
-------
@@ -775,20 +801,24 @@ def irr(values, *, raise_exceptions=False):
775
801
0.06206
776
802
>>> round(npf.irr([-5, 10.5, 1, -8, 1]), 5)
777
803
0.0886
778
-
804
+ >>> npf.irr([[-100, 0, 0, 74], [-100, 100, 0, 7]]).round(5)
805
+ array([-0.0955 , 0.06206])
806
+
779
807
"""
780
- values = np .atleast_1d (values )
781
- if values .ndim != 1 :
782
- raise ValueError ("Cashflows must be a rank-1 array" )
783
-
784
- # If all values are of the same sign no solution exists
785
- # we don't perform any further calculations and exit early
786
- same_sign = np .all (values > 0 ) if values [0 ] > 0 else np .all (values < 0 )
787
- if same_sign :
788
- if raise_exceptions :
789
- raise NoRealSolutionError ('No real solution exists for IRR since all '
790
- 'cashflows are of the same sign.' )
791
- return np .nan
808
+ values = np .atleast_2d (values )
809
+ if values .ndim != 2 :
810
+ raise ValueError ("Cashflows must be a 2D array" )
811
+
812
+ irr_results = np .empty (values .shape [0 ])
813
+ for i , row in enumerate (values ):
814
+ # If all values are of the same sign, no solution exists
815
+ # We don't perform any further calculations and exit early
816
+ same_sign = np .all (row > 0 ) if row [0 ] > 0 else np .all (row < 0 )
817
+ if same_sign :
818
+ if raise_exceptions :
819
+ raise NoRealSolutionError ('No real solution exists for IRR since all '
820
+ 'cashflows are of the same sign.' )
821
+ irr_results [i ] = np .nan
792
822
793
823
# We aim to solve eirr such that NPV is exactly zero. This can be framed as
794
824
# simply finding the closest root of a polynomial to a given initial guess
@@ -807,40 +837,25 @@ def irr(values, *, raise_exceptions=False):
807
837
#
808
838
# which we solve using Newton-Raphson and then reverse out the solution
809
839
# as eirr = g - 1 (if we are close enough to a solution)
810
-
811
- g = np .roots (values )
812
- eirr = np .real (g [np .isreal (g )]) - 1
813
-
814
- # realistic IRR
815
- eirr = eirr [eirr >= - 1 ]
816
-
817
- # if no real solution
818
- if len (eirr ) == 0 :
819
- if raise_exceptions :
820
- raise NoRealSolutionError ("No real solution is found for IRR." )
821
- return np .nan
822
-
823
- # if only one real solution
824
- if len (eirr ) == 1 :
825
- return eirr [0 ]
826
-
827
- # below is for the situation when there are more than 2 real solutions.
828
- # check sign of all IRR solutions
829
- same_sign = np .all (eirr > 0 ) if eirr [0 ] > 0 else np .all (eirr < 0 )
830
-
831
- # if the signs of IRR solutions are not the same, first filter potential IRR
832
- # by comparing the total positive and negative cash flows.
833
- if not same_sign :
834
- pos = sum (values [values > 0 ])
835
- neg = sum (values [values < 0 ])
836
- if pos >= neg :
837
- eirr = eirr [eirr >= 0 ]
838
840
else :
839
- eirr = eirr [eirr < 0 ]
840
-
841
- # pick the smallest one in magnitude and return
842
- abs_eirr = np .abs (eirr )
843
- return eirr [np .argmin (abs_eirr )]
841
+ g = np .roots (row )
842
+ eirr = np .real (g [np .isreal (g )]) - 1
843
+
844
+ # Realistic IRR
845
+ eirr = eirr [eirr >= - 1 ]
846
+
847
+ # If no real solution
848
+ if len (eirr ) == 0 :
849
+ if raise_exceptions :
850
+ raise NoRealSolutionError ("No real solution is found for IRR." )
851
+ irr_results [i ] = np .nan
852
+ # If only one real solution
853
+ elif len (eirr ) == 1 :
854
+ irr_results [i ] = eirr [0 ]
855
+ else :
856
+ irr_results [i ] = selection_logic (eirr )
857
+
858
+ return _ufunc_like (irr_results )
844
859
845
860
846
861
def npv (rate , values ):
0 commit comments