【问题标题】:financial python library that has xirr and xnpv function?具有xirr和xnpv功能的金融python库?
【发布时间】:2012-02-13 17:42:50
【问题描述】:

numpy 有 irr 和 npv 功能,但我需要 xirr 和 xnpv 功能。

此链接指出 xirr 和 xnpv 即将推出。 http://www.projectdirigible.com/documentation/spreadsheet-functions.html#coming-soon

有没有具有这两个功能的python库? tks。

【问题讨论】:

    标签: python financial


    【解决方案1】:

    借助我在网上找到的各种实现,我想出了一个python实现:

    def xirr(transactions):
        years = [(ta[0] - transactions[0][0]).days / 365.0 for ta in transactions]
        residual = 1
        step = 0.05
        guess = 0.05
        epsilon = 0.0001
        limit = 10000
        while abs(residual) > epsilon and limit > 0:
            limit -= 1
            residual = 0.0
            for i, ta in enumerate(transactions):
                residual += ta[1] / pow(guess, years[i])
            if abs(residual) > epsilon:
                if residual > 0:
                    guess += step
                else:
                    guess -= step
                    step /= 2.0
        return guess-1
    
    from datetime import date
    tas = [ (date(2010, 12, 29), -10000),
        (date(2012, 1, 25), 20),
        (date(2012, 3, 8), 10100)]
    print xirr(tas) #0.0100612640381
    

    【讨论】:

    • 警告:如果您使用这些stepguess 值,将无法在(-100%, -95%) 中获得irr
    • 由于这仍然是 Python 中 XIRR 的热门搜索结果之一,我觉得有必要说这个计算与方向无关。它假设流入为负,流出为正。如果你反过来,这不起作用。
    【解决方案2】:

    使用 Pandas,我可以执行以下操作: (注意,我使用的是 ACT/365 约定)

    rate = 0.10
    dates= pandas.date_range(start=pandas.Timestamp('2015-01-01'),periods=5, freq="AS")
    cfs = pandas.Series([-500,200,200,200,200],index=dates)
    
    # intermediate calculations( if interested)
    # cf_xnpv_days = [(cf.index[i]-cf.index[i-1]).days for i in range(1,len(cf.index))]
    # cf_xnpv_days_cumulative = [(cf.index[i]-cf.index[0]).days for i in range(1,len(cf.index))]
    # cf_xnpv_days_disc_factors = [(1+rate)**(float((cf.index[i]-cf.index[0]).days)/365.0)-1   for i in range(1,len(cf.index))]
    
    cf_xnpv_days_pvs = [cf[i]/float(1+(1+rate)**(float((cf.index[i]-cf.index[0]).days)/365.0)-1)  for i in range(1,len(cf.index))]
    
    cf_xnpv = cf[0]+ sum(cf_xnpv_days_pvs)
    

    【讨论】:

      【解决方案3】:

      这是实现这两个功能的一种方法。

      import scipy.optimize
      
      def xnpv(rate, values, dates):
          '''Equivalent of Excel's XNPV function.
      
          >>> from datetime import date
          >>> dates = [date(2010, 12, 29), date(2012, 1, 25), date(2012, 3, 8)]
          >>> values = [-10000, 20, 10100]
          >>> xnpv(0.1, values, dates)
          -966.4345...
          '''
          if rate <= -1.0:
              return float('inf')
          d0 = dates[0]    # or min(dates)
          return sum([ vi / (1.0 + rate)**((di - d0).days / 365.0) for vi, di in zip(values, dates)])
      
      def xirr(values, dates):
          '''Equivalent of Excel's XIRR function.
      
          >>> from datetime import date
          >>> dates = [date(2010, 12, 29), date(2012, 1, 25), date(2012, 3, 8)]
          >>> values = [-10000, 20, 10100]
          >>> xirr(values, dates)
          0.0100612...
          '''
          try:
              return scipy.optimize.newton(lambda r: xnpv(r, values, dates), 0.0)
          except RuntimeError:    # Failed to converge?
              return scipy.optimize.brentq(lambda r: xnpv(r, values, dates), -1.0, 1e10)
      

      【讨论】:

      • 您能解释一下为什么您的 XNPV 函数在任何低于 -1.0 (-100%) 的比率下返回无穷大吗?我完全理解 -100% 的情况,但是幂运算符在除法之前绑定,因此对于不完全等于 -100% 的比率,您不会得到除以零。例如,以 105% 的利率,从现在起每年支付 100 美元的 NPV = 100 / (1 + 1.05) ** 1 = 48.78 美元......未来以 -5% 的利率支付的相同金额约为 -105 美元( 100 / (1 - .05) ** 1)。目前一些债券“支付”负利率,所以这不仅仅是理论上的。现在考虑比率 -105%,我们得到 100 / (1 - 1.05) ** 1 = -1999.999...
      • 100/(1-1.05)**2 = 40000 怎么样?这有意义吗?就负利率债券而言,您所说的利率是否低于 0,而不是利率低于 -100%?
      • 我做了以下改进,似乎收敛得更快、更准确。基本上,它使用基于总回报的猜测作为牛顿法的起点。 ``` def xirr(values, dates): positives = [x if x > 0 else 0 for x in values]negatives = [x if x
      【解决方案4】:

      此答案是对@uuazed 答案的改进,并由此衍生而来。但是,有一些变化:

      1. 它使用 pandas 数据框而不是元组列表
      2. 它与现金流方向无关,即无论您将流入视为负数,将流出视为正数还是反之亦然,只要所有交易的处理方式一致,结果都会相同。
      3. 如果现金流未按日期排序,则使用此方法计算 XIRR 将不起作用。因此,我在内部处理了数据框的排序。
      4. 在较早的答案中,有一个隐含的假设,即 XIRR 将大部分为正。这造成了另一条评论中指出的问题,即无法计算 -100% 和 -95% 之间的 XIRR。这个解决方案可以解决这个问题。
      import pandas as pd
      import numpy as np
      
      def xirr(df, guess=0.05, date_column = 'date', amount_column = 'amount'):
          '''Calculates XIRR from a series of cashflows. 
             Needs a dataframe with columns date and amount, customisable through parameters. 
             Requires Pandas, NumPy libraries'''
      
          df = df.sort_values(by=date_column).reset_index(drop=True)
          df['years'] = df[date_column].apply(lambda x: (x-df[date_column][0]).days/365)
          step = 0.05
          epsilon = 0.0001
          limit = 1000
          residual = 1
      
          #Test for direction of cashflows
          disc_val_1 = df[[amount_column, 'years']].apply(
                      lambda x: x[amount_column]/((1+guess)**x['years']), axis=1).sum()
          disc_val_2 = df[[amount_column, 'years']].apply(
                      lambda x: x[amount_column]/((1.05+guess)**x['years']), axis=1).sum()
          mul = 1 if disc_val_2 < disc_val_1 else -1
      
          #Calculate XIRR    
          for i in range(limit):
              prev_residual = residual
              df['disc_val'] = df[[amount_column, 'years']].apply(
                      lambda x: x[amount_column]/((1+guess)**x['years']), axis=1)
              residual = df['disc_val'].sum()
              if abs(residual) > epsilon:
                  if np.sign(residual) != np.sign(prev_residual):
                      step /= 2
                  guess = guess + step * np.sign(residual) * mul   
              else:
                  return guess
      

      解释:

      在测试块中,它检查增加贴现率是增加贴现值还是降低贴现值。基于该测试,确定猜测应该移动的方向。该块使函数处理现金流,而不管用户假设的方向。

      np.sign(residual) != np.sign(prev_residual) 检查猜测何时增加/减少超过所需的 XIRR 率,因为那是残差从负变为正或反之亦然的时候。此时步长减小。

      numpy 包不是绝对必要的。如果没有 numpy,np.sign(residual) 可以替换为 residual/abs(residual)。我使用 numpy 让代码更具可读性和直观性

      我尝试使用各种现金流测试此代码。如果您发现任何此功能无法处理的情况,请告诉我。

      编辑:这是使用 numpy 数组的代码的更简洁和更快的版本。在我对大约 700 笔交易的测试中,这段代码的运行速度比上面的快 5 倍:

      def xirr(df, guess=0.05, date_column='date', amount_column='amount'):
          '''Calculates XIRR from a series of cashflows. 
             Needs a dataframe with columns date and amount, customisable through parameters. 
             Requires Pandas, NumPy libraries'''
      
          df = df.sort_values(by=date_column).reset_index(drop=True)
      
          amounts = df[amount_column].values
          dates = df[date_column].values
      
          years = np.array(dates-dates[0], dtype='timedelta64[D]').astype(int)/365
      
          step = 0.05
          epsilon = 0.0001
          limit = 1000
          residual = 1
      
          #Test for direction of cashflows
          disc_val_1 = np.sum(amounts/((1+guess)**years))
          disc_val_2 = np.sum(amounts/((1.05+guess)**years))
          mul = 1 if disc_val_2 < disc_val_1 else -1
      
          #Calculate XIRR    
          for i in range(limit):
              prev_residual = residual
              residual = np.sum(amounts/((1+guess)**years))
              if abs(residual) > epsilon:
                  if np.sign(residual) != np.sign(prev_residual):
                      step /= 2
                  guess = guess + step * np.sign(residual) * mul   
              else:
                  return guess
      

      【讨论】:

      • 您好 Gourav,首先,感谢您的工作,您的代码运行良好,除了以下几个场景,您介意看一下吗。 6/29/2018,-23979294.56 9/18,2018,-363717.94 11/26/2018,-3788281.69 12/21/2018,-932400 3/14/2019,-1614520.85 6/2019/2019,-1294216. 26/2019,-1321393.94 12/18/2019,-1632714.2 2/13/2020,-428904 3/23/2020,-843655.5 4/2/2020,-983682 11/06/2020, 6124619.985 我试过改变迭代,它不工作。提前致谢!
      【解决方案5】:

      我从 @KT 的解决方案开始,但在几个方面对其进行了改进:

      • 正如其他人指出的,如果贴现率
      • 如果现金流全部为正或全部为负,我们可以立即返回一个 nan:让算法永远搜索不存在的解决方案是没有意义的
      • 我已将 daycount 约定作为输入;有时是 365,有时是 360 - 这取决于具体情况。我没有建模 30/360。有关 Matlab 的更多详细信息docs
      • 我为最大迭代次数和算法的起点添加了可选输入
      • 我没有更改算法的默认容差,但这很容易更改

      以下具体示例的主要发现(其他案例的结果可能会有所不同,我没有时间测试许多其他案例):

      • 从值开始 = -sum(所有现金流)/sum(负现金流)会稍微减慢算法速度(7-10%)
      • scipi 的 netwon 比 scipy 的 fsolve 快


      newton vs fsolve 的执行时间:

      import numpy as np
      import pandas as pd
      import scipy
      import scipy.optimize
      from datetime import date
      import timeit
      
      
      def xnpv(rate, values, dates , daycount = 365):
          daycount = float(daycount)
          # Why would you want to return inf if the rate <= -100%? I removed it, I don't see how it makes sense
          # if rate <= -1.0:
          #     return float('inf')
          d0 = dates[0]    # or min(dates)
          # NB: this xnpv implementation discounts the first value LIKE EXCEL
          # numpy's npv does NOT, it only starts discounting from the 2nd
          return sum([ vi / (1.0 + rate)**((di - d0).days / daycount) for vi, di in zip(values, dates)])
      
      def find_guess(cf):
          whereneg = np.where(cf < 0)
          sumneg = np.sum( cf[whereneg] )
          return -np.sum(cf) / sumneg
          
          
      
      def xirr_fsolve(values, dates, daycount = 365, guess = 0, maxiters = 1000):
          
          cf = np.array(values)
          
          if np.where(cf <0,1,0).sum() ==0 | np.where(cf>0,1,0).sum() == 0:
              #if the cashflows are all positive or all negative, no point letting the algorithm
              #search forever for a solution which doesn't exist
              return np.nan
          
      
         
          result = scipy.optimize.fsolve(lambda r: xnpv(r, values, dates, daycount), x0 = guess , maxfev = maxiters, full_output = True )
          
          if result[2]==1: #ie if the solution converged; if it didn't, result[0] will be the last iteration, which won't be a solution
              return result[0][0]
          else:
              #consider rasiing a warning
              return np.nan
          
      def xirr_newton(values, dates, daycount = 365, guess = 0, maxiters = 1000, a = -100, b =1e5):
          # a and b: lower and upper bound for the brentq algorithm
          cf = np.array(values)
          
          if np.where(cf <0,1,0).sum() ==0 | np.where(cf>0,1,0).sum() == 0:
              #if the cashflows are all positive or all negative, no point letting the algorithm
              #search forever for a solution which doesn't exist
              return np.nan
          
          res_newton =  scipy.optimize.newton(lambda r: xnpv(r, values, dates, daycount), x0 = guess, maxiter = maxiters, full_output = True)
          
          if res_newton[1].converged == True:
              out = res_newton[0]
          else:
              res_b = scipy.optimize.brentq(lambda r: xnpv(r, values, dates, daycount), a = a , b = b, maxiter = maxiters, full_output = True)
              if res_b[1].converged == True:
                  out = res_b[0]
              else:
                  out = np.nan
                  
          return out
                  
      # let's compare how long each takes
      d0 = pd.to_datetime(date(2010,1,1))
      
      # an investment in which we pay 100 in the first month, then get 2 each month for the next 59 months
      df = pd.DataFrame()
      df['month'] = np.arange(0,60)
      df['dates'] = df.apply( lambda x: d0 + pd.DateOffset(months = x['month']) , axis = 1 )
      df['cf'] = 0
      df.iloc[0,2] = -100
      df.iloc[1:,2] = 2
      
      r = 100
      n = 5
      
      t_newton_no_guess = timeit.Timer ("xirr_newton(df['cf'], df['dates'], guess = find_guess(df['cf'].to_numpy() )  ) ", globals = globals() ).repeat(repeat = r, number = n)
      t_fsolve_no_guess = timeit.Timer ("xirr_fsolve(df['cf'], df['dates'],  guess = find_guess(df['cf'].to_numpy() ) )", globals = globals() ).repeat(repeat = r, number = n)
      
      t_newton_guess_0 = timeit.Timer ("xirr_newton(df['cf'], df['dates'] , guess =0.) ", globals = globals() ).repeat(repeat = r, number = n)
      t_fsolve_guess_0 = timeit.Timer ("xirr_fsolve(df['cf'], df['dates'], guess =0.) ", globals = globals() ).repeat(repeat = r, number = n)
      
      resdf = pd.DataFrame(index = ['min time'])
      resdf['newton no guess'] = [min(t_newton_no_guess)]
      resdf['fsolve no guess'] = [min(t_fsolve_no_guess)]
      resdf['newton guess 0'] = [min(t_newton_guess_0)]
      resdf['fsolve guess 0'] = [min(t_fsolve_guess_0)]
      # the docs explain why we should take the min and not the avg
      resdf = resdf.transpose()
      resdf['% diff vs fastest'] = (resdf / resdf.min() -1) * 100
      

      结论

      • 我注意到在某些情况下 newton 和 brentq 不收敛,但 fsolve 收敛了,所以我修改了函数,使其按顺序从 newton 开始,然后是 brentq,最后是 fsolve。李>
      • 我实际上还没有找到使用 brentq 来寻找解决方案的案例。我很想知道它什么时候会起作用,否则最好将其删除。
      • 我返回 try/except 是因为我注意到上面的代码并没有识别出所有不收敛的情况。这就是我想在我有更多时间的时候研究的事情

      这是我的最终代码:

      def xirr(values, dates, daycount = 365, guess = 0, maxiters = 10000, a = -100, b =1e10):
          # a and b: lower and upper bound for the brentq algorithm
          cf = np.array(values)
          
          if np.where(cf <0,1,0).sum() ==0 | np.where(cf >0,1,0).sum() == 0:
              #if the cashflows are all positive or all negative, no point letting the algorithm
              #search forever for a solution which doesn't exist
              return np.nan
          
          try:
              output =  scipy.optimize.newton(lambda r: xnpv(r, values, dates, daycount),
                                              x0 = guess, maxiter = maxiters, full_output = True, disp = True)[0]
          except RuntimeError:
              try:
      
                  output = scipy.optimize.brentq(lambda r: xnpv(r, values, dates, daycount),
                                            a = a , b = b, maxiter = maxiters, full_output = True, disp = True)[0]
              except:
                  result = scipy.optimize.fsolve(lambda r: xnpv(r, values, dates, daycount),
                                                 x0 = guess , maxfev = maxiters, full_output = True )
          
                  if result[2]==1: #ie if the solution converged; if it didn't, result[0] will be the last iteration, which won't be a solution
                      output = result[0][0]
                  else:
                      output = np.nan
                      
          return output
      

      测试

      这些是我用 pytest 进行的一些测试

      import pytest
      import numpy as np
      import pandas as pd
      import whatever_the_file_name_was as finc
      from datetime import date
      
          
          
      def test_xirr():
      
          dates = [date(2010, 12, 29), date(2012, 1, 25), date(2012, 3, 8)]
          values = [-10000, 20, 10100]
          assert pytest.approx( finc.xirr(values, dates) ) == 1.006127e-2
      
          dates = [date(2010, 1,1,), date(2010,12,27)]
          values = [-100,110]
          assert pytest.approx( finc.xirr(values, dates, daycount = 360) ) == 0.1
          
          values = [100,-110]
          assert pytest.approx( finc.xirr(values, dates, daycount = 360) ) == 0.1
          
          values = [-100,90]
          assert pytest.approx( finc.xirr(values, dates, daycount = 360) ) == -0.1
          
          # test numpy arrays
          values = np.array([-100,0,121])
          dates = [date(2010, 1,1,), date(2011,1,1), date(2012,1,1)]
          assert pytest.approx( finc.xirr(values, dates, daycount = 365) ) == 0.1
          
          # with a pandas df
          df = pd.DataFrame()
          df['values'] = values
          df['dates'] = dates
          assert pytest.approx( finc.xirr(df['values'], df['dates'], daycount = 365) ) == 0.1
          
          # with a pands df and datetypes
          df['dates'] = pd.to_datetime(dates)
          assert pytest.approx( finc.xirr(df['values'], df['dates'], daycount = 365) ) == 0.1
          
          # now for some unrealistic values
          df['values'] =[-100,5000,0]
          assert pytest.approx( finc.xirr(df['values'], df['dates'], daycount = 365) ) == 49
          
          df['values'] =[-1e3,0,1]
          rate = finc.xirr(df['values'], df['dates'], daycount = 365)
          npv = finc.xnpv(rate, df['values'], df['dates'])
          # this is an extreme case; as long as the corresponsing NPV is between these values it's not a bad result
          assertion = ( npv < 0.1 and npv > -.1)
          assert assertion == True
          
      

      附注这个 xnpv 和 numpy.npv 之间的重要区别

      严格来说,这与此答案无关,但对于使用 numpy 进行财务计算的人来说很有用:

      numpy.npv 不打折现金流的第一项 - 它从第二项开始,例如

      np.npv(0.1,[110,0]) = 110
      

      np.npv(0.1,[0,110] = 100
      

      不过,Excel 从第一个项目开始就有折扣:

      NPV(0.1,[110,0]) = 100
      

      Numpy 的财务函数将被弃用,并被 numpy_financial 的函数取代,但如果只是为了向后兼容,它们的行为可能会继续保持不变。

      【讨论】:

        【解决方案6】:
        def xirr(cashflows,transactions,guess=0.1):
        #function to calculate internal rate of return.
        #cashflow: list of tuple of date,transactions
        #transactions: list of transactions
        try:
            return optimize.newton(lambda r: xnpv(r,cashflows),guess)
        except RuntimeError:
            positives = [x if x > 0 else 0 for x in transactions]
            negatives = [x if x < 0 else 0 for x in transactions]
            return_guess = (sum(positives) + sum(negatives)) / (-sum(negatives))
            return optimize.newton(lambda r: xnpv(r,cashflows),return_guess)
        

        【讨论】:

        • 你能在你的代码中添加一些描述吗?
        • @WBM 上面的代码尝试使用他的现金流找到 Xirr 并猜测你提供的。我需要将它与excel XIRR函数相匹配,所以使用guess=0.1。但是在某些情况下我遇到了运行时错误,所以我添加了一个 except 块,它使用交易计算猜测。它为我解决了运行时错误场景。代码中提到了正在使用的输入的定义。
        【解决方案7】:

        创建了一个python包finance-calulator,可用于xirr计算。底层,它使用牛顿法。

        我也做了一些时间分析,它比@KT.'s answer中建议的 scipy 的 xnpv 方法好一点。

        Here's 实现。

        【讨论】:

          【解决方案8】:

          为快速 XIRR 计算创建了一个包,PyXIRR

          它没有外部依赖,比任何现有实现都运行得更快。

          from datetime import date
          from pyxirr import xirr
          
          dates = [date(2020, 1, 1), date(2021, 1, 1), date(2022, 1, 1)]
          amounts = [-1000, 1000, 1000]
          
          # feed columnar data
          xirr(dates, amounts)
          
          # feed tuples
          xirr(zip(dates, amounts))
          
          # feed DataFrame
          import pandas as pd
          xirr(pd.DataFrame({"dates": dates, "amounts": amounts}))
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2011-01-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-11-17
            • 2019-08-30
            • 1970-01-01
            相关资源
            最近更新 更多