You are on page 1of 21

Journal of Instructional Techniques in Finance

Volume 4, Number 1, 2012

Automating Markowitz Optimizations Using VBA


David Porter and Robert Stretcher

In the interest of reducing grading workload and managing cross-student sharing (cheating), we provide an example of
automating the construction of Markowitz (1959) efficient frontiers. The program automates the entire procedure from
downloading data for a specified number of securities (using either randomly generated securities from a provided list or
specified tickers) to generating graphs of the efficient frontier. Many commercially available optimization programs are
available, but this program is free to use and works within Microsoft Excel (2007-2010), the software package most
professors use when creating optimization assignments. The program is written in Visual Basic for Applications (VBA) but
does not require any knowledge of VBA to use. The program allows easy creation of random portfolios for student
assignments so that cross-student sharing can be minimized without adding an additional grading burden on the professor or
TA. The professor can also be assured that the stocks selected produce the desired type of optimization (actually work) before
using each set of securities. Where students are allowed to select their own portfolios, the program allows professors to
easily check computations and optimizations without having to manually optimize each portfolio. The program can also be
used to demonstrate the similarity of efficient frontiers across different stocks, portfolio sizes, and dates.
INTRODUCTION
Cutbacks in government funding to public universities
has resulted in reductions across many areas including but
certainly not limited to the hiring of new faculty to keep
class sizes small. As class sizes increase, grading of
assignments becomes more problematic both in the amount
of time required and in the possibility of cross-student
sharing (also referred to as free-riding or cheating). In the
case of investments courses, many professors require
students to complete some type of optimization assignment.
With class sizes exceeding 100 students, however, offering
individual students the opportunity to select their own
stocks makes the grading process difficult or prohibitive. As
a result, students are commonly placed in groups, and entire
classes may be assigned the same set of stocks to optimize.
Although this technique significantly reduces the length of
time needed to evaluate the assignments, it also increases
the probability of cross-student or cross-group sharing
making it difficult to determine which students actually
understand the optimization process. One solution to these
issues is to use an automated system designed to generate
Markowitz (1959) optimizations so that every group or
student could be either assigned a separate set of stocks to
optimize, or be allowed to select their own securities. In this
paper we describe a VBA program that can be used to
automate the optimization of portfolios using Solver in
Microsoft Excel. The object is not to teach VBA but to offer
some typical code that can be used to expedite a professors
use of this type of automation.
DESCRIPTION OF THE "FRONT-END" OF THE
PROGRAM
The program is designed for a personal computer (PC)
running a recent version of Excel (2007 or 2010) with the

Solver add-in installed and does not require any knowledge


of VBA, unless the individual professor wishes to adjust the
code for his/her own purposes. The program will not run on
an Apple operating system but could work if the MAC was
dual booted into Windows and was running the appropriate
Excel version. Since the VBA code is essentially a large
macro, when the file containing the program is opened, two
warnings may occur: (1) This file originated from an
Internet location and might be unsafe and, (2) Some active
content has been disabled. Click for more details. In both
cases, clicking Enable Content allows the VBA to run.
The file will then default to the "Explanation" sheet that
contains a brief explanation of the program.
As noted in the red box, the program downloads
monthly data from Yahoo for either a randomly selected
portfolio of securities or a specific set of ticker symbols. If
the user wants random portfolios, the number of securities is
entered in cell F1. If the user wants a specific set of
securities, the tickers should be entered in column A,
starting with cell A1, and cell F1 should be set to 0. Cell F1
is checked first, so if it contains a number between 2 and 20,
a random portfolio will be generated even if there are
specific tickers in column A. The restriction of 2 to 20
tickers is usually sufficient for most assignments but can
easily be altered by changing the VBA code. The program
will compute and graph the Markowitz optimization
assuming no short selling. Stocks with less than 30 months
of data are not used since the mean return is used as the
expected return and 30 observations is typically assumed to
be a reasonable statistical sample. The user can also
change the minimum observation assumption by altering the
VBA code.

Journal of Instructional Techniques in Finance

Volume 4, Number 1, 2012

Figure 1. Front End Explanation Box

Before clicking Run Program, the user should note


the status bar at the bottom of the spreadsheet:
Figure 2. Status Bar.

Figure 3. Prices Sheet.

additional sheet is the list of stocks used for randomly


generating portfolios. This sheet can be populated by the
user but the original file contains about 500 stocks as
examples. The "Prices" sheet contains the adjusted closing
prices downloaded from Yahoo. Since the program uses
monthly prices, these prices are the last adjusted closing
price of each month.
The "Returns" sheet contains the returns computations
based on the downloaded prices. Where the number of
prices differs across the securities, the returns are truncated
to the shortest set of data. This is necessary for the
computation of correlations and covariances. If the number
of returns is less than 30 for any of the securities the sample
is considered too small and the program will terminate.
The Basic Stats sheet contains summary measures for
the stocks: means, standard deviations, minimum return,
maximum return, and range of returns, as well as the
correlation matrix.
Figure 4. Returns Sheet.

The status bar reports where the program is at any point


in time. If it appears that nothing is happening, the user
should check the status bar. Also note that the program
generates six additional spreadsheets. Prices, Returns,
BasicStats, Model, ChartData and Graph. The seventh

10

Journal of Instructional Techniques in Finance


Figure 5. Basic Stats Sheet.

Volume 4, Number 1, 2012


Figure 7. ChartData Sheet.

When you click Run Program the date selection


dialog box pops up:
Figure 8. Date Selection Dialogue Box.
The "Model" sheet contains the data needed for the
optimization: the weights for each stock for the last
optimization run, the expected returns (the means), the
variance-covariance matrix, the expected return, variance
and standard deviation for the optimized portfolio and the
target cell. The target cell changes as the program generates
the chart data from the minimum variance portfolio to the
maximum return portfolio.
Figure 6. Model Sheet.

