簡體   English   中英

具有 xirr 和 xnpv 功能的金融 Python 庫?

[英]financial python library that has xirr and xnpv function?

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

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

有沒有具有這兩個功能的python庫? tks。

這是實現這兩個功能的一種方法。

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)

在網上找到的各種實現的幫助下,我想出了一個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

創建了一個用於快速 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}))

這個答案是對@uuazed 答案的改進,並由此衍生。 但是,有一些變化:

  1. 它使用熊貓數據框而不是元組列表
  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

我從@KT 的解決方案開始,但在幾個方面對其進行了改進:

  • 正如其他人所指出的,如果貼現率 <= -100%,則 xnpv 無需返回 inf
  • 如果現金流全部為正或全部為負,我們可以立即返回 nan:讓算法永遠搜索不存在的解決方案是沒有意義的
  • 我已將 daycount 約定作為輸入; 有時是 365,有時是 360 - 這取決於具體情況。 我沒有建模 30/360。 有關 Matlab 文檔的更多詳細信息
  • 我為最大迭代次數和算法的起點添加了可選輸入
  • 我沒有改變算法的默認容差,但這很容易改變

下面具體示例的主要發現(其他案例的結果可能會有所不同,我沒有時間測試許多其他案例):

  • 從值開始 = -sum(all cashflows) / sum(negative cashflows) 會稍微減慢算法的速度(降低 7-10%)
  • scipi 的 netwon 比 scipy 的 fsolve 快


牛頓與 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
    

PS 這個 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 的功能,但如果只是為了向后兼容,它們可能會繼續保持相同的行為。

創建了一個可用於 xirr 計算的 python 包 金融計算器。 底層,它使用牛頓的方法。

我也做了一些時間分析,它比@KT. 的答案中建議的 scipy 的 xnpv 方法好一點。

這是實現。

使用 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)
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)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM