XIRR in VBA

  • Thread starter Thread starter Tim Tabor
  • Start date Start date
T

Tim Tabor

On 2/22, Myrna Larson posted the following message. What is the proper
etiquette for requesting this implementation, or perhaps Myrna might be
persuaded to post the code?

"BTW, if the only function you need from the ATP is XIRR, I have written my
own
version, which gives the same results and runs quite a bit faster."

Thank you.
 
This quite long -- about 10K -- because I've tried to mimic the ATP-XIRR
function WRT to accepting either ranges or literal arrays as arguments.

It does not have XIRR's limitation that the 1st date must be the earliest one,
and it ignores both blanks and text in the lists (AIR, XIRR chokes on text).

I don't think there are any lines that are more than 79 characters, but beware
of possible problems due to line-wrapping in the message.

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Option Explicit

Const MaxChange As Double = 0.00000001
Const MaxTries As Long = 100

Enum ArrayDims '07/26/2003
NotArray = 0
SingleDim = 1
Horizontal = 2
Vertical = 3
Rectangular = 0
End Enum

Function XXIRR(TransactionAmounts As Variant, TransactionDates As Variant, _
Optional Guess As Double = 0.1) As Variant

'replacement for XIRR in ATP

Dim N As Long
Dim Dates() As Double
Dim Amounts() As Double

N = PreprocessTransactions(TransactionAmounts, TransactionDates, _
Amounts(), Dates())

If N = 0 Then
XXIRR = CVErr(xlErrRef)
Else
XXIRR = DoXIRR(Amounts(), Dates(), Guess)
End If

End Function 'XXIRR

Function XXNPV(Rate As Double, TransactionAmounts As Variant, _
TransactionDates As Variant) As Variant

'replacement for XNPV in ATP

Dim N As Long
Dim Dates() As Double
Dim Amounts() As Double

If Rate <= -1 Then
XXNPV = CVErr(xlErrNum)
Else
N = PreprocessTransactions(TransactionAmounts, TransactionDates, _
Amounts(), Dates())
If N = 0 Then
XXNPV = CVErr(xlErrRef)
Else
XXNPV = DoXNPV(Rate, Amounts(), Dates())
End If
End If

End Function 'XXNPV

Private Function DoXIRR(TxnAmts() As Double, TxnDates() As Double, _
Optional Guess As Double = 0.1) As Variant
'02/23/2005
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'Newton-Raphson method:
' Given PV a function of X, determine the value of X
' such that SUM[PV] = 0, using iteration.
' dPVdX = derivative of PV with respect to X
' NB: if X = 1 + Rate, dX/dRate = 1
'
' Calculate PV and dPVdX using an arbitrary initial estimate of X.
' Change X by -PV/dPVdX and calculate again.
' Repeat until X changes by less than some arbitrary small amount.

' PV of an individual cash flow = CashFlow(i) / [(1 + Rate)^(YearFrax(i))]
' = CashFlow(i) * [(1 + Rate)^(-YearFrax(i))]
' PVSum = SUM[individual cash flows]
' Let X = 1 + Rate
' Let t(i) = -YearFrax(i)
' PV(i) = CashFlow(i) * X^(t(i))
' dPVSum/dX = SUM[dPV/dX]
' dPV/dX = CashFlow(i) * t(i) * X^(t(i)- 1)
' = CashFlow(i) * t(i) * X^(t(i)) / X
' = CashFlow(i) * X^(t(i)) * t(i) / X
' = PV(i) * t(i) / X
' dPVSum/dX = 1/X * SUM[PV(i) * t(i)]
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Dim InitialRate As Double
Dim SignOfNetCashFlows As Long
Dim t() As Double
Dim X As Double
Dim N As Long
Dim FoundSolution As Boolean
Dim Try As Long
Dim SumPVs As Double
Dim dPVdX As Double
Dim i As Long
Dim PV As Double
Dim Delta As Double

CheckGuess TxnAmts(), Guess, InitialRate, SignOfNetCashFlows
If SignOfNetCashFlows = 0 Then
DoXIRR = 0
Exit Function
End If

'calculate -t for each transaction just once
'NB: array t() contains values of -t, not +t, so no
'need to use -t(i) when calculating PV and dPVdX

DatesToT TxnDates(), t()

X = 1 + InitialRate
N = UBound(TxnAmts)
FoundSolution = False
Try = 0

Do
SumPVs = 0
dPVdX = 0