The "ChartData" sheet contains the Solver optimization


output for each of the 16 runs from the minimum variance
portfolio to the maximum return the weights for each
security for each run, as well as the portfolio expected return
and standard deviation. The number of runs is arbitrary and
16 (15 Solver runs plus the maximum return portfolio) was
chosen as a number that would produce a nice looking graph
the majority of the time.
The "Graph" sheet contains a scatter plot of the chart data.
Since most graphs of the efficient frontier look better with
more points near the minimum variance point, the chart data
is generated using that assumption. The graph will also
auto-scale itself based on the data. An example of the graph
output is in Figure 10.

Use the mouse to select the desired start and end dates.
There are several checks built into the dialog box such as
the date cannot be in the future and the start date must be
before the end date. Once again, the user is reminded that a
minimum of 30 observations must be used. Once the dates
are selected and the OK is clicked, a number of observations
check dialog box is displayed:
Figure 9. Download Initiation Selection Box.

If the user clicks "No" they are returned to the date


dialog box. After clicking Yes, the user can watch the

11

Journal of Instructional Techniques in Finance


status bar as the program attempts to download the data
from Yahoo and complete the optimization. Once
completed, the "Graph" sheet is shown:
Figure 10. Markowitz Graph Output.

SUGGESTED CLASSROOM USE


UW-Whitewater runs two sections of undergraduate
Investments each term with section sizes between 55 and 60
students. Lectures and exams cover the optimization process
for two risky assets when rf exists, but the undergraduate
students rarely show a thorough understanding of the
method. One way to increase their understanding is use real
world data with more than 2 risky assets. With
undergraduates, 5 risky assets is sufficient to determine if
they can complete the optimization process and graph an
efficient frontier. Instructions on how to download data
from finance.yahoo.com are provided to students along with
instructions on how to optimize a portfolio using Solver in
Excel. Some textbooks contain this information but the
undergraduate textbook currently in use at UW-Whitewater
does not. The yahoo instructions are also available from the
authors and instructions for Markowitz style optimization
are available in many textbooks including Bodie, Kane and
Marcus, Investments (McGraw-Hill Irwin), Chapter 7,
Appendix A (pages 234-239 in the 9th edition) and Craig
Holden's Excel Modeling and Estimation in Investments
(Pearson Prentice Hall), Chapters 7 and 8 in the 3rd edition.
When class sizes were smaller (in the early 1990's
Investment class sizes at Whitewater were in the 15-20
range), students were put into small groups and allowed to
select their own securities. Grading of 6-10 assignments was
not prohibitive and assignments could be returned in a few
days. As class sizes grew, group sizes increased but class
sizes reached the point where it was necessary to assign a
single group of specific stocks for the entire class. These
adjustments allowed the grading of 30-40 assignments in a
reasonable timeframe but dramatically increased the amount

Volume 4, Number 1, 2012


of cross-group sharing. Requesting students behave ethically
and increasing the penalty for cross-group sharing had little
effect on the issue. It was clear that each group or student
would need to be assigned a separate set of securities and
that a more efficient method of grading had to be used; thus
the development of the automated program.
We use the program for several purposes. For the
undergraduates, we randomly generate the 5 stock portfolios
they use in their optimizations. If there are 40 groups, we
randomly generate 40 5-stock portfolios. Since the program
generates the graph for the random securities, it is possible
to determine if the optimization produces a "good" graph
before including that set of securities in the assignment.
Students may select different points for their optimization
than the automated program but the program significantly
reduces the time needed to determine if the optimization
was done correctly. For the graduates, they select their own
10 stock portfolios, but those assignments can still be easily
graded since the program can download and optimize a 10
stock portfolio in about 5 seconds (assuming a reasonable
Internet connection). We also use the program in-class to
demonstrate to students that most efficient frontiers have a
similar shape regardless of the time period, length of time
period or number of securities in the portfolio. We find that
simply discussing correlations and their relationship to the
shape of the efficient frontier is never as effective as using
real data. The students also get to see the entire process
from hand calculating a two stock optimization, to using
Excel to optimize a multi-stock portfolio, to a fully
automated computation of an optimization.
Assigning every group their own set of stocks has
significantly reduced the amount of cross-group sharing but
free-riding within groups still exists. Use of student selfevaluations within the group helps with this issue but often
students are more concerned they will have to work with the
same student again in another class than they are with the
ethics of giving another student a good evaluation when
they were actually a free-rider.
CONCLUSION
This paper described the "front-end" of an automated
program for generating efficient frontiers using Microsoft
Excel. The program can be used to reduce grading time
while significantly curtailing cross-group sharing (cheating)
when professors use assignments to improve student
understanding of Markowitz (1959) optimizations. The
program is freely available by contacting David Porter in the
UW-Whitewater Department of Finance and Business Law.
To minimize the distribution of the program to students,
professors should use either their office phone or university
email as evidence they are not a student. For those familiar
with VBA, a PDF file is also available which explains the
code thereby reducing the learning curve if changes are
desirable to meet the individual needs of a specific course.
To view the VBA code in the Excel file, use Alt+F11 or use

12

Journal of Instructional Techniques in Finance

Volume 4, Number 1, 2012

the Developer tab. The Developer tab is not shown on the


Excel toolbar by default but can be turned on by going to
File | Options | Customize Ribbon and checking
Developer under the Main Tabs. The code can then be
viewed by clicking on "View Code":

Demonstrating Retrieval of
Financial Information in
XBRL

Figure 11. Developer Tab.

