REBOL [ Title: "Cash Flow Functions" File: %cash-flow-functions.r Purpose: "Provide some financial functions." Author: "Brett Handley" Date: 23-Apr-2002 Version: 0.1.1 History: [ 0.1.1 [23-Apr-2002 "Work around 0 <> $0. Allow IRR to handle money!" "Brett Handley"] 0.1.0 [21-Apr-2002 "Initial version." "Brett Handley"] ] Comment: { (0) This is NOT rigoursly tested. Please send me any bugs you find and preferrably solutions to them. :^) (1) Throws in a solve function which could be used to simulate "Goal Seek". (2) The input to the DCF function has been chosen with a database application in mind - that is the amounts may not be ordered in the block and there could be other fields in the records. I'm not sure yet if I'll change this later to a more simple raw format. (3) The DCF function employs some "self destroying functions". I did this as a performance enhancement in the event the function needs to process many records. Note that DCF will be called iteratively by someone wanting to do an IRR. } Rights: {Copyright (c) 2002 Brett Handley Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, subrights, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.} ] npv: function [ {Returns the net present value for a sequence of periodic cash flows. Note: (1) The date of the first amount is considered the value date. (2) The datatype of the result is the same as the first amount however the currency part is ignored. (3) Amounts should be decimal! or money! only.} rate [decimal!] amounts [block!] "Cash flows - first is in today's money." ] [cf sum factor amt] [ if empty? amounts [return 0.00] sum: 0.00 factor: 1 + rate cf: tail amounts until [ if money? amt: first cf: back cf [amt: second amt] sum: add divide sum factor amt same? cf amounts ] make first amounts sum ] dcf: function [ {Returns the net present/discounted value for a sequence of cash flows. ^- Note: (1) Each cash-flow must be either an object or a ^- context specification. Fields in use are DATE and AMOUNT ^- although they are not mandatory. ^- (2) The datatype of the result is the same as the first amount. ^- (3) The value-date is defaulted to the first date encountered. ^- (4) If the cash flow has no date - value date is assumed. ^- (5) If the cash flow element has no amount - it is skipped without error.} rate [decimal!] cash-flows [block!] "Cash flows - first is in today's money." /as-at value-date "The date to which all cash flows should be discounted to." /days ann-days [integer! decimal!] "Number of days considered to be in a year (Default 365)." /spec "Returns result in context specification format." ][sum factor amt date-check sum-check][ if not days [ann-days: 365] factor: 1 + rate date-check: do [ ; Create self destroying function - performance enhancement. func[flow][ if all [none? value-date in flow 'date][ value-date: flow/date date-check: none ] ] ] sum-check: do [ ; Create self destroying function - performance enhancement. func[flow][ if all [none? sum in flow 'amount][ sum: make flow/amount 0 sum-check: none ] ] ] repeat flow cash-flows [ if not object? flow [flow: context flow] date-check flow if in flow 'amount [ sum-check flow amt: flow/amount if in flow 'date [ amt: factor ** ((value-date - flow/date/date) / ann-days) * amt ] sum: sum + amt ] ] either spec [ compose [date: (value-date) amount: (sum)] ][sum] ] irr: function [ {Returns the internal rate of return for a sequence of amounts. ^- Note: (1) The first amount represents the value date. ^- (2) The amounts are are considered to be periodic net cash flow amounts. ^- If you want to calculate on non-periodic cash flows then you must ^- supply a function that takes rate as an argument and ^- returns the NPV for your amounts.} cash-flow [block! function!] "A block of amounts or a function that returns NPV for given rate." /guess "Initial guess interval" guess1 [decimal!] guess2 [decimal!] ][npv-func][ if not guess [guess1: 0.10 guess2: 1.00] if all [block? :cash-flow lesser? length? cash-flow 2] [return none] npv-func: either function? :cash-flow [:cash-flow][ func [rate][npv rate cash-flow] ] solve/limit :npv-func guess1 guess2 0.00000001 0.00000001 100 ] solve: function [ "Solve for x such that F(x)=0 using secant iteration." F "Function to solve." [any-function!] x1 "First initial point." x2 "Second initial point." x-tol "Input point tolerance." f-tol "Output value tolerance." /limit max-tries "Maximum number of iterations." ][x3 f1 f2 f3 slope tries zero][ if not limit [max-tries: 100] f1: f x1 f2: f x2 x-tol: make x1 x-tol ; Make datatypes compatible. f-tol: make f1 f-tol ; Make datatypes compatible. if lesser? abs f1 f-tol [return x1] if lesser? abs f2 f-tol [return x2] zero: make f1 0 ; Workaround 0 <> $0. tries: 0 while [greater? abs (x2 - x1) x-tol] [ if tries > max-tries [return none] slope: (f2 - f1) / (x2 - x1) if slope = zero [return none] x3: x2 - (f2 / slope) f3: f x3 if (abs f3) <= f-tol [break] x1: x2 f1: f2 x2: x3 f2: f3 tries: tries + 1 ] x3 ] pmt: func [ "Periodical Payment (e.g Standard Mortgage payment)." principal [decimal! money!] rate [decimal!] "Rate (per period)." periods [integer! decimal!] "Number of periods." ][ Principal * ( rate / ( 1.0 - ((1.0 + rate) ** (-1 * periods)) ) ) ] dcf-irr-example: does [ amounts: [ [date: 31-dec-2000 amount: -100] [date: 31-dec-2001 amount: 110] ] irr func [rate][dcf rate amounts] ]