Option Explicit
'===============================================================================
' Function: PIRR (Percentage-based internal rate of return)
' Purpose: To Calculate the internal rate of return (IRR)/compound annual
' growth rate (CAGR) of a fund or index when intermediate returns are
' only known as percentages as opposed to values. This function creates
' and calculates on the intermediate values necessary for compounding.
' Arguments: - FundOrIndex: Whether the calculation will be made on a fund or
' an index.
' - Id: The ID number of the fund or index. If this is not a cell
' reference, it must be enclosed with quotation marks.
' - PeriodLength: The length of the period over which the return
' will be calculated given in months. Use -1 to calculate the annual
' return since inception; use -2 to calculate year-to-date
' - PeriodEndDate: The last day of the period for which the return
' is begin calculated. This will be given as the last day of the
' month.
' Generic form: PIRR(FundOrIndex, Id, PeriodLength, PeriodEndDate)
' Example: PIRR(fund, 47, 36, DATE(2007,12,31))
' Requirements: - Named ranges, "FundReturnDate," and "IndexReturnDate," which
' contain monthly month-end dates
' - Named ranges, "FundReturn," and "IndexReturn"
' - Named ranges, "FundId," and "IndexId"
' Author: RLB
' Last modified: October 2008
'===============================================================================
Public Function PIRR(FundOrIndex As String, _
Id As String, _
PeriodLength As Double, _
PeriodEndDate As Date) _
As Variant
Dim FutureValue As Double ' The fabricated future value for use with RATE
Dim IdRange As String
Dim InceptionDate As Variant
Dim InceptionDateLocation As String ' The cell address where the inception date is located; This could be used for funds and indices, but it is only used for indices as it is slightly indirect
Dim InceptionLength As Date
Dim InitialValue As Double ' An arbitrary values that serves as a starting point for RATE
Dim PeriodStartDate As Date
Dim ReturnDateRange As String
Dim ReturnRange As String
Dim WorkingDate As Date ' In the loop, this represents the date currently calculated upon
Dim WorkingReturn As Double
' Assign appropriate (fund or index) variables -----------------------------
Select Case FundOrIndex
Case "fund"
If Application.Evaluate("SUMPRODUCT(--(FundId=" & Id & "))") = 0 Then
PIRR = "#FUND ID NOT FOUND!"
Exit Function
Else
IdRange = "FundId"
ReturnDateRange = "FundReturnDate"
ReturnRange = "FundReturn"
End If
Case "index"
If Application.Evaluate("SUMPRODUCT(--(IndexId=" & Id & "))") = 0 Then
PIRR = "#INDEX ID NOT FOUND!"
Exit Function
Else
IdRange = "IndexId"
ReturnDateRange = "IndexReturnDate"
ReturnRange = "IndexReturn"
End If
Case Else
PIRR = "#SELECT FUND OR INDEX!"
Exit Function
End Select
' Check period end date
If Application.Evaluate("SUMPRODUCT(--(" & IdRange & "=" & Id & "),--(" & ReturnDateRange & "=" & CLng(PeriodEndDate) & "))") = 0 Then
If PeriodLength = 3 Then
PIRR = "–" ' The previous quarterly periods are the only periods with a end date that is different than the explicitly set end date. That is, data might not be available for new funds
Exit Function
Else
PIRR = "#END DATE NOT FOUND!"
Exit Function
End If
End If
PeriodStartDate = Application.WorksheetFunction.EoMonth(PeriodEndDate, -PeriodLength)
PeriodStartDate = DateSerial(Year(PeriodStartDate), Month(PeriodStartDate), Day(PeriodStartDate))
' Inception date
If FundOrIndex = "fund" Then
'InceptionDate = Application.Evaluate("SUMPRODUCT(--(MarketValueByFundId=" & Id & "),InceptionDate)")
InceptionDate = Application.Evaluate("OFFSET(INDEX(MarketValueByFundId,1,1),MATCH(" & Id & ", MarketValueByFundId,0)-1,4)")
If InceptionDate > PeriodStartDate Then
'PIRR = "#NOT INVESTED LONG ENOUGH!"
PIRR = "–" ' Not the error message but a place-holder so the formula can be left in the cell
Exit Function
End If
End If
Select Case PeriodLength
' Since inception ------------------------------------------------------
Case -1 ' A value of -1 for PeriodLength is the flag to calculate it since inception
If FundOrIndex = "index" Then
InceptionDateLocation = Application.Caller.Address
InceptionDate = Worksheets("Performance").Range(InceptionDateLocation).Offset(0, 1) ' The inception date resides in the cell one column to the right of the annualized-since-inception return
End If
InceptionLength = (PeriodEndDate - InceptionDate) / 365.25 ' Return InceptionLength in years and also account for leap years
If InceptionLength < 1 Then ' If the fund has not been invested for a whole year, then pretend it has been a whole year
InceptionLength = 1
End If
PeriodLength = InceptionLength * 12 ' PeriodLength, which will be used by RATE(), is now the length of the entire investment period and back in units of months
PeriodStartDate = InceptionDate
' Year-to-date ---------------------------------------------------------
Case -2
PeriodStartDate = DateSerial((Year(PeriodEndDate) - 1), 13, 0) ' That last day (Decemeber 31) of the previous year
PeriodLength = 12 ' This is fixed like a 1 yr (12 mo) period
' Regular periods ------------------------------------------------------
End Select
' Check period start date ' This part is after the "Since inception" part in case the inception date is used as a start date
PeriodStartDate = Application.WorksheetFunction.EoMonth(PeriodStartDate, 1) ' The first return value will be at the end of the first month--this might be the first date the fund has a value, especially for sector totals
If Application.Evaluate("SUMPRODUCT(--(" & IdRange & "=" & Id & "),--(" & ReturnDateRange & "=" & CLng(PeriodStartDate) & "))") = 0 Then
Select Case FundOrIndex
Case "fund"
PIRR = "#START DATE NOT FOUND!"
Case "index"
PIRR = "–" ' If the start date is not found, then the index has not been around that long and that period can be dismissed
End Select
Exit Function
End If
'---------------------------------------------------------------------------
' Calculate the future value with the performance of past months for each
' month until the period is fulfilled
'---------------------------------------------------------------------------
WorkingDate = PeriodStartDate
InitialValue = 100 ' An arbitrary value
FutureValue = InitialValue
Do While WorkingDate <= PeriodEndDate
If Application.Evaluate("SUMPRODUCT(--(" & IdRange & "=" & Id & "),--(" & ReturnDateRange & "=" & CLng(WorkingDate) & "))") = 0 Then ' Make sure there is a record for that date' This is done explicitly and separate from the return because a DNE in the main SUMPRODUCT() will just return a zero, which is a valid return value
PIRR = "#NO DATA " & CStr(WorkingDate) & "!"
Exit Function
Else
WorkingReturn = Application.Evaluate("SUMPRODUCT(--(" & IdRange & "=" & Id & "),--(" & ReturnDateRange & "=" & CLng(WorkingDate) & ")," & ReturnRange & ")") ' Look-up the correct return value based on the unique combination of Id and date ("=SUMPRODUCT(--(Symbols=Id),--(Dates=WorkingDate),Returns)")
FutureValue = FutureValue * (1 + (WorkingReturn / 100)) ' Adjust the value by the return (given as a percentage (e.g., A value of 25 is given, which means, "twenty-five percent.")
WorkingDate = Application.WorksheetFunction.EoMonth(WorkingDate, 1) ' Advance by one month for the next loop iteration
End If
Loop
' The final calculation using the RATE function ----------------------------
If PeriodLength < 12 Then
PeriodLength = 12
End If
PIRR = Application.WorksheetFunction.Rate((PeriodLength / 12), 0, (-1 * InitialValue), FutureValue) * 100 ' The pv argument (InitalValue here) needs to be negative (I don't understand why)
If PIRR > -0.0000000001 And PIRR < 0.0000000001 Then ' If the theoretical result of RATE() is 0, RATE() will still produce a non-zero result ~1E-14 or 1E-15; using the Round expression preserved the negative sign (which we don't want because we want a true zero)
PIRR = 0
End If
End Function