Ameeta Jaiswal-Dale and Jianing


(Jade) Fang

The main code can be found by double clicking on the


Module "MainApplication":
Figure 12. Main Application Location.

The Securities and Exchange Commission (SEC) has


upgraded the existing system of Electronic Data Gathering,
Analysis, and Retrieval (EDGAR), to Interactive Data
Electronic Applications (IDEA) platform, using eXtensible
Business Reporting Language (XBRL). In January 2009, the
SEC issued its final mandate for XBRL adoption and the
conversion target dates for all firms. This conversion
enables users to retrieve listed companies financial
statement information at two levels, document and the data
element, compared to the document level alone under the
existing system of EDGAR. Beneficial to all users, this
change is particularly important to resource-strapped
entities such as small businesses, and universities. The
result is user friendly for students researching information.
With simply the industry standard tool of Microsoft Excel,
without expensive proprietary XBRL modules, detailed
information is now available, quickly, efficiently and at
minimum cost. This paper explains the basic concept of
XBRL and demonstrates the ease of the retrieval process in
Microsoft Excel.
INTRODUCTION

REFERENCE
Markowitz, H. (1959). Portfolio Selection: Efficient
Diversification of Investments, John Wiley & Sons, New
York City, New York.
David Porter is a Professor of Finance at the University of
Wisconsin-Whitewater.
Robert Stretcher is a Professor of Finance at Sam Houston
State University.

On January 30, 2009, the Securities and Exchange


Commission (SEC) chairperson, Mary Schapiro, announced
the agencys final mandate for eXtensible Business
Reporting Language (XBRL) adoption and the firm
conversion target dates (SEC, January 2009). The mandate
stated the largest domestic and foreign public companies
that use U.S. Generally Accepted Accounting Principles
(GAAP) to file their financial statements in XBRL format
by June 15, 20091: medium-sized filers by June 15, 2010,
and the rest of the filers, either using U.S. GAAP or
International Financial Reporting Standards (IFRS), by June
15, 2011 (Fang 2010).
The new rules are intended to make financial
information easier for investors to analyze and to assist in
automating regulatory filings and business information
processing. The XBRL system lists information at both the
document level, such as the entire set of financial statements
for a given firm, and the data element level, such as
individual accounts like inventory. In XBRL interactive
data, or data tagged at the data element level, can function
across multiple and/or different platforms or application
programs. Thus, XBRL has the potential to increase the
speed, accuracy and usability of financial disclosure, and

13

Automating Markowitz with VBA: An Explanation


This file describes the functions and subroutines used to gather the data and generate the optimization
for the application titled "Automating Markowitz Optimizations using VBA". We use one form to request
the start and end dates for the data that is not described below. The object is not to teach VBA
programming but to provide some guidance to expedite the learning curve for those wishing to alter the
code for their own needs. We would describe the code below as "typical" code that can be found online or
in a good VBA book such as Power Programming with VBA by John Walkenback or VBA and Macros for
Microsoft Excel by Bill MrExcel Jelen and Tracy Syrstad.
The code begins with Option Explicit requiring that all variables be explicitly declared. Option Base 1
starts the array counters at 1 instead of the default 0. Most of the variables are declared as Public so they
are usable in all subroutines:
Option Explicit
Option Base 1
Public nTickers As Long, nSymbols As Long, nRandom As Long
Public nSamples As Long, i As Long, j As Long, k As Long, MinObs As Long
Public Symbols() As String, Symb As String, LegalName() As String
Public IndexSymbol As String, IndexName As String, maxTicker As String
Public LendRate As Single, BorrowRate As Single, RiskAversion As Single
Public EndMonth As Integer, EndDay As Integer, EndYear As Integer
Public StartMonth As Integer, StartDay As Integer, StartYear As Integer
Public nMonths As Integer, nRuns As Integer
Public EndDate As Date, StartDate As Date
Public Msg As String, Ans As Variant
Public maxStdDev As Double, maxReturn As Double, MVPStdDev As Double
Public MinVar As Boolean, Frontier As Boolean, AllowShort As Boolean
Public Error As Boolean
Public FormatRange As Range
The main application starts with additional variable declarations and then sets the display alerts and
screen updating to false (turns them off). Since the program updates the status bar, it is turned on:
Sub MainApplication()
Dim Msg As String, Ans As String
Dim ColNum As Long, nRow As Long
Dim UsedRow() As Integer
Dim SheetName As Worksheet
Dim Duplicate As Boolean
On Error Resume Next
Application.DisplayAlerts = False
Application.ScreenUpdating = False
Application.DisplayStatusBar = True
Although it is personal preference, we prefer to start everything from a blank slate so we delete previous
worksheets:
If SheetExists("Prices") Then Sheets("Prices").Delete
Sheets.Add after:=Sheets("Stocks")
ActiveSheet.Name = "Prices"

If SheetExists("Returns") Then Sheets("Returns").Delete


