Dividing matrices using a VBA function - excel

Very new to VBA so apologize if it's a really simple question. This is finance/ portfolio management related.
I would like to create a VBA function that can give me the Global Minimum Variance Portfolio (GMVP) weights of a set of assets with just the assets' variance-covariance matrix.
I'm able to key in this formula directly into the cells on Excel to get the weights: =MMULT(MINVERSE(S),Ones)/SUM(MMULT(MINVERSE(S),Ones)) where S is the variance-covariance matrix and ones is a column matrix containing "1"s
There's no issues with mapping the dimensions of the matrices (if S is 5x5 then ones will be 5x1)
I've been trying to make a VBA function so that I don't have to type the lengthy formula every time to find the GMVP. Here's the code I have. Before this I also tried just writing everything in a really long single line, using "/" to divide (didn't work)
I wonder if it is because we can't use "/" to divide matrices in VBA? Is there a way to make the formula into a function?
Thank you!
Function GMVPcol(S As Range, Ones As Range) As Range
Dim num As Range
Dim dem As Range
num = Application.WorksheetFunction.MMult(Application.WorksheetFunction.MInverse(S), Ones)
dem = Application.WorksheetFunction.Sum(Application.WorksheetFunction.MMult(Application.WorksheetFunction.MInverse(S), Ones))
GMVPcol = Application.WorksheetFunction.MMult(num, Application.WorksheetFunction.MInverse(dem))
End Function

If you simplify your formula extremely, to something like this:
then, this is the correct way to represent it as a VBA function and call it in Excel:
Public Function SimpleMultiplication(matrix1 As Range, matrix2 As Range) As Variant
SimpleMultiplication = WorksheetFunction.MMult(matrix1, matrix2)
End Function
The function returns a variant, not a range. And in your code, make sure to always use the keyword Set, when an object of type range is to be assigned. In general, if you want to pass through a middle range and assign it, then it is better to work with an array and take the numbers from there - Creating an Array from a Range in VBA

Related

MS-Excel: Retrieve Row Number where VBA Function is located

I hope I'm asking this in the correct forum:
I'm writing a UDF in VBA for MS-Excel; it basically builds a status message for the transaction on that row. It steps through a series of IF statements, evaluating cell values in different columns FOR THAT ROW.
However, this UDF will reside in multiple rows. So it might be in C12, C13, C14, etc. How would the UDF know which row to use? I'm trying something like this, to no effect
Tmp_Row = Application.Evaluate("Row()")
which appears to return a null
What am I missing here ?
Thanking everyone in advance
Application.Caller is seldom used, but when a UDF needs to know who called it, it needs to know about Application.Caller.
Except, you cannot just assume that a function was invoked from a Range. So you should validate its type using the TypeOf...Is operator:
Dim CallingCell As Excel.Range
If TypeOf Application.Caller Is Excel.Range Then
'Caller is a range, so this assignment is safe:
Set CallingCell = Application.Caller
End If
If CallingCell Is Nothing Then
'function wasn't called from a cell, now what?
Else
'working row is CallingCell.Row
End If
Suggestion: make the function take its dependent cells as Range parameters (if you need the Range metadata; if you only need the values then take in Double, Date, String parameters instead) instead of making it fetch values from the sheet. This decouples the worksheet layout from the function's logic, which in turn makes it much more flexible and easier to work with - and won't need any tweaks if/when the worksheet layout changes.
Application.ThisCell
MS Docs:
Returns the cell in which the user-defined function is being called from as a Range object.
You can put it to the test using the following code:
Function testTC()
testTC = Application.ThisCell.Row
End Function
In Excel use the formula
=testTC()
and (Cut)Copy/Paste to various cells.

Save FormulaArray Evaluation to an Actual Array

Background:
I need to retrieve the values that an array formula (in the excel interface) calculates in order to loop through the results there.
Problem:
I have not found a way on how to store all the values that it calculates. I can store only the first one.
Code:
Sub test()
Dim ArrTopValues() As Long
'Fails
ArrTopValues = Application.Evaluate("=LARGE(A1:A11,{1,2,3})")
End Sub
I need to work with the 3 values that the formula can hold on the excel interface by pressing "F9"
Further thoughts
I know that I can write a UDF that recreates the Large function (or even that just evaluates the "k" on the Large function and build the Array variable that way). Please understand that I am looking how to store this array evaluations for more scenarios and a workaround to solve this has been done already to "make it work".
Use INDEX to return the array and you need to make the array a variant:
Sub test()
Dim ArrTopValues()
ArrTopValues = Application.Evaluate("=INDEX(LARGE(A1:A11,{1,2,3}),0)")
End Sub