For i = 1 To N
PV = TxnAmts(i) * (X ^ t(i))
SumPVs = SumPVs + PV
dPVdX = dPVdX + PV * t(i)
Next i

'finish calculation of derivative
dPVdX = dPVdX / X

'calculate size of change in x for next try
Delta = SumPVs / dPVdX

'check that new X will be > 0; if not,
'decrease X by 50% instead
If X - Delta <= 0 Then Delta = X / 2

If Abs(Delta) < MaxChange Then
FoundSolution = True
Else
X = X - Delta
End If

Try = Try + 1

Loop Until FoundSolution Or (Try = MaxTries)

If FoundSolution Then
X = X - 1 'X = 1 + Rate: Rate = X - 1
If Sgn(X) <> SignOfNetCashFlows Then X = -X
DoXIRR = X
Else
DoXIRR = "Could not determine rate!"
End If
End Function 'DoXIRR

Private Function DoXNPV(Rate As Double, _
TxnAmts() As Double, TxnDates() As Double) As Variant
Dim t() As Double
Dim X As Double
Dim SumPV As Double
Dim Txn As Long

DatesToT TxnDates(), t()
X = 1 + Rate
SumPV = 0
For Txn = 1 To UBound(TxnAmts)
SumPV = SumPV + TxnAmts(Txn) * (X ^ t(Txn))
Next Txn
DoXNPV = SumPV

End Function 'DoXNPV

Private Function PreprocessTransactions(OrigAmts As Variant, _
OrigDates As Variant, TxnAmts() As Double, TxnDates() As Double) As Long
'07/26/2003: modify per changes in StandardizedArray function
'move data from worksheet ranges (vertical or horizontal) or literal arrays
'to 2-dim VBA vertical arrays, then remove blanks
'return number of transactions

Dim Amt As Double
Dim Dt As Double
Dim i As Long
Dim j As Long
Dim N As Long
Dim vaTxnAmts As Variant
Dim vaTxnDates As Variant

PreprocessTransactions = 0

'a/o 7/26/2003 StandardizedArray returns vertical array
N = StandardizedArray(OrigDates, vaTxnDates, True)
i = StandardizedArray(OrigAmts, vaTxnAmts, False)
If N = 0 Or i <> N Then Exit Function

ReDim TxnDates(1 To N)
ReDim TxnAmts(1 To N)

j = 0
For i = 1 To N
Dt = Val(vaTxnDates(i, 1))
If Dt <> 0 Then
Amt = Val(vaTxnAmts(i, 1))
If Amt <> 0 Then
j = j + 1
TxnDates(j) = Dt
TxnAmts(j) = Amt
End If
End If
Next i

Select Case j
Case 0
ReDim TxnDates(0)
ReDim TxnAmts(0)
Case 1 To N - 1
ReDim Preserve TxnDates(1 To j)
ReDim Preserve TxnAmts(1 To j)
End Select

PreprocessTransactions = j

End Function 'PreprocessTransactions

Private Sub CheckGuess(Amounts() As Double, UsersGuess As Double, _
NewGuess As Double, Sign As Long)
'adjust Guess to ensure that (a) it's the same sign as the net cash flows,
'(b) is > -1, (c) is not 0;
'also returns the sign of the net cash flows

Sign = Sgn(ArraySum(Amounts()))

If Sign = 0 Then
'true rate is 0
NewGuess = 0

ElseIf UsersGuess <> 0 Then
'we're here if sign of cash flows <> 0 and guess <> 0
'ensure that guess and net cash flows have same sign
If Sgn(UsersGuess) = Sign Then
NewGuess = UsersGuess
Else
NewGuess = -UsersGuess
End If

'correct guess <= -1
If NewGuess <= -1 Then NewGuess = -1 + MaxChange * 10

Else
'we're here if sign of cash flows <> 0 and guess = 0
NewGuess = 0.1 * Sign
End If
End Sub 'CheckGuess

Private Sub DatesToT(TxnDates() As Double, TimeInterval() As Double)
Dim j As Long
Dim BaseDate As Double
Dim N As Long

'get earliest date
j = LocateMin(TxnDates())
BaseDate = TxnDates(j)

'calculate time inverval in years for each transaction
'NB: result is always <= 0

N = UBound(TxnDates(), 1)
ReDim TimeInterval(1 To N)
For j = 1 To N
TimeInterval(j) = (BaseDate - TxnDates(j)) / 365
Next j