Sheets.Add after:=Sheets("Prices")
ActiveSheet.Name = "Returns"
If SheetExists("BasicStats") Then Sheets("BasicStats").Delete
Sheets.Add after:=Sheets("Returns")
ActiveSheet.Name = "BasicStats"
If SheetExists("Model") Then Sheets("Model").Delete
Sheets.Add after:=Sheets("BasicStats")
ActiveSheet.Name = "Model"
If SheetExists("ChartData") Then Sheets("ChartData").Delete
Sheets.Add after:=Sheets("Model")
ActiveSheet.Name = "ChartData"
If SheetExists("Graph") Then Sheets("Graph").Delete
In this example, we either randomly generate the stocks to be used in the optimization by selecting them
from the worksheet Stocks or use the supplied symbols. First, check to see if the random value in cell
F1 is set to zero and if it is then count the number of tickers in column A, check that there are between 2
and 20 tickers, redimension the arrays for the number of tickers and read in the tickers:
nRandom = Range("F1").Value
If nRandom = 0 Then
nTickers = Cells(Rows.Count, 1).End(xlUp).Row
If nTickers < 2 Or nTickers > 20 Then
Msg = "You must have between 2 and 20 tickers. Either the " & vbCrLf & _
"random number of tickers must be between 2 and 20, " & vbCrLf & _
"or the number of tickers must meet the same criteria."
Ans = MsgBox(Msg, vbOKOnly + vbExclamation)
Exit Sub
End If
ReDim UsedRow(nTickers)
ReDim Symbols(nTickers)
For i = nTickers To 1 Step -1
Symbols(i) = Range("A1").Offset(i - 1, 0).Value
Next i
End If

If the random value in cell F1 is not zero, then check that the random value is between 2 and 20 and
redimension the arrays for the appropriate number:
If nRandom <> 0 Then
nTickers = nRandom
If nTickers < 2 Or nTickers > 20 Then
Msg = "You must have between 2 and 20 tickers. Either the " & vbCrLf & _
"random number of tickers must be between 2 and 20, " & vbCrLf & _
"or the number of tickers must meet the same criteria."
Ans = MsgBox(Msg, vbOKOnly + vbExclamation)
Exit Sub
End If
ReDim UsedRow(nTickers)
ReDim Symbols(nTickers)
Count the number of security symbols on the "Stocks" sheet and make sure it is greater than the random
number in cell F1:
Sheets("Stocks").Select

nSymbols = Cells(Rows.Count, 1).End(xlUp).Row


If nSymbols < nTickers Then
Msg = "The number of security symbols on the Stock sheet " & vbCrLf & _
"is less than the random number in Cell F1. Please add " & vbCrLf & _
"more security symbols or reduce the random number."
Ans = MsgBox(Msg, vbOKOnly + vbExclamation)
Exit Sub
End If
Initialize the random number generator and create an array of random numbers:
Randomize
For i = 1 To nTickers
Do
nRow = Int(Rnd() * nSymbols)
If nRow = 0 Then nRow = 1
Duplicates are not allowed, so check to see if the random ticker has been used before. If it has, generate
a new random number:
Duplicate = False
For j = 1 To i
If UsedRow(j) = nRow Then Duplicate = True
Next j
Loop While Duplicate
UsedRow(i) = nRow
Symbols(i) = Sheets("Stocks").Range("A1").Offset(nRow, 0).Value
Next i
End If
Stock tickers can be illegal names for named ranges, so set up a set of legal names:
ReDim LegalName(nTickers)
For i = 1 To nTickers
LegalName(i) = "Stock" & i
Next i

Clear a spot for data on the "Explanation" sheet:


With Sheets("Explanation")
Range("AA1:AG65500").Clear
End With
Set the Error to False before calling the GetYahooData Subroutine (this routine accesses
finance.yahoo.com and downloads the data) and then check the Error after the subroutine to see if there
was an error accessing the data. If there was, exit the program:
Error = False
Call GetYahooData(ColNum)
If Error Then
Sheets("Explanation").Select
Range("F1").Select
Exit Sub
End If

Although it is personal preference, it might be useful to let the user know if there was an issue trying to
download a particular ticker:
If ColNum < nTickers Then
Msg = "Yahoo reports that the ticker '" & Symb & "' does not exist," & vbCrLf & _
"or does not exist for the specified dates." & vbCrLf & _
"This may be from an internet error or a ticker symbol error." & vbCrLf & _
"If you are sure that this ticker symbol is correct then you should" & vbCrLf & _
"close Excel then try running the program again."
Ans = MsgBox(Msg, vbOKOnly + vbExclamation)
Sheets("Explanation").Select
Exit Sub
End If
Call the Returns subroutine (to compute the returns). Again if there is an error, exit the program:
Call Returns
If Error Then Exit Sub
Create named ranges for the returns for each of the stocks:
For k = 1 To nTickers
Sheets("Returns").Range("A2").Offset(0, k - 1).Resize(MinObs).Name = LegalName(k)
Next k
Call each of the subroutines need to compute and chart the optimization:
Call GetStats
Call ModelSheet_Setup
Call ChartDataSheet_Setup
If Error Then Exit Sub
Call UpdateChart
The main program finishes by turning on the display alerts and screen updating, then clears the status
bar and ends the subroutine:
Application.DisplayAlerts = True
Application.ScreenUpdating = True
Application.StatusBar = Empty
End Sub
The main program calls the function SheetExists and returns TRUE if a worksheet exists in the active
workbook. As noted above, if the sheet exists, we delete it so we always start from a clean slate:
Public Function SheetExists(sname) As Boolean
Dim x As Object
On Error Resume Next
Set x = ActiveWorkbook.Sheets(sname)
If Err.Number = 0 Then SheetExists = True _
Else SheetExists = False
End Function
The web query to download data from Yahoo can be found in Bill Jelens book referenced above. We
place the web query in the subroutine GetYahooData. The subroutine begins with variable declarations
and then opens the form frmGetDates. This form requests the start and end dates for the data and does
a few checks such as making sure the end date is not before the begin date and the end date is not in the

future. You could just as easily put the dates in cells on the Explanation sheet but the form is a bit more
professional:
Sub GetYahooData(ColNum As Long)
Dim LastRow As Long
Dim FreqS As String
Show (display) the form GetDates to the user so that they can select the start and end dates for the data:
frmGetDates.Show
If Error Then Exit Sub
For simplicity, we only use monthly data in this example. We also need to adjust the information passed
from the form GetDates so it matches what is expected by Yahoo (January is month 0 and December
month 11):
FreqS = "m"
StartMonth = StartMonth - 1
EndMonth = EndMonth - 1
Then for each ticker, we request the selected data from Yahoo. The counter ColNum is set to zero for
starters:
ColNum = 0
For i = 1 To nTickers
Sheets("Explanation").Select
Range("AA1:AG65000").Clear
Symb = Symbols(i)
Application.StatusBar = "Attempting to Download " & Symb & " ..."
With ActiveSheet.QueryTables.Add(Connection:= _
"URL;http://ichart.finance.yahoo.com/table.csv?s=" & Symb & _
"&a=" & StartMonth & "&b=" & StartDay & "&c=" & StartYear & _
"&d=" & EndMonth & "&e=" & EndDay & "&f=" & EndYear & "&g=" & FreqS _
, Destination:=Range("AA1"))
.RefreshStyle = xlOverwriteCells
.RowNumbers = False
.FillAdjacentFormulas = False
.BackgroundQuery = True
.SavePassword = False
.SaveData = True
.AdjustColumnWidth = False
.RefreshPeriod = 0
.Refresh BackgroundQuery:=False
.WebSelectionType = xlSpecifiedTables
.WebTables = "19"
Most of the options, RefreshStyle, etc., are not needed for the query to work but are usually kept for
clarity. We have run the query with only the last three options. A complete list of options is available at the
Microsoft Developer Library: http://msdn.microsoft.com/enus/library/microsoft.office.interop.excel.querytable_properties.aspx. Note that the WebTables page of 19
requires either some good guessing or an examination of the source code at Yahoo. There is no direct
way to determine the correct web table to download so if Yahoo moves where the table is located, this
number will need to be changed.
The data is downloaded as comma delimited so it must be parsed into columns. There are many ways to
do this but the example below is relatively efficient:

Sheets("Explanation").Range("AA1").Select
Range(Selection, Selection.End(xlDown)).Select
Selection.TextToColumns Destination:=Range("AA1"), DataType:=xlDelimited, _
TextQualifier:=xlDoubleQuote, ConsecutiveDelimiter:=True, Tab:=False, _
Semicolon:=False, Comma:=True, Space:=False, Other:=False, FieldInfo:= _
Array(Array(1, 1), Array(2, 1), Array(3, 1), Array(4, 1), Array(5, 1), Array(6, 1), _
Array(7, 1)), TrailingMinusNumbers:=True
End With
If the query was successful, increment the counter:
ColNum = ColNum + 1
Determine how much data was downloaded and copy the closing prices to the prices sheet:
LastRow = Sheets("Explanation").Cells(Rows.Count, 27).End(xlUp).Row
Sheets("Prices").Range("A1").Offset(0, ColNum) = Symbols(i)
Sheets("Explanation").Range("AG1:AG" & LastRow).Copy _
Sheets("Prices").Range("A2").Offset(0, ColNum)
Next i
After downloading the data for each of the tickers, copy the dates to the Prices sheet, set the column
width and set the correct date format:
Sheets("Explanation").Range("AA1:AA" & LastRow).Copy
Sheets("Prices").Range("A2").PasteSpecial (xlPasteValues)
Sheets("Prices").Columns("A:A").ColumnWidth = 12.86
Selection.QueryTable.Delete
Set the correct date format on worksheet "Prices":
With Sheets("Prices")
.Activate
Range("A1").Select
If FreqS = "m" Then .Range("A3:A" & LastRow).NumberFormat = "[$-409]mmm-yy;@"
Add some other formatting:
Range("B3").Resize(LastRow, nTickers).NumberFormat = "0.00"
With Range("A1").Resize(2, nTickers + 1)
.Interior.Color = 13209
.Font.ThemeColor = xlThemeColorDark1
.Font.Bold = True
.HorizontalAlignment = xlCenter
.VerticalAlignment = xlCenter
End With
Range("A1").Interior.ColorIndex = xlNone
With Range("A2:A" & LastRow)
.Font.ThemeColor = xlThemeColorDark1
.Interior.Color = 13209
.Font.Bold = True
.HorizontalAlignment = xlCenter
.VerticalAlignment = xlCenter
End With
End With

To complete the subroutine, clear the data range and turn off copy mode:
Sheets("Explanation").Range("AA1:AG65500").Clear
Application.CutCopyMode = False
End Sub
The Returns subroutine computes returns from the prices downloaded from Yahoo. It starts with an
additional variable declaration and sets the Symb variable to null (blank).
Sub Returns()
Dim LastRow As Long
Symb = ""
Determine the minimum number of observations for each ticker:
For i = 1 To nTickers
LastRow = Sheets("Prices").Cells(Rows.Count, i + 1).End(xlUp).Row - 2
If i = 1 Then
MinObs = LastRow
Else
If LastRow < MinObs Then
MinObs = LastRow
Symb = Symbols(i)
End If
End If
Next i
Returns have one less observation than prices:
MinObs = MinObs - 1
If a symbol has fewer observations than the others, report that symbol to the user:
If Not Symb = "" Then
Msg = "Minimum number of observations is " & MinObs & " for symbol " & Symb
Ans = MsgBox(Msg, vbOKOnly)
End If
If a symbol has fewer than 30 observations, report that to the user, set the Error to true, delete the
BasicStats worksheet and exit the subroutine:
If MinObs < 30 Then
Msg = "Minimum number of observations is less than 30" & Chr(10) & _
"Terminating the application."
Ans = MsgBox(Msg, vbOKOnly)
Error = True
If SheetExists("BasicStats") Then Sheets("BasicStats").Delete
Sheets("Explanation").Select
Exit Sub
End If
On the Returns sheet, insert the ticker symbols for the titles:
Sheets("Returns").Select

For i = 1 To nTickers
Range("A1").Offset(0, i - 1) = Symbols(i)
Next i
Compute the returns:
Range("A2") = "=(Prices!B3-Prices!B4)/Prices!B4*100"
Range("A2").AutoFill Destination:=Range("A2").Resize(, nTickers), Type:=xlFillDefault
Range("A2").Resize(, nTickers).AutoFill Destination:=Range("A2").Resize(MinObs, nTickers),
Type:=xlFillDefault
Add some formatting:
Range("A2").Resize(MinObs, nTickers).NumberFormat = "0.0000"
Set FormatRange = ActiveSheet.UsedRange.SpecialCells(xlCellTypeConstants, 2)
With FormatRange
.Interior.Color = 13209
.Font.ThemeColor = xlThemeColorDark1
.Font.Bold = True
.HorizontalAlignment = xlCenter
.VerticalAlignment = xlCenter
End With
End Sub
The GetStats subroutine computes the basic statistics from the returns.
Sub GetStats()
The subroutine starts by selecting the BasicStats worksheet, then adds some formatting and the titles:
Sheets("BasicStats").Select
Range("A:A").ColumnWidth = 12
Range("A1:D1").MergeCells = True
Range("A1").Value = "Summary Measures for Stock Returns"
Range("A3").Value = "Stocks"
Range("A4").Value = "Means"
Range("A5").Value = "Std. Dev."
Range("A6").Value = "Min"
Range("A7").Value = "Max"
Range("A8").Value = "Range"
Range("A10").Value = "Correlations"
For each ticker, enter the formulas for the average, standard deviation, minimum, maximum and range of
returns:
For i = 1 To nTickers
Range("A3").Offset(0, i).Value = Symbols(i)
Range("A4").Offset(0, i).Formula = "=Average(" & LegalName(i) & ")"
Range("A5").Offset(0, i).Formula = "=Stdev(" & LegalName(i) & ")"
Range("A6").Offset(0, i).Formula = "=Min(" & LegalName(i) & ")"
Range("A7").Offset(0, i).Formula = "=Max(" & LegalName(i) & ")"
Range("A8").Offset(0, i).FormulaR1C1 = "=R[-1]C-R[-2]C"
Next
Insert the titles (symbols) for the correlation table:

With Range("A10")
For i = 1 To nTickers
With .Offset(i, 0)
.Value = Symbols(i)
End With
With .Offset(0, i)
.Value = Symbols(i)
End With
Next
Compute the correlations:
For i = 1 To nTickers
For j = 1 To nTickers
.Offset(i, j).Formula = "=Correl(" & LegalName(i) & "," & LegalName(j) & ")"
Next
Next
End With
Add some formatting:
Range("B4").Resize(nTickers + 7, nTickers).NumberFormat = "0.0000"
Set FormatRange = ActiveSheet.UsedRange.SpecialCells(xlCellTypeConstants, 2)
With FormatRange
.Interior.Color = 13209
.Font.ThemeColor = xlThemeColorDark1
.Font.Bold = True
.HorizontalAlignment = xlCenter
.VerticalAlignment = xlCenter
End With
End Sub
The ModelSheet_Setup subroutine sets up the model worksheet with the necessary data to optimize the
portfolio and calls the subroutine "Solver" to perform the optimization:
Sub ModelSheet_Setup()
Dim modelSheet As Worksheet
Set modelSheet = Worksheets("Model")
Application.StatusBar = "Calculating Covariances"
With modelSheet
.Activate
Add some formatting and setup the titles:
.Columns("A:A").ColumnWidth = 12
.Columns("B:F").ColumnWidth = 9.29
.Range("A1:C1").MergeCells = True
.Range("A1").Value = "Portfolio Selection Model"
With .Range("A3")
.Value = "Stock"
.Offset(1, 0).Value = "Weights"
.Offset(2, 0).Value = "Means"

For i = 1 To nTickers
.Offset(0, i).Value = Symbols(i)
Enter the naive weights as a starting point for the optimizations:
.Offset(1, i).Value = 1 / nTickers
Enter the average returns as the expected returns:
.Offset(2, i).Formula = "=Average(" & LegalName(i) & ")"
Next
End With
Create named ranges for the weights and expected returns:
With .Range("A4")
Range(.Offset(0, 1), .Offset(0, 1).End(xlToRight)).Name = "Weights"
Range(.Offset(1, 1), .Offset(1, 1).End(xlToRight)).Name = "Means"
End With
Sum the weights, add a title, and create a named range:
With .Range("A3").Offset(0, nTickers + 1)
.Value = "Sum"
.Offset(1, 0).Name = "SumWeights"
.Offset(1, 0).Formula = "=Sum(Weights)"
End With
Add the titles for the covariance table:
With .Range("A7")
.Value = "Covariances"
For k = 1 To nTickers
.Offset(k, 0).Value = Symbols(k)
.Offset(0, k).Value = Symbols(k)
Next k
Compute the covariances:
For i = 1 To nTickers
For j = 1 To nTickers
.Offset(i, j).Formula = "=Covar(" & LegalName(i) & "," & LegalName(j) & ")"
Next
Next
Range(.Offset(1, 1), .Offset(nTickers, nTickers)).Name = "Covar"
End With
With .Range("A7").Offset(nTickers + 2, 0)
Add the title for the expected return on the portfolio and enter the formula to compute its value:
.Value = "E(Rp)"
.Offset(0, 1).Name = "ERp"
.Offset(0, 1).Formula = "=Sumproduct(Weights,Means)"

10

Add the title for the target cell and set its starting value to zero. The target cell is used as the target for the
optimization
.Offset(0, 3).Value = "Target"
.Offset(0, 4).Value = 0
.Offset(0, 4).Name = "Target"
Add the title for the variance of the portfolio and enter the formula to compute its value:
.Offset(2, 0).Value = "Var(Rp)"
With .Offset(2, 1)
.FormulaArray = "=MMult(Weights,MMult(Covar,Transpose(Weights)))"
.Name = "VarRp"
End With
Add the title for the standard deviation of the portfolio and enter the formula to compute its value:
.Offset(2, 3).Value = "Sigma(Rp)"
With .Offset(2, 4)
.Formula = "=Sqrt(VarRp)"
.Name = "SigmaP"
End With
End With
End With
Add some formatting and end the subroutine:
Range("B4").Resize(nTickers + 8, nTickers + 1).NumberFormat = "0.0000"
Set FormatRange = ActiveSheet.UsedRange.SpecialCells(xlCellTypeConstants, 2)
With FormatRange
.Interior.Color = 13209
.Font.ThemeColor = xlThemeColorDark1
.Font.Bold = True
.HorizontalAlignment = xlCenter
.VerticalAlignment = xlCenter
End With
End Sub
The subroutine ChartDataSheet is used to call RunSolver and record the solutions for each optimization
on the ChartData sheet for charting. It starts with some additional variable declarations:
Sub ChartDataSheet_Setup()
Dim CDSheet As Worksheet
Dim nErr As Integer
Dim MVPRet As Double, Temp1 As Double, Temp2 As Double
Dim SameReturn As Boolean
Dim T1 As Double, T2 As Double, T3 As Double
Set CDSheet = Worksheets("ChartData")
Add some formatting and titles:
With CDSheet
.Select
.Cells.ClearContents

11

.Range("A:Z").Font.Bold = False
.Columns("A:A").ColumnWidth = 15.14
.Columns("B:B").ColumnWidth = 12.14
.Columns("C:C").ColumnWidth = 10.86
.Range("A1").Value = "Efficient Frontier"
.Range("B2").Value = "Port. Std.Dev."
.Range("C2").Value = "Port. Return"
.Range("E1").Value = "Optimal Portfolio Weights"
.Range(Cells(1, 5), Cells(1, nTickers + 4)).MergeCells = True
With .Range("D2")
For k = 1 To nTickers
.Offset(0, k).Value = Symbols(k)
Next
End With
End With
Find the highest return and the matching standard deviation - the endpoint of the efficient frontier:
With Sheets("BasicStats")
.Activate
For i = 1 To nTickers
If i = 1 Then
maxReturn = Range("A4").Offset(0, i).Value
maxStdDev = Range("A5").Offset(0, i).Value
maxTicker = Range("A3").Offset(0, i).Value
Else
If maxReturn < Range("A4").Offset(0, i).Value Then
maxReturn = Range("A4").Offset(0, i).Value
maxStdDev = Range("A5").Offset(0, i).Value
maxTicker = Range("A3").Offset(0, i).Value
End If
End If
Next i
End With
Number of times to run Solver to get a "nice" graph:
nRuns = 15
Compute the minimum variance portfolio:
Range("Target") = 0
Call RunSolver
Copy the expected return, standard deviation and weights to the appropriate cells:
With CDSheet.Range("A" & nRuns + 3)
.Value = "M-V Portfolio"
.Offset(0, 1).Value = Range("SigmaP").Value
.Offset(0, 2).Value = Range("ERp").Value
.Offset(0, 2).Name = "MVPReturn"
For k = 4 To nTickers + 3
.Offset(0, k).Value = Range("Weights").Cells(k - 3).Value
Next
End With
MVPRet = Range("MVPReturn").Value

12

MVPStdDev = Range("SigmaP").Value
Use an exponential function to select more points near the minimum variance point. The divisor "T1"
should scale between 0 and 1. The 1.5 is arbitrary and is used to get the graph to look appropriate:
T1 = 1 / Exp(1 / 1.5)
Run Solver "nRuns" times and record the results. Solver can make errors, so set the error counter to zero
and increment with each error:
For i = nRuns - 1 To 1 Step -1
nErr = 0
Set the target cell for each optimization by altering the standard deviation between the minimum variance
point and the maximum standard deviation:
T2 = 1 / Exp((i + 1) / 1.5)
T3 = T2 / T1
Range("Target") = MVPStdDev + T3 * (maxStdDev - MVPStdDev)
Run Solver with the new target cell. Since Solver can make errors, check for an error after each run and
re-run if necessary:
Do
Call RunSolver
With CDSheet.Range("B4")
.Offset(i - 1, 0).Value = Range("SigmaP").Value
.Offset(i - 1, 1).Value = Range("ERp").Value
End With
Make sure Solver did not get stuck and return an inferior point:
SameReturn = False
Temp1 = Round(CDSheet.Range("C4").Offset(i - 1, 0).Value, 4)
Temp2 = Round(CDSheet.Range("C4").Offset(i, 0).Value, 4)
If Temp1 <= Temp2 Then
SameReturn = True
Sheets("Model").Range("B4").Resize(, nTickers).Value = 0
nErr = nErr + 1
If nErr = 1 Then Sheets("Model").Range("B4") = 1
If nErr = 2 Then Sheets("Model").Range("C4") = 1
If nErr = 3 Then Sheets("Model").Range("D4") = 1
If nErr = 4 Then Sheets("Model").Range("E4") = 1
If nErr = 5 Then Sheets("Model").Range("F4") = 1
If nErr = 6 Then Sheets("Model").Range("B4").Resize(, nTickers).Value = 1/nTickers/2
If nErr = 7 Then Sheets("Model").Range("B4").Resize(, nTickers).Value = 1/nTickers*2
If nErr > 7 Then
Error = True
MsgBox "Error in Solver Solution"
Exit Sub
End If
End If
If the point is an inferior point, run Solver again with the same target cell:
Loop While Same Return

