################################################################################################################# # # Module: cost_analysis.py # Version: 0.1 (testing release, not final) # public domain, no licnese required # however, as a matter of fairness it would be nice if anyone using this set # of functions would: # a. cite my name as the author # b. notify me of any additions or changes in order to expand this collection # # Contains financial calculation functions for industrial cost analysis. # # The algorithms in this module and the examples are mainly inspired by # one book: # Title : Engineering Economic Analysis # Authors: Donald G. Newman, Jerome P. Lavelle # Publisher: Engineering Press, Austin/Texas # Edition: 7th, 1998 # # The best technical book on financial analysis, I ever found, however, you need to have a strong interest # for math in order to enjoy it. It is a book you will read over years and dive into again and again. # It exceeds by fast the superficial financial analysis taught in MBA programs. # (At least I never met an MBA graduate from top business schools who knew about # the traps of MS Excel's IRR() and NPV() functions. Which makes you think, # considering how much money these persons are responsible for.) # # If you need to start at the basics (logarithms, how the formulas used can be # deducted from common algebraic principles, &c.) you should take a good math # book on Algebra. I recommend Chapter 6 of # Titel: Algebra & Trigonometry # Author: Michael Sullivan # Publisher: Prenice Hall # Edition: 6th, 2002 # # All functions in this module have been tested with text book examples, # and many in real life (I am a professional number cruncher) but # it may well be that the one or other error was not found, so do not rely # blindly on this code. # # As documentation I have included examples from the python shell and hope it # will make it easier to start-up using this module. I also added extensive # explanations, so that persons who are not specialized in finance can understand # what the functions do. # # I appreciate feed back and suggestions. # Most of these functions are also embedded in a GUI tool with an included # text editor. See the cost_gui.pyw module. # # If you are looking for formulas for financial markets calculation, # try the pyFinancials module, published by gp.ciceri@acm.org, you may be able # to find the module under http://starship.python.net/crew/zanzi/ # If you have any problems finding this module, and are desperate to lay your # hands on it, mail me and I will mail it by return. # # Harm Kirchhoff # Izumi(Osaka), Japan # January 2004 # hk@pop.ms # # ############################################################################################################# """ TABLE OF CONTENTS Function Description Days and interest: interest_daily Amount of daily interest on a loan. interest_days Calculate number of bond days between two dates. Calculation of days follows specific conventions, depending on the market where the loan is taken. Conversions between interest rates: interest_effective(APR, Per) interest_annual(EFF, Per) interest_per_period(Int,SPers) interest_ipp_to_annual(IPP,SPers) interest_cont_comp(CCInt) interest_real(MarketIR, InflationR) Simple interest: future_val_simple(PV, Int, Per) present_val_simple(FV, Int, Per) interest_rate_simple(FV, PV, Per) interest_periods_simple(FV, PV, Int) capitalized_cost(Pmt, Int) Compounded Interest: future_value(Int,Per,PV, PMT=0, S=0) present_value(Int,Per,FV, PMT=0, S=0) payment(Int,Per,PV,FV,S=0) present_worth_factor(Int, Per) compound_amount_factor(Int,Per) compound_interest_factor(Int,Per) interest_periods(Int,PV,FV) interest_rate(Per,PV,FV) Continuous Compounding: cont_int_future_value(Principal, Int, Years) cont_int_present_value(Principal, Int, Years) cont_int_interest(PV, FV, Years) cont_int_years(PV, FV, Int) Installment Loans / Annuities / Uniform Series: annuity_outstanding(Pr, Pmt, Int, Per) annuity_principal(Pmt, Int, NPer) annuity_payment(Pr, Int, Per) annuity_periods(Pr, Pmt, Int) annuity_table(Pr, Pmt, Int, NPer) annuity_interest(Pr, Pmt, NPer, Prec=2) Depreciation / Appreciation Functions depreciation_linear(PurchVal,ResVal,YearsUse,FstYear) depreciation_decl_bal(PurchVal,ResVal,YearsUse,FstYear,FactDig=3,NumDig=2) geometric_present_worth_factor(Int, Per, Rate) depreciation_readable(Table,Diggits,Factor=0,Factor1=0) Cash Flow Analysis: net_present_value(cashflows,ir) net_future_value(cashflows,ir) benefit_cost_ratio(cashflows,ir) payback_period(cashflows) repayment(cashflow, outstanding, ir payment_schedule(cashflows, ir) payment_schedule_readable(sched) euac_salvage_value(Inv,Salv,Int,Per) euac_cashflows(CashFl, Int) gordon_growth(CashFl, Growth, Ir) irr_analysis(cashflows, externalrr, msg) cashflow_analysis(CashFl, Ir, Er) """ import hkToolBox, math, pprint #============================================================================================================== """ General Tools Interest Calculations: When talking about interest, there are three types: a. simple b. compounded c. continuously compounded In financial analysis, usually the compounded interest is used. """ #============================================================================================================== # Interest and Days: ----------------------------------------------------------------------------- def interest_daily(PV, Int, DD, DYear=360): "Calculate interest if principal is borrowed for DD days." """ Returns the amount of interest payable at anual interest rate of Int for a period of DD days. Dyear is the number of days per year. Most countries use 360 days/year, however, commonwealth countries use traditionally use 365/year, the default value is 360. Use interest_days() to determine DD. This function does not take any compounding into account. Returns a tuple: (amount, string representing the caluclation) amount,*%*(DD/DYear)=amount """ amount = PV*Int*DD/DYear msg = ('%0.2f' % PV)+" * "+('%6.2f' % (Int*100))+'% * '+str(DD)+"/"+str(DYear) return amount,msg def interest_days(Date1,Date2,E=0): "Calculate no. of Bond Days between two dates." """ Calculates the number of interest paying days between Date1 and Date2. These days are also sometimes called 'Bond Days'. Date1 and Date2 are supplied in the form DDMMYYYY. The calculation is the done as: {(YY2-YY1)*360}+{(MM2-MM1)*30}+(DD2-DD1) Regarding the values of DD2 and DD1, there are two standards:30/360 and 30E/360. (Actual/365 and Actual/Actual are ignored) At 30/360, if DD2=31 and DD1=30, change DD2 to 30, so that the extra day is taken into account, if DD1<30. At 30E/360,if DD2=31, change DD2 to 30, so that every month has at maximum 30 days. DD1 must be changed to 30 if 31 in any case. 30E/360 is used for the vast majority of Eurobonds and is the default method. 30/360 is used in the US domesitc market for some US gov., municipal and corporate bonds. Set E to any value to use this. """ # extraction of dates DD1=int(Date1[0:2]) ; MM1=int(Date1[2:4]) ; YY1=int(Date1[4:8]) if DD1 == 31: DD1=30 DD2=int(Date2[0:2]) ; MM2=int(Date2[2:4]) ; YY2=int(Date2[4:8]) # corrections according to 30E/360 or 30/360 standard if E <> 0: if DD1==30 and DD2==31: DD2=30 else : if DD2==31: DD2=30 # Error Checking if DD1<1 or DD2<1 : return -1 if DD1>30 or DD2>31 : return -2 if MM1<1 or MM2<1 : return -3 if MM2>12 or MM2>12 : return -4 if YY1>YY2 : return -5 if YY1==YY2: if MM1 > MM2 : return -6 if YY1==YY2 and MM1==MM2: if DD1>DD2 : return -7 # Calculation YY=YY2-YY1 ; MM=MM2-MM1 ; DD=DD2-DD1 return (YY*360)+(MM*30)+DD # Conversion between Interest Types: ----------------------------------------------------------------------------- # # The functions assume an interest per Period. If the period differs from a # calendar year, such as monthly, quarterly &c. The interest charge per annum # will differ: # # Example: # You borrow 3000 and will pay back in 30 equal monthly installments # with an interest rate of 12% p.a., compounded monthly. How much will the # monthly payment be ? # The interest rate quoted is an APR, but since the periods are less that a # calendar year, it needs to be converted to the effective interest rate. # To use the effective interest rate in the calculation for the annuity, it needs # to be converted further into a monthly interest rate. # >>> interest_effective(0.12, 12) # 0.12682503013196977 # >>> interest_per_period(0.126825,12) # 0.0099999977493334669 # >>> annuity_payment(3000, 0.01, 30) # 116.24433964754134 def interest_effective(APR, Per): "Convert APR to effective interest per year." """ The interest rate here, represents the Annual Percentage Rate (APR), where interest is paid Per times annualy. The annual percentage rate (APR) or nominal interest rate, is the rate of simple interest for 1 year. The effective interest rate takes into account the compounding effect that occurs when interest is paid several times a year. """ return (1+(APR/Per))**Per - 1 def interest_annual(EFF, Per): "Convert effective interest rate to APR." return ((1+EFF)**(1.0/Per) -1)*Per def interest_per_period(APR,SPers): "Convert APR/EFF into ir for sub-periods." """ All of the functions used, assume an interest rate per period. If the an APR was given per annum, but there are several (Spers) sub-periods in which interest is paid (ex. monthly). The APR p.a. needs to be converted into an interest rate per period, by taking the SPers'th root. Attention ! If the interest is compounded monthly (which is normal) use the effective interest rate instead of the APR. """ return (1+APR)**(1.0/SPers) - 1 def interest_ipp_to_annual(IPP,SPers): "Converts interest per Sub-Period into annual rate." """ Reverses the result of interest_per_period(). Example: You buy a car for 3,575 and make a down payment of 375 and pay 93.41 for 45 month. What is the effective annual interest rate ? >>> annuity_interest(3575-375,93.41,45) # interest per period 0.012500852986977576 >>> interest_ipp_to_annual(0.012500852986977576,12) # 12 periods make a year 0.16076625239648701 """ return (1+IPP)**SPers -1 def interest_cont_comp1(CCInt): "Calculate effective interest rate for countinuous compounding." """ In compounded interest calculations, there is a definite interest payment period, normally annually, but semi-annual and quarterly interest payments also occur. In continous compounding, this period becomes infinitely small. In principle a infinitely small payment of interest is made continously and compounded immediately. This function takes the annual interest rate for continous compounding CCInt and converts it into an APR that can be used in the compounded interest functions. Example: The bank pays 5% interest compounded continously. If 2000 are deposited, how much would be in the account after 2 years: >>> finance.interest_cont_comp(.05) 0.051271096376024117 >>> future_value(0.051271096376024117,2,2000) 2210.3418361512954 """ return math.e**CCInt - 1 def interest_cont_comp2(APR): """ Reverse of the above. """ return math.log(APR+1) def interest_real(MarketIR, InflationR): "Real Interest rate after inflation." """ When talking about interest, keep in mind that there is also inflation (or sometimes deflation.) Thus the real worth of money inflates or deflates and the purchase power of the cash to be received is different. To calculate how much purchase power is added in real terms, it is necessary to isolate the real interest rate from the market rate by stripping the market rate off the effects of inflation. The market interest rate contains the real interest rate plus the inflation rate and the effect of inflation on the real interest: MarketIR = RealIR + InflationR + (RealIR*InflationR) """ return (MarketIR - InflationR)/(1+InflationR) # Simple Interest Calculations: ----------------------------------------------------------------------------- # (As opposed to compounded interest, the interest earned will itself not earn interest). # # Simple interest calculations are required for projects, where the analysis # period is indefinite, normally public works, such as roads, bridges &c. # In these cases a measure for the cost is the capital, which needs to be # invested in the present, to finance the maintainance costs of the future. # Or, to put it simple: Cases where the interest is used to finance maintenance, # so that it will not be available to increase the amount of the deposit. def future_val_simple(PV, Int, Per): "Future value of a simple interest deposit." return PV*(1+Int*Per) def present_val_simple(FV, Int, Per): "Present value of a simple interest deposit." return FV/(1+Int*Per) def interest_rate_simple(FV, PV, Per): "Interest rate required for a simple interest deposit." return (FV/PV-1)/Per def interest_periods_simple(FV, PV, Int): "Determine how many periods are required to reach a given amount." return (FV/PV-1)/Int def capitalized_cost(Pmt, Int): "Capital to yield Interest amount equal to Pmt." """ Returns the capital that must be invested, in order to receive an interest amount equal to Pmt, at an interest rate of Int. """ return float(Pmt)/Int # Compounded Interest Calculations: ------------------------------------------------------------------------- # # The following functions represent saving situations, as opposed to loans/annuities. # Therefore the underlying assumption is, that money can be deposited at the interest # rate int. # Note that in the vast cases of economic analysis, it is normal to assume that # interest is paid at the end of the period. Exceptions exist mainly in the # financial markets. # While most of the formulas in this module assume that interest is payable # at the end of the period, some of the functions in this sub-section can # handle the case that interest is paid at the beginning of the period, if # the S flag is set to 1. # NOTE THAT THE INTEREST RATE IS THE INTEREST RATE PER PERIOD, NOT THE APR, IF # THERE ARE SEVERAL PERIODS PER ANNUM. def future_value(Int,Per,PV, PMT=0, S=0): "Calculate compound future value of capital PV." """ Calculates the future value of the invested capital PV based on the interest rate Int over a number of interest payment periods Per on a compund interest basis, assuming that interest is paid at the end of each period. PMT :A fixed payment per period (such as a monthly saving amount). S :S=0 if PMT is made at the end of each term S=1 if PMT is made at the beginning of each term Example: You deposit 100 at the end of each month in a savings account at 6% interest compounded and paid quarterly. No interest is paid on money that was not in the account for the full three month period. What will be the balance after 3 years ? >>> finance.interest_effective(0.06,4) 0.061363550624999652 >>> finance.interest_per_period(0.061363550624999652,4) 0.014999999999999902 >>> finance.future_value(0.015, 12, 0,300) 3912.3634292306797 """ if Int == 0: return 0 if type(Int) <> float: Int = float(Int) if type(Per) <> float: Per = float(Per) if type(PV) <> float: PV = float(PV) if S<>0 and S <> 1: return 0 pwf = present_worth_factor(Int,Per) return (1/pwf)*(PV + (1+Int*S) * PMT * (1-pwf)/Int) def payment(Int,Per,PV,FV,S=0): """ Calculates payment per period required, to reach FV, starting from PV and assuming an interest rate if Int for Per periods. """ if Int == 0: return 0 if Per == 0: return 0 if type(Int) <> float: Int = float(Int) if type(Per) <> float: Per = float(Per) if type(PV) <> float: PV = float(PV) if type(FV) <> float: FV = float(FV) pwf = present_worth_factor(Int,Per) return FV / ( (1/pwf)*(PV + (1+Int*S) * (1-pwf)/Int) ) def present_value(Int,Per,FV, PMT=0, S=0): "Calculates present value at compounded interest." """ Calculates the present value of the future return FV based on the interest rate Int over a number of interest payment periods Per, assuming that interest is paid at the end of each period. PMT :A fixed payment per period (such as a monthly saving amount). S :S=0 if PMT is made at the end of each term S=1 if PMT is made at the beginning of each term """ if Int == 0: return 0 if Per == 0: return 0 if FV == 0: return 0 if type(Int) <> float: Int = float(Int) if type(Per) <> float: Per = float(Per) if type(FV) <> float: FV = float(FV) pwf = present_worth_factor(Int,Per) return FV * pwf + PMT*(1+Int*S) * (1-pwf)/Int def present_worth_factor(Int, Per): "Int=interest rate, Per = period: returns a discount factor." """ Returns a standard present worth factor (also often called a discount factor), assuming that interest is paid at the end of the interest payment period. This is the standard factor used in analysis in industry. In the banking/bond and financial world, other factors may be used, depending on when the interest is payable. """ return 1.0/((1+Int)**Per) # Note # the following functions: interest_periods() and interest_rate() do not # allow for PMT to be taken into consideration. # I never found a way to resolve the compound formula with PMT and never # had a need to program an approximation algorithm. def compound_amount_factor(Int,Per): "Multiplyer to calculate compound amount." """ The compound amount factor is the opposited of the present worth factor. If a present worth factor is used to multiply a FV in order to arrive at a PV, the compound amount factor is used to multiply a PV in order to arrive at a FV. """ return 1.0/present_worth_factor(Int,Per) def compound_interest_factor(Int,Per): "Multipliyer to calculate compound interest amount." """ Returns a factor, which multiplied with a principal amount, gives the total compound interest that will be paid at an interest rate of Int over Per periods. Example: The following illustrates the relationship with the compound_amount_factor() function. >>> 100*finance.compound_amount_factor(0.07,70)-100 11298.939219763352 >>> 100*finance.compound_interest_factor(0.07,70) 11298.939219763352 """ return (1+Int)**Per - 1 def interest_periods(Int,PV,FV): "Number of interest paying periods required." """ Calculates the number of interest payment periods required to achieve a future value of FV with a capital investment of PV at a given interest rate of Int. Assumes that interst is paid at the end of each period. """ if Int == 0: return 0 if PV == 0: return 0 if FV == 0: return 0 if type(Int) <> float: Int = float(Int) if type(PV) <> float: PV = float(PV) if type(FV) <> float: FV = float(FV) return math.log10(FV/PV) / math.log10(1+Int) def interest_rate(Per,PV,FV): "Calculate Interest rate required to grow PV to FV." """ Calculates the Interest Rate required to achive a future return of FV with an investment of PV over a number Per interest payment periods. This formula works only, if there is one investment of PV made at the beginning and no further payments made. In other cases, use the IRR functions to determine the interest rate. """ if Per == 0: return 0 if PV == 0: return 0 if FV == 0: return 0 if type(Per) <> float: Per = float(Per) if type(PV) <> float: PV = float(PV) if type(FV) <> float: FV = float(FV) return (FV/PV)**(1/Per) -1 def compound_table(Per, Int, PV, Pmt=0): "Calculate a payment schedule." """ Calculates a payment schedule for compounded interest payments starting from a present value and going up for Per number of periods. Each period (from 0 to Per) has the format: [Per, beginning balance, Interest thereon, Payment, Ending balance] """ ret = [ [0,PV,0,0,PV] ] tot_int = 0 tot_pmt = 0 for period in range(1,Per+1): line = ret[ period-1] beg_bal = line[4] int = beg_bal * Int end_bal = beg_bal + int + Pmt tot_int += int tot_pmt += Pmt ret.append( [period , beg_bal , int , Pmt , end_bal ] ) # format into readable standard: for list in ret: list[0] = '%4i' % list[0] list[1] = hkToolBox.float_to_str( list[1] ) list[2] = hkToolBox.float_to_str( list[2] ) list[3] = hkToolBox.float_to_str( list[3] ) list[4] = hkToolBox.float_to_str( list[4] ) tot_int = hkToolBox.float_to_str( tot_int ) tot_pmt = hkToolBox.float_to_str( tot_pmt ) ret.insert(0, ['Per.' ,'Beg.B/L','Int.' ,'Pmt.' ,'End B/L'] ) ret.append( ['Total',' ' ,tot_int,tot_pmt,' ' ] ) ret = hkToolBox.table_format(ret,0) return ret # Continuous compunding ----------------------------------------------------------------------------------------------------- # The definition and explanation of the formulas for continuous compounding can # be found in good Algebra books. # For continuous compounding, the period of interest payment is indefinitely small. # Therefore, the concept of interest periods does not exist. # THE INTEREST QUOTED HERE IS ANNUAL (APR) def cont_int_future_value(Principal, Int, Years): "FV of Principal continously compounded." """ Int : annual interest rate in decimal form (0.1 instead of 10%) Years : number of years """ return Principal * math.pow(math.e, (Int * Years) ) def cont_int_present_value(Principal, Int, Years): "PV of Principal continuously compounded." """ Int : annual interest rate in decimal form (0.1 instead of 10%) Years : number of years """ return Principal * math.pow(math.e, (-Int * Years)) def cont_int_interest(PV, FV, Years): "Interest Rate" """ returns the interest reate required to grow PV to FV over a number of Years. """ return math.log(FV/PV) / Years def cont_int_years(PV, FV, Int): "Years." """ Returns number of years required to grow PV to FV at a given Int. rate. """ return math.log(FV/PV) / Int # Installment Loans / Annuities / Uniform Series: ------------------------------------------------------------------------- # # I would like to thank: # Dr. Math, The Math Forum@Drexel, http://mathforum.org/dr.math/ # (A research and educational enterprise of Drexel University) # for publishing a section on loans and interest, which helped me to compile # the following code. Many of the examples are taken from the book of # Newman/Lavelle, mentioned in the top of this module. # # These formulas apply to situations, where a equal cash payments are made # or received at the end of each period. # A typical example: # Lender loans Borrower a principal amount Pr, on which Borrower pays interest # rate of Int per interest period Per. ,Borrower repays a fixed amount # PMT per Per. At the end of the N'th Per, the last payment PMT by Borrower # pays off the last part of the principal and interest owned. # # Below a sample session, for some of these functions: # # >>> finance.annuity_payment(1000,.1,3.0) # 402.11480362537731 # >>> t1=finance.annuity_table(1000, 402.11480362537731, .1,3) # >>> t2=finance.payment_schedule_readable(t1) # >>> for x in t2: print x # ['Per.', 'Begining Bal', ' Cashflow ', ' Interest ', ' Principal ', ' Ending Bal.'] # [' 0', ' 0.00', ' 1000.00', ' 0.00', ' 1000.00', ' 1000.00'] # [' 1', ' 1000.00', ' -402.11', ' 100.00', ' -302.11', ' 697.89'] # [' 2', ' 697.89', ' -402.11', ' 69.79', ' -332.33', ' 365.56'] # [' 3', ' 365.56', ' -402.11', ' 36.56', ' -365.56', ' 0.00'] # ['Sum: ', ' ', ' -206.34', ' 206.34', ' 0.00', ''] # >>> finance.annuity_periods(1000, 402.11480362537731, .1) # 3.0000000000000004 # >>> finance.annuity_outstanding(1000, 402.11480362537731, .1, 2) # 365.55891238670711 # >>> finance.annuity_principal(402.11480362537731, .1,3) # 1000.0 # # It is important to use the appropriate interest rate. Often a APR is quoted, # but it can not be used, because the interest rate per period must be used. # Example: # How many month will it take to pay off a debt of 525 with monthly payments # of 15 at the end of each month if the interest is 18% compounded monthly ? # First, because of the continuous compounding, you need to calculate the # effective interest rate, and then convert it to a monthly interest rate: # >>> finance.interest_effective(0.18,12) # 0.19561817146153393 # >>> finance.interest_per_period(0.19561817146153393,12) # 0.014999999999999902 # >>> finance.annuity_periods(525,15,0.014999999999999902) # 50.000661596906014 # It would take 50 month to pay off the debt. # NOTE THAT THE INTEREST RATE IS THE INTEREST RATE PER PERIOD, NOT THE APR, IF # THERE ARE SEVERAL PERIODS PER ANNUM. def annuity_outstanding(Pr, Pmt, Int, Per): "FV is the amount still owned after Per payments." """ Calculate the amount of Principal (FV) that is still outstanding, after Borrower has made Per payments of Pmt onto the loan. """ if type(Pr ) <> float: Pr = float(Pr) if type(Pmt) <> float: Pmt = float(Pmt) if type(Int) <> float: Int = float(Int) if type(Per) <> float: Per = float(Per) return (Pr- Pmt/Int) * (1.0+Int)**Per + Pmt/Int def annuity_principal(Pmt, Int, NPer): "Amount of principal paid off after NPer." """ This function is only useful to verify if all principal is paid off in the last period. It does not yield the amount of principal that was paid off in any particular period. To determine the amount of principal paid off in any period, use: Pr - annuity_outstanding(Pr, Pmt, Int, Per) """ if type(Pmt) <> float: Pmt = float(Pmt) if type(Int) <> float: Int = float(Int) if type(NPer) <> float: NPer= float(NPer) a = present_worth_factor(Int, NPer) return Pmt * (1.0-a) / Int def annuity_payment(Pr, Int, Per): "Determine amount of fixed payment on an annuity." """ Example: You borrow 500 from the bank and have to repay this loan in 16 equal monthly payments at 1% interest per month. How much would each payment be. >>> annuity_payment(500, 0.01, 16) 33.972298410447131 """ if type(Pr ) <> float: Pr = float(Pr) if type(Int) <> float: Int = float(Int) if type(Per) <> float: Per = float(Per) if Per == 0: return Pr # if borrowed and returned at once, no interest return Pr*Int/ (1.0-present_worth_factor(Int, Per) ) def annuity_periods(Pr, Pmt, Int): "The number of periods required to pay off the loan." if type(Pr ) <> float: Pr = float(Pr) if type(Pmt) <> float: Pmt = float(Pmt) if type(Int) <> float: Int = float(Int) a = 1.0- (Pr*Int/Pmt) if a < 0: return False a = math.fabs( math.log10( a) ) return a / math.log10(1.0+Int) def annuity_table(Pr, Pmt, Int, NPer): "Compiles table of payments for an annuity." """ Function uses payment_schedule(cashflows, ir) to compile the table. You can use payment_schedule_readable() to compile a formatted version. NPer: Total number of periods. Pmt : Fixed payment per period. Pr : Principal loaned. Example: You take a car loan of 19,699 at 9% APR compounded monthly and intend to repay the loan in 4 years with monthly payments. How much would the monthly payment have to be ? How would a repayment schedule look ? >>> interest_effective(0.09,12) 0.093806897670983824 >>> interest_per_period(0.09381,12) 0.0075002381279873909 >>> annuity_payment(19699,0.0075,4*12) 490.21044972412648 >>> d=annuity_table(19699,490.210,0.0075,4*12) >>> t=payment_schedule_readable(d) >>> for l in t: print l ['Per.', 'Begining Bal', ' Cashflow ', ' Interest ', ' Principal ', ' Ending Bal.'] [' 0', ' 0.00', ' 19699.00', ' 0.00', ' 19699.00', ' 19699.00'] [' 1', ' 19699.00', ' -490.21', ' 147.74', ' -342.47', ' 19356.53'] [' 2', ' 19356.53', ' -490.21', ' 145.17', ' -345.04', ' 19011.50'] ... To create a table of a saving account or to see how the nominal value would have to grow with inflation, use the following: >>> t = payment_schedule_readable( annuity_table(1,0,.05,20) ) >>> for l in t: print l ['Per.', 'Begining Bal', ' Cashflow ', ' Interest ', ' Principal ', ' Ending Bal.'] [' 0', ' 0.00', ' 1.00', ' 0.00', ' 1.00', ' 1.00'] [' 1', ' 1.00', ' 0.00', ' 0.05', ' 0.05', ' 1.05'] [' 2', ' 1.05', ' 0.00', ' 0.05', ' 0.05', ' 1.10'] [' 3', ' 1.10', ' 0.00', ' 0.06', ' 0.06', ' 1.16'] ... """ if type(Pr ) <> float: Pr = float(Pr) if type(Pmt) <> float: Pmt = float(Pmt) if type(Int) <> float: Int = float(Int) if type(NPer) <> float: NPer= float(NPer) # compile series of cash flows: cf=[] cf.append(Pr) for x in range(0,NPer): cf.append(-Pmt) # compile payment schedule: return payment_schedule(cf, Int) def annuity_interest(Pr, Pmt, NPer, Prec=2): "Returns interest rate for an annuity to Prec diggits." """ The Interest Rate can not be solved for algebraically, so it must be found by trial and error. The algorithm works as follows: The lowest interest rate is : int_low = 0.00001. The highest interest rate is: int_high = Pmt*NPer/Pr -1 Compute the principal for int_low (pr_low) Compute the principal for int_high (pr_high) Enter a loop: Compute int_mid by using the fact that if the interest rate is too high, the computed principal will be lower than the given principal. numerator = int_low*(Pr-pr_high) + int_high*(pr_low-Pr) denominator = pr_high - pr_low int_mid = math.fabs(numerator/denominator) Compute Principal using int_mid: pr_mid if pr_midPr , set int_low = int_mid if rounded pr_mid == rounded Pr, exit loop, returning int_mid """ if type(Pr ) <> float: Pr = float(Pr) if type(Pmt) <> float: Pmt = float(Pmt) if type(NPer) <> float: NPer= float(NPer) int_low = 0.00001 int_high = Pmt*NPer/Pr -1.0 pr_low = annuity_principal(Pmt, int_low , NPer) pr_high = annuity_principal(Pmt, int_high, NPer) for c0 in range(0,300): # max number of tries num = int_low*(Pr-pr_high) + int_high*(pr_low-Pr) den = pr_high - pr_low int_mid = math.fabs(num/den) # print 'int: ', int_low, ' < ', int_mid ,' < ', int_high pr_mid = annuity_principal(Pmt, int_mid , NPer) # print 'Pr : ', pr_high, ' < ', pr_mid , ' < ',pr_low if pr_mid < Pr : int_high = int_mid pr_high = annuity_principal(Pmt, int_high, NPer) if pr_mid > Pr : int_low = int_mid pr_low = annuity_principal(Pmt, int_low , NPer) if round(pr_mid,Prec) == round(Pr,Prec) : return int_mid return -1 #=============================================================== # # Depreciation / Appreciation Functions # # Depreciation is a concept, whereby the purchase cost of an asset are # booked as cost over several years, in order to simulate its declining # value in economic terms. # Three generally accepted methods exist: # - Linear, depreciation in equal amounts. # - Declining Balance, where the amount declines by the same proportion. # - Use, where the amounts declines with the actual use. # Appreciation is the opposite concept, where a value is increased over the years. def depreciation_linear(PurchVal,ResVal,YearsUse,FstYear): "Calculates a table with the linear depreciation." """ PurchVal The value at which an asset was purchased and from where depreciation starts. ResVal The value at which depreciation is to end (could be 0 or any other value) In Japan, a residual value of 10% is required by tax regulations. YearsUse Number of years over which the asset is to be depreciated. FstYear The factor by which depreciation will be done in the first year. If an asset is purchased in June, depreciation in the first year may be calculated from July to Dec., thus 6/12=0.5 Return Function returns: ,,Table1 Factor :The depreciation amount for one full period: (PurchVal-ResVal)/YearsUse Control Sum:The accumulated depreciation amounts or -1 if larger than PurchVal-ResVal Table1 :A dictionary, to contain the depreciation table, with the format: ~~ Start Val Book value at the start of the deprciation period. Depreciation The amount by which the asset is depreciated End Val Start Val - Depreciation Example: In June 01 an asset is purchased for 100. Its useful life is 2 years and at the end a scrap value of 10 is expected. In 01, depreciation for 6 month (6/12=0.5) is taken: >>> f,f,t=finance.depreciation_linear(100, 10, 2, 0.5) >>> t=depreciation_readable(t,2) >>> for l in t: print l ['Yr. Beg. Value Depr. Amount End. Value '] [' 1 100.00 22.50 77.50'] [' 2 77.50 45.00 32.50'] [' 3 32.50 22.50 10.00'] """ if type(PurchVal) <> float: PurchVal = float(PurchVal) if type(ResVal) <> float: ResVal = float(ResVal) factor =(PurchVal-ResVal)/YearsUse startAmt=PurchVal ; ctrSum=0 ; Table1 = {} # First Year year=1 ; factor1 = factor * FstYear endVal = startAmt - factor1 Table1[year] = (startAmt, factor1, endVal) startAmt = endVal ctrSum = ctrSum + factor1 # Years between first and last year for year in range(2,YearsUse+1,1): endVal = startAmt - factor Table1[year] = startAmt,factor,endVal startAmt = endVal ctrSum = ctrSum+factor # Last Year if (PurchVal-ResVal) > ctrSum: # if still amounts outstanding factor2=(PurchVal-ResVal)-ctrSum year=year+1 endVal = startAmt-factor2 Table1[year] = startAmt,factor2,endVal startAmt = endVal ctrSum = ctrSum+factor2 # Check for error: if ctrSum > (PurchVal-ResVal): ctrSum=-1 return factor,ctrSum,Table1 def depreciation_decl_bal(PurchVal,ResVal,YearsUse,FstYear,FactDig=3,NumDig=2): "Declining Balance depreciation." """ Calculates a depreciation table, using the declining balance method. PurchVal The value at which an asset was purchased and from where depreciation starts. ResVal The value at which depreciation is to end (could be 0 or any other value) YearsUse Number of years over which the asset is to be depreciated. FstYear The factor by which depreciation will be done in the first year. If an asset is purchased in June, depreciation in the first year may be calculated from July to Dec., thus 6/12=0.5 FactDig Number of diggits after the comma to be used for the depreciation factor. NumDig Number of diggits to which the depreciation amount is to be rounded. Return Function returns: ,,, Factor The factor for depreciation used. Control Sum The accumulated amount of depreciation. Factor first yr.If FstYear'=1, then the factor used in the first year will be adjusted. Table A depreciation table: Starting Balance, Depreciation, Ending balance Note that any difference due to rounding, is corrected in the last year's depreciation, in order to arrive exactly at the ResVal. """ if type(ResVal) <> float: ResVal = float(ResVal) if type(PurchVal) <> float: PurchVal = float(PurchVal) Table1 = {} # Calculate depreciation factor: # Note: The n'th root out of X equals X to the power of 1/nth factor =(ResVal/PurchVal)**(1.0/YearsUse) factor =1-factor if not factor<1: return 0 factor=round(factor,FactDig) startAmt=PurchVal ; ctrSum=0 # First Year year=1 ; factor1=factor*FstYear ; factor1=round(factor1,FactDig) amt = round(startAmt*factor1,NumDig) endVal = startAmt-amt Table1[year]= [startAmt,amt,endVal] startAmt = endVal ctrSum = ctrSum+amt # Years between first and last year for year in range(2,YearsUse+1,1): amt = round(startAmt*factor, NumDig) endVal = startAmt-amt Table1[year]= [startAmt,amt,endVal] startAmt = endVal ctrSum = ctrSum+amt # Last Year: # this code will normally only be executed if FstYear <> 1 if (PurchVal-ResVal) > ctrSum: year = year+1 factor2 = round( (PurchVal-ResVal)-ctrSum , NumDig) endVal = startAmt-factor2 Table1[year]= [startAmt,factor2,endVal] TrimctrSum = ctrSum+factor2 # Adjust any rounding differences in last year: if math.fabs(ctrSum) <> (PurchVal-ResVal): #if less then re-calculate the last line of table ctrSum = ctrSum - Table1[year] [1] # correct ctrSum to year-1 Table1[year] [1]= round(PurchVal-ctrSum-ResVal , NumDig) Table1[year] [2]= Table1[year] [0] - Table1[year] [1] ctrSum = ctrSum+Table1[year] [1] if Table1[year] [2] <> ResVal: ctrSum=-2 return factor,ctrSum,factor1,Table1 def geometric_present_worth_factor(Int, Per, Rate): "Calculates PV of a linear appreciation series." """ Used to calculate the PV of a series of steadily increasing cash flows. Int : Interest rate applicable to the discount. Rate : Rate of growth of the underlying initial investment. The result of this function is a 'geometric series present worth factor'. Which multiplied with the underlying initial yields a present value. Example: >>> a,b,c,t=depreciation_decl_bal(100.0,146.41,4,1,3,2) >>> ret = depreciation_readable(t,2) >>> for l in ret: print l ['Yr. Beg. Value Depr. Amount End. Value '] [' 1 100.00 -10.00 110.00'] [' 2 110.00 -11.00 121.00'] [' 3 121.00 -12.10 133.10'] [' 4 133.10 -13.31 146.41'] [' 5 146.41 0.00 146.41'] At an 8% interest rate, the NPV of this steadily increasing cash flow can be calculated as: >>> 100*geometric_present_worth_factor(.08, 5, .1) 480.43022327414855 >>> net_present_value([0,100,110,121,133.1,146.41],.08) 480.43022327414894 """ a = compound_amount_factor(Rate,Per) b = present_worth_factor(Int, Per) if Int <> Rate : return (1.0-a*b) / (Int-Rate) else : return Per*present_worth_factor(Int, 1) def depreciation_readable(Table,Diggits,Factor=0,Factor1=0): "Converts table returned by depriciation functions into a readable form." """ Factor : CtrSum : Factor1: Table : As returned by depreciation_decl_bal() or depreciation_linear() Diggits: Number of diggits after the comma. Returns a list of lists, where every list is one line. """ ret = [] ; format = '%15.'+str(Diggits)+'f' if Factor <> 0 : ret.append(['Depreciation factor:' + str(Factor)]) if Factor <> Factor1 : ret.append(['Depreciation factor in first year:' + str(Factor1)]) ret.append(['Yr. Beg. Value Depr. Amount End. Value ']) keys = Table.keys() keys.sort() for k in keys: ret.append([('%3.0f' % k)+(format % Table[k][0])+(format % Table[k][1])+(format % Table[k][2])]) return ret #=============================================================================== # # Cashflow functions: # The following, general functions work on a series of cashflows and are # commonly used to perform investment appraisals. # Contrary to the above compound interest calculation, they appraise # cash flows, which do not rely on constant payments. # NOTE THAT THE FIRST AMOUNT IN ANY LIST OF CASHFLOWS FALLS INTO PERIOD ZERO, # meaning that its PV equals its nominal value and that interest will not # affect it. # NOTE THAT THE INTEREST RATE IS THE INTEREST RATE PER PERIOD, NOT THE APR, IF # THERE ARE SEVERAL PERIODS PER ANNUM. #=============================================================================== def net_present_value(cashflows,ir): "Calculates the net present value of a series of consecutive cashflows." """ Example: You are offered an investment where you pay 300 at the start, and receive 50 in year 2, 100 in year 3 and 150 in year 4. You can leave the money in the bank at 10% interest. >>> finance.net_present_value([0,0,50,100,150],0.1) 218.9058124445051 You calculate the NPV of the promised cash flows at the banks interest rate. You find out, that the NPV is considerably lower than the 300 you would have to pay, so you leave your money in the bank. Keep in mind that the first element in the list of cash flows is assumed to be a start-up payment, at the very beginning (period 0), which will not be discounted. Be careful, when you compare the results of this function to results of MS Excel. MS Excel's NPV function starts with period 1, discounting also the first cash flow. Consequently, in MS Excel you have to add the cash flow in period 0 to the result of the NPV(periods 1 to n). MS Excel's IRR function, however, starts with period 0, not discounting the first period. """ pers = len(cashflows) # number of periods = number of cashflows # compute series of discount factors disc = [] for p in range(0,pers): disc.append(present_worth_factor(ir, p)) # sum up present values: pv = [] for c in range(0,pers): pv.append(cashflows[c] * disc[c]) # sum up the present values return sum(pv) def net_future_value(cashflows,ir): "Future value of cashflows." """ Just like net_present_value(), except that it returns the net future value of the cash flows. """ pers = len(cashflows) # number of periods = number of cashflows # compute series of discount factors disc = [] for p in range(pers-1,-1,-1): disc.append(compound_amount_factor(ir, p)) print disc # sum up present values: fv = [] for c in range(0,pers): fv.append(cashflows[c] * disc[c]) # sum up the present values return sum(fv) def benefit_cost_ratio(cashflows,ir): "PV Benefit/Cost ratio." """ Takes all positive amounts as benefits and all negative amounts as cost and then calculates the PV of each. A benefit cost ratio lower than 1 indicates an unattractive investment. """ benefit = [] ; cost = [] # split into cost and benefit cashflow for amount in cashflows: if amount >=0: benefit.append(amount) ; cost.append(0 ) else : benefit.append(0 ) ; cost.append(amount) ben = net_present_value(benefit,ir) cst = net_present_value(cost ,ir)*(-1) if cst <> 0 : return ben/cst else : return 0 def payback_period(cashflows): "Pay back period for the investment." """ Takes a cashflow and expects the first amount to be an investment, ie. a negative amount. It will then calculate, after how many periods, the investment will be paid off. """ per = 0.0 ; total = 0.0 ; c0 = 0 # skip any leading 0 amounts. for amount in cashflows: if amount <> 0 : cashflows = cashflows[c0:] break per += 1 ; c0 += 1 for amount in cashflows: total += amount if total >= 0.0: # if we are breaking even or exceed, calculate fraction total -= amount fraction = 1 + total/amount # (total will always be negative) per -= fraction break per += 1.0 return per def repayment(cashflow, outstanding, ir): "Returns the repayment portion of a cashflow, used to repay outstanding." """ Assumes cashflow to represent a single repayment onto an amount outstanding. It will then return a tuple: outstanding * ir , cashflow - outstanding * ir , outstanding - cashflow + outstanding * ir interest due , principal repayment portion , principal outstandin after repayment Note: outstanding and cashflow must have different signs in order to represent cash outflow and cash in-flow. >>> finance.repayment(1252,-5000,.08) (-400.0, 852.0, -4148.0) A repayment of 1252 on an outstanding loan of 5000 at an interest rate of 8%, is applied 852 to principal, 400 to interest and the remaining balance is 4148. """ int = outstanding * ir pri = cashflow+(outstanding*ir) bal = outstanding*(1+ir)+cashflow return int, pri, bal def payment_schedule(cashflows, ir): "Returns a table of interest/principal payments out of a cash flow." """ cashflows is a list of cashflows in consecutive periods. The first element is assumed to fall into period 0, the second into period 1 &c. ir is an interest rate to be applied on the outstanding balance. returns a dictionary in the form: period:( tuple) (outstanding balance at beginning of the year, cashflow, interest thereon principal repayment portion of the cashflow outstanding balance at the end ) >>> pprint.pprint(finance.schedule([-5000,1252,1252,1252,1252,1252], 0.08)) """ ret = {} ; per = 0 ; outstanding = 0 for cf in cashflows: ret[per] = (outstanding,cf) + repayment(cf, outstanding, ir) outstanding = ret[per] [4] per += 1 return ret def payment_schedule_readable(sched): "Returns a human readable representation of the schdule returned by payment_schedule()." ret = [] ret.append(['Per.','Begining Bal',' Cashflow ',' Interest ',' Principal ',' Ending Bal.']) keys = sched.keys() keys.sort() totalcf=0 ; totalpr=0; totalint=0 for k in keys: l = [] l.append('%4.0f' % k) for c in range(0,5): l.append('%12.2f' % sched[k][c]) ret.append(l) totalcf += sched[k][1] totalint += sched[k][2] totalpr += sched[k][3] ret.append(['Sum:',' ','%12.2f' % totalcf,'%12.2f' % totalint,'%12.2f' % totalpr,'']) return ret # # Annual Cash Flow Analysis, # # Functions to convert cashflows into Equivalent Uniform Annual Cashflows (EUAC). # # The advantage of this method over the present worth analysis is, that when # comparing two alternatives, the number of periods do not necessarily have # to be the same. Thus, when equipment is to be replaced, under a PW analysis, # it is crucial, that the periods of use for both types of equipment are the # same. However, when comparing the annual cash flows, essentially the costs # of one year are compared, so that flexibility on the number of periods is # possible (in limits). # # Purchase something for 1000, to last 10 years. At the end no value is left. # The equivalent uniform annual cashflow could be calculated as: # >>> finance.annuity_payment(1000,0.07,10) # 142.37750272736463 # i.e. this cash flow corresponds to taking a loan an repaying it. # In all other cases, the EUAC can be calculated, be calculating the NPV # of the cost and using the annuity_payment() function to calculate the # EUAC. # Purchase somethin for 1000 to last 10 years, but at the end you expect to # sell it for 200. # NOTE THAT THE INTEREST RATE IS THE INTEREST RATE PER PERIOD, NOT THE APR, IF # THERE ARE SEVERAL PERIODS PER ANNUM. def euac_salvage_value(Inv,Salv,Int,Per): "EUAC at a salvage value." """ The EUAC can be calculated in different ways. This method calculates the EUAC of the Investment annuity_payment() and deducts the EUAC of the salvage value payment(). I.e. it deducts the saving cash flow from the cashflow for the repayment of the loan. """ euac_inv = annuity_payment(Inv,Int,Per) # installments to repay loan euac_sal = payment(Int,Per,0,Salv) # installments to save return euac_inv - euac_sal def euac_cashflows(CashFl, Int): "EUAC of a series of cash flows." """ CashFl : List of cash flows Int : Annual interest rate Returns the EUAC for a series or arbitrary cash flows, by 1. calculating the PV of the cashflows, 2. calculating the payment required to pay down that amount, annuity_payment() Example: You incurred the following maintenance cost for 5 years and wonder what constant annual payment corresponds to it: >>> finance.euac_cashflows([0,45,90,180,135,225], 0.07, 5) 129.50886645262651 Keep in mind that net_present_value() starts with period 0, as a period in which no discounting occurs. """ Int = float(Int) npv = net_present_value(CashFl, Int) per = len(CashFl)-1 return annuity_payment(npv,Int,per) def gordon_growth(CashFl, Growth, Ir): "Gordon Growth Model." """ The Gordon Grwoth Model is used to price assets, comapnies and securities. It is a very simple formula where the price of an asset is the sum of all future cash flows discounted by the required rate of return Ir, assuming a base cash flow of CashFl, that will grow constantly at a rate of Growth. A good deduction of the formula could be found on http://usurer.ubersportingpundit.com/archives/007012.html The formula is simply a short cut of calculating the FV of a constantly growing cash flow and then discounting it by the required rate of return. """ return CashFl* (1+Growth) / (Ir-Growth) #============================================================================================================== # # Internal Rate of Return # # This section deals with internal rate of return analysis. # # NOTE THAT THE INTEREST RATEs ARE THE INTEREST RATE PER PERIOD, NOT THE APR, IF # THERE ARE SEVERAL PERIODS PER ANNUM. #============================================================================================================== def irr_analysis(cashflows, externalrr, msg): "Internal Rate of Return of a given cash flow and an external rate of return." """ While the calculation of an IRR is relatively simple, if there is only one sign change in a cashflow, the calculation gets much more complex in situations, where investments are followed by returns and subsequent new investments and so on. In this case, there will be several sign changes representing the alternating in- and out-flow of cash. This routine returns a detailed text analysis in msg[], making it possible to follow the calculation in detail and to interpret the results. Returns a positive irr or -1, if no irr could be located. cashflows: A series of numbers. Positive numbers represent cash receipts, negative numbers represent cash outflows. exteranlrr: External rate of returns, at which excess outflows from a project can be invested, until they flow back into the project. msg : A list, which will be filled with diagnostic messages in list form. The list will contain one list entry per message/line. These messages constitute the real analysis. """ s = '' # universal dummy string msg.append('=== Internal Rate of Return (IRR) Analysis. ===') msg.append('') msg.append('An IRR to be found for the following cash flow:') per = 0 for ele in cashflows: msg.append( ('%4.0f' % per) + ('%12.2f' % ele) ) per += 1 msg.append('An external rate of return (ERR) was specified to be:'+str(externalrr) ) msg.append('At this ERR, the cash flow has a NPV of:'+ '%15.2f' % net_present_value(cashflows,externalrr) ) # compute number of sign changes in the cash flow: sign_ch = sign_changes(cashflows) # if no sign change: if sign_ch == 0: msg.append('No sign changes in the cash flow') total = sum(cashflows) msg.append('Total of cash flows:' + str(total)) if total < 0: msg.append('There are only costs, no IRR is possible.') msg.append('--End--') return -1 if total == 0: msg.append('The sum of the cash flows is 0. There can not be a IRR.') msg.append('--End--') return -1 if total > 0: msg.append('There are only benefits, no IRR is possible.') msg.append('--End--') return -1 if sign_ch == 1: msg.append('One sign change in cash flows') total = sum(cashflows) msg.append('Total of cash flows:' + str(total)) if total < 0: msg.append('Cost exceed benefits! (Borrowing situation?)') msg.append('--End--') return -1 if total == 0: msg.append('Cost equal benefits.') msg.append('--End--') return -1 if total > 0: msg.append('Benefits exceed costs. There must be a single positive IRR:') ret = irr(cashflows) msg.append('IRR = ' + str(ret) ) msg.append('A payment schedule:') l = payment_schedule(cashflows, ret) l = payment_schedule_readable(l) for ele in l: msg.append(s.join(ele)) if ret <= externalrr: msg.append('The IRR is smaller than /equal to the ERR ! Invest externally !') return ret # now come the complicated cases: more than one sign change in the # series of cash flows: # Accumulated Cash Flow Sign Test: msg.append('More than one sign change in cash flow, in total:' +str(sign_ch)+' changes') msg.append(' The Cash Flow Rule of Signs, based on Descartes rule, states that') msg.append(' there may be as many poistive values for the IRR as there are sign changes in the cash flow.') msg.append(' However, if there is only one sign change in the accumulated cash flows.') msg.append(' and the total of accumulated cash flows is larger than 0, there will be only one IRR.') msg.append(' According to Merret and Sykes, where there are two or more sign changes in the') msg.append(' cash flow, and a substantial outlay near the end of the life of a project,') msg.append(' the result may be one or more positive IRR.') msg.append('') msg.append('Apply accumulated cash flow sign test:') msg.append('Per. Cash Flow Accumulated') acc_cashflows = [] ; total = 0 ; per = 0 for ele in cashflows: total += ele acc_cashflows.append(total) msg.append( ('%4.0f' % per) + ('%12.2f' % ele) + ('%12.2f' % total) ) per += 1 sign_ch = sign_changes(acc_cashflows) msg.append('Number of sign changes in accumulated cash flow:' + str(sign_ch) ) if sign_ch == 0: msg.append('No rate of return possible with 0 sign change in accumulated cashflows') return -1 if total > 0: msg.append('Total of accumulated cash flows > 0, hence positive irr should exist.') if sign_ch == 1: msg.append('Since there is exactly one sign change in the accumulated cash flows,') msg.append(' according to Carl J. Nordstrom, a positive IRR must exist.') ret = irr(cashflows) if ret == -1: msg.append('No single IRR located! This indicates that several IRR exist.') else: msg.append('IRR = '+str(ret) ) msg.append(' Validated IRR by net investment throughout life of project.') msg.append(' A net investment requires ending balance to be zero or negative throughout the project.') # make a schedule of the payments: l =payment_schedule(cashflows,ret) l_r=payment_schedule_readable(l) s = '' for ele in l_r: msg.append(s.join(ele)) keys=l.keys() keys.sort() end_net_inv=l[keys[-1]] [4] msg.append('The final net investment is:'+ ('%6.2f' % end_net_inv) ) if -1 0.1: # act_ir is too low low_ir = act_ir elif outstanding < -0.1: # act_ir is too high high_ir = act_ir else: return act_ir,ret return -1,ret def irr(cashflows, intr_low=0, intr_high=5): "Simple internal rate of return calculation." """ This algorithm works well, if there is only one sign change in the list of cash flows provided, but it can yield potentially wrong results if there are more than one sign changes and it will yield useless results if there are no sign changes. For an extensive analysis, use irr_analysis(). irr() tries to find a irr, where the discounted sum of all cashflows is close to 0 (0.1',intr_mid,'>',intr_low,'=>',total if total < -0.1 : # intr_mid is too high intr_high = intr_mid elif total > 0.1: # intr_mid is too low intr_low = intr_mid else : return intr_mid return -1 def repayment_dual_ir(cashflows,iror,eror): "Compute a payment schedule using two interest rates." """ Computes a repayment schedule, whereby iror is applied to negative amounts (ie. investments) and eror is applied to positive amounts (ie. repayments from the investments). Since money received will usually be invested at an external rate of return, which is different from the internal rate of return of the project. Returns a tuple: (last outstanding balance, dictionary) The dictionary has the same for as returned by repayment_schedule() except that the interest rates are appended: ret[period] = (Cashflow, Beginning balance, Interest, Principal, Ending Balance, Interest rate, '') """ ret = {} ; per = 0 ; outstanding = 0 for cf in cashflows: if outstanding <=0: ir = iror else: ir = eror ret[per] = (cf, outstanding) + repayment(cf, outstanding, ir) + (ir,'') outstanding = ret[per] [4] per += 1 return outstanding, ret def sign_changes(cashflow): "Returns the number of sign changes in the list of values cashflow." sig_old,sign_change = 0,0 for val in cashflow: if val > 0: sig = 1 elif val < 0: sig = -1 else: continue # zero is neutral, no sign change ! if sig_old == 0 : sig_old = sig if sig_old <> sig : sign_change +=1 sig_old = sig return sign_change #============================================================================================================== # # Comprehensive Cashflow Analysis: # # Accepts one cashflow and issues a report, that contains all analysis methods which can normally # be performed on one single cash flow. # #============================================================================================================== def cashflow_analysis(CashFl, Ir, Er): "Analyze cashflow in multiple ways." """ Arguments: Cashflow: A list of cashflows, first value falls in period 0 Ir : interest rate to be used for all discounts Er : external interest rate to be used for the irr analysis Returns a list with output messages and a dictionary with values. """ msg = [] val = {} val['EUAC'] = euac_cashflows(CashFl, Ir) val['NPV' ] = net_present_value(CashFl, Ir) val['IRR' ] = irr(CashFl) val['NFV' ] = net_future_value(CashFl, Ir) val['PBP' ] = payback_period(CashFl) val['BCR' ] = benefit_cost_ratio(CashFl,Ir) msg.append('Analyzing Cashflow:') msg.append( ';'.join( map( (lambda x :'%0.2f' % x), CashFl) ) ) msg.append('Major Economic Analysis Techniques: ===' ) msg.append('Equivalent Uniform Annual Cashflow:' + '%0.2f' % val['EUAC'] ) msg.append('Net Present Value :' + '%0.2f' % val['NPV' ] ) msg.append('IRR (simple analysis !!!) :' + '%0.2f' % val['IRR' ] ) msg.append('Net Future Value :' + '%0.2f' % val['NFV' ] ) msg.append('Payback takes place after :' + '%0.2f' % val['PBP' ]+' periods.') msg.append('Benefit/Cost Ratio :' + '%0.2f' % val['BCR' ] ) val['IRR' ] = irr_analysis(CashFl, Er, msg) return msg, val