EXCEL matching elements in a discontinuous range

I want to be able to create a list of matching elements from two columns in different worksheets.
Let me provide a mock example:
I have two lists of elements, in this case boys names and girls names. Those will be placed in different sheets. In the figure below, for simplicity, they are placed in different columns (discontinuos range). I want the formula to generate the list in cells A3:A14 (list all elements in the discontinuous range which match containing the text "jo".
I've learned that the best way to provide such discontinuous range to a formula is by defining a named range.
So far, what I got is:
{IF(
MATCH(
INDEX(Named_Range,
SMALL(IF(Named_Range="*"&$A$3&"*",
ROW(Named_Range)-ROW(INDEX(Named_Range,1,1))+1),ROW()-1)),
'(Names)'!C:C,
0),
INDEX(Named_Range,
SMALL(IF(Named_Range="*"&$A$3&"*",
ROW(Named_Range)-ROW(INDEX(Named_Range,1,1))+1),
ROW()-1)),
"")
}
Named_Range is declared as
=D9:D13,F3:F6
The small function returns the nth element matching the criteria. It should also MATCH one of the names in (Names)!C:C. If so, I get the value of that cell. For the sake of simplicity, consider that '(Names)'!C:C contains all names, so it will always be true.
This formula worked for me using a single column as the range. However, I cannot evaluate the formula using the discontinuous range without Excel crashing, so I'm looking for a piece of advice on how to get it right.
Thanks.
Perhaps the best way to deal with a discontinuous range is to make it continuous...?
Your problem can be simplified a lot with a small VBA function. This function takes a discontinuous named range and returns a single continuous column containing all the same values. That way you can use your INDEX/MATCH/SMALL etc formulas in the normal way
Function Arrange(rng As Range) As Variant
Dim temp As Variant
Dim i As Long
Dim r As Range
ReDim temp(1 To rng.Cells.Count)
i = 1
For Each r In rng
temp(i) = r.Value
i = i + 1
Next r
Arrange = Application.Transpose(temp)
End Function
After you have added this code to a new module in your VBA editor you will be able to use it like this on the worksheet:
=INDEX(ARRANGE(Named_Range),1)
To get the first element, for example.

VBA function to add a value to cells in range and get product

I've been tearing my hair out over this and after endless searching online still can't find the answer.
Here's a quick summary of what I'm looking for an a longer explanation below!
I need a VBA function to replicate a formula similar to this: {=product(A:A+1)}. This goes through every cell in column A where there is a number and adds 1, then multiplies. This can be done using either (1) Evaluate unless there are blanks or text in the range, which gives an error or (2) worksheetfunction.product, but it doesn't like it when I look through the range and add 1.
If anyone can figure out a solution for using either of these options I would be over the moon. I may also be missing something very basic!
Longer version...
What I'm trying to do:
Create three VBA functions that calculate 1) compound return 2) annualised compound return and 3) annualised volatility. These would be for variable ranges and for the annualised functions you can also specify whether it is days, months or years. For example, the compound return function when used would look something "=compoundreturn(range,number of periods)"
The calculations are easy using formula but I want to create functions to make it a quick process.
I can get all three to work through VBA using EVALUATE.
They look like this,
Function PerRet(rng As range)
PerRet = Worksheet.Evaluate("=exp(sumproduct(ln(" & rng.Address & "+1)))-1")
End Function
Function AnnRet(rng As range, np As Double)
AnnRet = Evaluate("=exp((Average(ln(" & rng.Address & "+1)))*" & np & ")-1")
End Function
Function AnnVol(rng As range, np As Double)
AnnVol = Worksheet.Evaluate("=stdev.s(ln(" & rng.Address & "+1))*sqrt(" & np & ")")
End Function
The problem is that the range selected may sometimes by the entire column, which contains blanks and potentially text. Functions like AVERAGE or STDEV.S will ignore these when you are just using them in formulas. Unfortunately, they don't do this when using Evaluate.
I know that instead of using Evaluate for AVERAGE, I could use worksheetfunction.average, which would then ignore the blanks and text. But here I come across another problem in that part of the function has to look through a range of returns (postiive and negative) and add 1 to each before averaging. This doesn't seem to work for a range unless that range is simply one cell.
An example of what I have so far for this is
Function AnnRet(rng As range, np As Double)
AnnRet = exp(WorksheetFunction.Average(WorksheetFunction.Ln(rng + 1)) * np) - 1
End Function
Any ideas how I can make it add 1 to each of the cells in the range before getting the Ln and then averaging?
Many thanks for looking
Duncan
A little light testing suggests something along these lines might work:
Sub Tester()
Dim v
v = ActiveSheet.Evaluate("=SUM(IF(ISBLANK(A:A),false,A:A+1))")
Debug.Print v
End Sub
I just had a few numbers in ColA and it only added (number+1) where the cell was populated - ie. it did not also sum up a bunch of 1's where cells were empty.
EDIT:
After your comments about problem with non-numeric cells, a little more testing comes up with something like:
Function Tester(rng As Range)
Dim v, f
f = "=AVERAGE(IF(ISBLANK({a}),FALSE,IF(ISNUMBER({a}),LN({a}+1),FALSE)))"
'remove any unsed part of the input range (for performance)
Set rng = Application.Intersect(rng, rng.Parent.UsedRange)
v = rng.Parent.Evaluate(Replace(f, "{a}", rng.Address(False, False)))
Tester = v
End Function
This first filters out blanks - non-blanks can then safely be passed to ISNUMBER() to filter out non-numeric cells. This is not quite at your final formula but not far off. Do you have some inputs and expected output for testing?
EDIT 2:
Using Intersect() to restrict a full-column input range to only the "used" part of the sheet seems to improve performace significantly (by ~100x when I only had a thousand values in the range).

Excel: Count same number values in noncontiguous range

I'm looking for the most elegant way to count the same number values in a noncontiguous range (I'll refer to it as just 'range'). This is the range:
=$C$2:$C$31,$E$2:$E$31,$G$2:$G$31,$I$2:$I$31,$K$2:$K$31,$M$2:$M$31,$O$2:$O$31,$Q$2:$Q$31,$S$2:$S$7
These are the parameters:
The range contains non-adjacent columns.
The columns differ in height.
The cells in the range are either empty or contain integers.
I'm checking for how many cells equal '1', how many equal '2' etc. in the range. (Not in one go, but in seperate formulas).
I've used a named range to reference the range. I'd really like to use this named range in the formula, in one way or another.
I hope I've given you enough info... Thanks in advance!
I agree with Kartik that a VBA solution is required. However the solution offered is a little inefficient in that it loops over every cell in the ranged passed into the function. It also limits the key parameter to a range reference, and can only count up to 32767 matches. Here's an alternative addresses these shortcomings
Function CountIf_N(rng As Range, key As Variant) As Variant
Dim r As Range
Dim count As Long
count = 0
For Each r In rng.Areas
count = count + WorksheetFunction.CountIfs(r, key)
Next
CountIf_N = count
End Function
Note: assumes Excel 07 or later. If using with an ealier version replace CountIfs with CountIf
One approach is to use excel built in function Countif, but it won't work with non-contigous range. The other way (the easy way) will be to use VBA to create your own custom function, and then use it in excel.
I've presented that technique here.
Goto visual basic editor in excel by pressing Alt+F11, in the project window insert a new module and paste the below code:
Function countif_n(rng As Range, key As Range) As Integer
Dim count As Integer
count = 0
For Each cell In rng
If cell.Value = key.Value Then
count = count + 1
End If
Next cell
countif_n = count
End Function
Here rng is your non-contigous range, and key represent the "range"(cell) which contains the value you want to count. For eg., to check for 1 enter 1 in any cell lets suppose "F2", and your non-contigous range is "testrange"
Then use the above function by entering the following in any blank cell:
=countif_n(testrange, F2)
Although COUNTIF can't handle non-contiguous ranges some functions can, for example RANK and COUNT so for a range called Range this formula will give the number of instances of a number in Z2 within Range
=IFERROR(COUNT(Range)-SUM(RANK(Z2,Range,{1,0}))+2,0)
assumes Excel 2007 or later but can be amended to work in earlier versions
This doesn't quite work if there's stuff below S7 that can't be counted, but you may be able to modify. It also doesn't incorporate the named range.
=SUM(IF(MOD(COLUMN(A2:S31),2)=0,IF(A2:S31=2,1,0)))
This example counts the number of 2's.
This needs to be array-entered with ctrl-shift-enter. It's based on the fact that you're counting in every other column, at least in your example. Also, although you mention the columns are different heights, it looks like all except S are the same height. So maybe there's a way to work around that.

Resources