End Sub 'DatesToT

Private Function StandardizedArray(OrigArg As Variant, _
NewArray As Variant, ConvertDates As Boolean) As Long
Dim Ary As Variant
'07/26/2003: always return a vertical array

'convert original argument (range or Variant()),
'to 2 dimension array in variant, 1-based,
'with dates converted to doubles

StandardizedArray = 0

Select Case TypeName(OrigArg)
Case "Range"
If OrigArg.Cells.Count > 1 Then
StandardizedArray = MakeTwoDimArray(OrigArg.Value2, NewArray, False)
End If
Case "Variant()"
StandardizedArray = _
MakeTwoDimArray(OrigArg, NewArray, ConvertDates)
End Select

End Function 'StandardizedArray

Private Function MakeTwoDimArray(OldArray As Variant, _
NewArray As Variant, ConvertDates As Boolean) As Long
'07/26/2003: return num rows in NewArray instead of TRUE/FALSE

'OldArray is known to be a Variant(), either a
'range used in an array formula or an array constant;
'if from a range, it's base 1, 2 dim, which is OK;
'if from a literal array, it's base 0, usually 1 dim;
'convert the latter to base 1, 2 dim
'optionally convert date data types to doubles

Dim i As Long
Dim j As Long
Dim k As Long
Dim N As Long
Dim SourceRow As Long

N = 0

Select Case ArrayShape(OldArray)
Case SingleDim
'convert to 2-dim vertical, base 1
j = LBound(OldArray)
k = UBound(OldArray)
ReDim NewArray(1 To k - j + 1, 1 To 1)
For i = j To k
N = N + 1
NewArray(N, 1) = OldArray(i)
Next i

Case Horizontal
'transpose to vertical, base 1
j = LBound(OldArray, 2)
k = UBound(OldArray, 2)
SourceRow = LBound(OldArray, 1)
ReDim NewArray(1 To j - k + 1, 1 To 1)
For i = j To k
N = N + 1
NewArray(N, 1) = OldArray(SourceRow, i)
Next i

Case Vertical
'convert to base 1 prn
j = LBound(OldArray, 1)
k = UBound(OldArray, 1)
If j = 0 Then
ReDim NewArray(1 To k - j + 1, 1 To 1)
For i = j To k
N = N + 1
NewArray(N, 1) = OldArray(i, 0)
Next i

Else
N = k
NewArray = OldArray
End If

End Select

If (N <> 0) And ConvertDates Then
For i = 1 To N
If IsDate(NewArray(i, 1)) Then
NewArray(i, 1) = CDbl(CDate(NewArray(i, 1)))
End If
Next i
End If

MakeTwoDimArray = N

End Function 'MakeTwoDimArray

Private Function ArrayShape(vaArray As Variant) As ArrayDims
'07/26/2003: use Enum for return values
Dim R As Long
Dim C As Long

'vaArray is known to be Variant(), no need to check that

R = UBound(vaArray, 1)
On Error Resume Next
C = UBound(vaArray, 2)

If Err.Number <> 0 Then
ArrayShape = SingleDim

Else
On Error GoTo 0

'it's 2-dim: check # rows and # cols
R = R - LBound(vaArray, 1) + 1
C = C - LBound(vaArray, 2) + 1

If (R = 1 And C > 1) Then
ArrayShape = Horizontal

ElseIf (R > 1 And C = 1) Then
ArrayShape = Vertical

Else
ArrayShape = Rectangular

End If

End If

End Function 'ArrayShape

Private Function ArraySum(dArray() As Double) As Double
Dim S As Double
Dim i As Long

S = 0
For i = LBound(dArray) To UBound(dArray)
S = S + dArray(i)
Next i
ArraySum = S

End Function 'ArraySum

Private Function LocateMax(dArray() As Double) As Long
Dim Lo As Long
Dim Where As Long
Dim i As Long

Lo = LBound(dArray)
Where = Lo
For i = Lo + 1 To UBound(dArray)
If dArray(i) > dArray(Where) Then Where = i
Next i
LocateMax = Where

End Function 'LocateMax

Private Function LocateMin(dArray() As Double) As Long
Dim Lo As Long
Dim Where As Long
Dim i As Long

Lo = LBound(dArray)
Where = Lo
For i = Lo + 1 To UBound(dArray)
If dArray(i) < dArray(Where) Then Where = i
Next i
LocateMin = Where

End Function 'LocateMin
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Similar Threads


Back
Top