有xirr和xnpv功能的金融python库?

时间:2012-01-19 00:52:37

标签: python financial

numpy有irr和npv函数,但我需要xirr和xnpv函数。

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

是否有任何具有这两个功能的python库? TKS。

8 个答案:

答案 0 :(得分:10)

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

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)

答案 1 :(得分:9)

借助我在网上找到的各种实现,我想出了一个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

答案 2 :(得分:3)

为快速 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}))

答案 3 :(得分:1)

此答案是对@uuazed答案的改进,并由此得出。但是,有一些更改:

  1. 它使用pandas数据框而不是元组列表
  2. 这与现金流方向无关,即,将流入视为负数,将流出视为正数还是将流出视为正数,只要处理对所有交易都是一致的,结果都是相同的。
  3. 如果未按日期排序现金流量,则使用此方法的
  4. XIRR计算不起作用。因此,我已经在内部处理了数据框的排序。
  5. 在较早的答案中,有一个隐含的假设,即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

答案 4 :(得分:1)

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

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

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

  • 从一个值开始 = -sum(all cashflows) / sum(negative cashflows) 会稍微减慢算法的速度(降低 7-10%)
  • scipi 的网络比 scipy 的 fsolve 快


newton 与 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 的功能,但如果只是为了向后兼容,它们可能会继续保持相同的行为。

答案 5 :(得分:1)

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

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

Here's 实现。

答案 6 :(得分:0)

通过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)

答案 7 :(得分:0)

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)