13

Write the optimal weights for each stock for each for each run:
With CDSheet.Range("D4")
For k = 1 To nTickers
.Offset(i - 1, k).Value = Range("Weights").Cells(k).Value
Next
End With
Next i
The maximum return is the last point on the efficient frontier:
CDSheet.Range("B3").Value = maxStdDev
CDSheet.Range("C3").Value = maxReturn
With CDSheet.Range("D2")
For k = 1 To nTickers
If .Offset(0, k).Value = maxTicker Then
.Offset(1, k).Value = 1
Else
.Offset(1, k).Value = 0
End If
Next
End With
Add some formatting and end the subroutine:
CDSheet.Activate
With Range("A1").Resize(nRuns + 3, nTickers + 4)
.NumberFormat = "0.0000"
.HorizontalAlignment = xlCenter
.VerticalAlignment = xlCenter
End With
Set FormatRange = ActiveSheet.UsedRange.SpecialCells(xlCellTypeConstants, 2)
With FormatRange
.Interior.Color = 13209
.Font.ThemeColor = xlThemeColorDark1
.Font.Bold = True
End With
End Sub
The subroutine RunSolver runs the Solver optimization routine:
Sub RunSolver()
Sheets("Model").Select
We include the code to run Solver directly but there can be conflicts if Solver is not correctly installed and
referenced. Application.Run does not have as many conflict issues as just using Solver but it runs slower
and is not as intuitive since the options are not referenced with words. For a good description of this issue
see http://peltiertech.com/Excel/SolverVBA.html. We start by resetting Solver:
SolverReset
The first constraint is that the weights must sum to 1. "Relation:=2" refers to "=":
SolverAdd CellRef:=Range("SumWeights"), _

14

Relation:=2, _
FormulaText:=1
The second constraint is that the portfolio standard deviation must equal the target cell:
SolverAdd CellRef:=Range("SigmaP"), _
Relation:=2, _
FormulaText:=Range("Target")
The maximization function will vary depending on the computation. In this example, we maximize the
portfolio expected return by changing the weights. A "MaxMinVal=1" refers to maximizing:
SolverOk SetCell:=Range("ERp"), _
MaxMinVal:=1, _
ByChange:=Range("Weights")
In this example we run Solver with all the options set to the default, (there is no Solver options line). Since
the Solver option AssumeNonNeg is set to True by default, it is not necessary to add the constraint that
the weights are >= 0 when short selling is not allowed. A complete list of options along with their default
values is available at the Microsoft Developer Library, http://msdn.microsoft.com/enus/library/office/ff195446.aspx. You can also find references to the other Solver functions on the same
page.
Run the optimization. "UserFinish:=True" completes the optimization without asking if the user want to
keep the optimization:
SolverSolve UserFinish:=True
Reset the status bar to empty and end the subroutine:
Application.StatusBar = Empty
End Sub
The UpdateChart subroutine plots the chart data. It also starts with the declaration of a few variables
specific to this subroutine:
Sub UpdateChart()
Dim minX As Single, maxX As Single
Dim minY As Single, maxY As Single
Dim xLength As Single, yLength As Single
Application.StatusBar = "Updating Chart"
Activate the chart data sheet, select the chart type, data range, location, style, labels format, axis
captions:
Sheets("ChartData").Activate
ActiveSheet.Shapes.AddChart.Select
ActiveChart.ChartType = xlXYScatterSmooth
ActiveChart.SetSourceData Source:=Range("ChartData!$B$3:$C$" & nRuns + 3)
ActiveChart.Location Where:=xlLocationAsNewSheet
ActiveChart.ChartStyle = 36
ActiveChart.ClearToMatchStyle
ActiveChart.SetElement msoElementLegendNone

15

ActiveChart.Location Where:=xlLocationAsNewSheet
ActiveChart.Axes(xlValue).TickLabels.NumberFormat = "0.00"
ActiveChart.Axes(xlCategory).TickLabels.NumberFormat = "0.00"
ActiveChart.SetElement (msoElementPrimaryCategoryAxisTitleAdjacentToAxis)
ActiveChart.Axes(xlCategory, xlPrimary).AxisTitle.Caption = _
"Standard Deviation of Portfolio Returns %"
ActiveChart.SetElement (msoElementPrimaryValueAxisTitleRotated)
ActiveChart.Axes(xlValue, xlPrimary).AxisTitle.Caption = "E(Rp) %"
ActiveChart.Name = "Graph"
Sheets("ChartData").Move Before:=Sheets("Graph")
Update the chart settings to improve chart spacing and end the subroutine:
minX = Application.WorksheetFunction.Min(Range("B3:B" & 18))
maxX = Application.WorksheetFunction.Max(Range("B3:B" & 18))
minY = Application.WorksheetFunction.Min(Range("C3:C" & 18))
maxY = Application.WorksheetFunction.Max(Range("C3:C" & 18))
xLength = maxX - minX
yLength = maxY - minY
Sheets("Graph").Select
With ActiveChart
With .Axes(xlCategory)
.MinimumScale = minX - 0.1 * xLength
.MaximumScale = maxX + 0.1 * xLength
End With
With .Axes(xlValue)
.MinimumScale = minY - 0.1 * yLength
.MaximumScale = maxY + 0.1 * yLength
End With
End With
Application.StatusBar = Empty
End Sub

16