Modern Portfolio Theory

Introduction

In 1952, Harry Markowitz published a seminal paper in the Journal of Finance titled Portfolio Selection, where he first introduced Modern Portfolio Theory (MPT), which quantifies the risk and return trade-off when constructing portfolios of risky assets. The theory formalizes the concept of diversification in mathematical terms, and suggests that rational investors seek to make investment decisions that maximize portfolio return for a given level of risk. It frames the investment problem not only as an asset selection challenge, but also of sizing the positions of each chosen risky asset in the portfolio. MPT also introduces the concept of systematic versus non-systematic risk, the latter of which can be diversified away in a well proportioned portfolio. Systematic risk refers to general market wide risks such as interest rate risk, business recessions or wars, while non-systematic risk (also known as specific risk) relates to the idiosyncratic risks associated with an individual security.

Modern Portfolio Theory is still in widespread use today in professional investment management circles, and remains one of the foundational frameworks used to build efficient risk adjusted portfolios. Markowitz was awarded the Nobel Memorial Prize in Economic Sciences in 1990 for his work on MPT.

Risk & Return

Before we look at an example, we first need to discuss the concepts of risk and return in the context of a portfolio of risky assets. It seems eminently reasonable to expect that a rational investor makes decisions so as to maximize investment return while minimizing the risk or uncertainty of that return. While return is an unambiguous concept, risk or uncertainty is not necessarily so easily quantified. In the context of MPT however, risk is defined as the variance of the portfolio returns, which is a function of the variance and covarinace of the individual asset returns in the portfolio. More on this later, but first let us discuss portfolio return.

Portfolio Return

Consider investment returns generated by a portfolio of risky assets. Portfolio return is simply the weighted sum of the returns on the individual assets in the portfolio, which can be expressed as per the equation below, where \(w_{i}\) represents the weight (percentage of capital) of asset i, \(r_{i}\) the return on asset i, and \(R_{p}\) the overall portfolio return.

$$ R_{p} = \sum_{i=1}^{n} w_{i} * r_{i} $$

In order to illustrate the validity of this expression, consider a two asset hypothetical portfolio. Let us assume we have $1000 of capital, and we have decided to invest in just two stocks (n=2), namely Amazon and Apple. For lack of a strong opinion, we simply split our investment 50/50 between the two names, and then over a year we see Apple return 15% and Amazon returns 8%. How much does our portfolio return?

Ignoring the above formula for the moment, we can easily calculate the profit from each $500 dollar investment, sum these profits and then calculate the resulting return on our $1000 initial investment, which comes out to be 11.5%. In general terms, the future value F of some monetary amount can be related to its present value P and a rate of return R as follows:

$$ F = P (1 + R) $$

Since we are ultimately trying to calculate the portfolio return, we need to write this expression in terms of the present value and the return on the individual assets in the portfolio. The expression below does just this, where \(p_{i}\) and \(r_{i}\) represent the present value and return of individual assets respectively, and subscript p denotes the portfolio value:

$$ F_{p} = \sum_{i=1}^{n} p_{i} (1 + r_{i}) \ where \ P_{p} = \sum_{i=1}^{n} p_{i} $$

Considering our two asset example, we can expand the equation to yield:

$$ \begin{align} F_{p} &= p_{1} (1 + r_{1}) + p_{2} ( 1 + r_{2}) \\ F_{p} &= p_{1} + p_{2} + p_{1} r_{1} + p_{2} r_{2} \\ F_{p} &= P_{p} + p_{1} r_{1} + p_{2} r_{2} \\ \end{align} $$

We know that from our original future value equation we can express the return of the portfolio as:

$$ R_{p} = \frac{F_{p}}{P_{p}} - 1 $$

Therefore if we divide the prior equation for our two asset scenario by the present value of the portfolio we get an expression for the portfolio return in terms of the individual asset returns, which is essentially the same as the weighted sum of the asset returns as defined earlier.

$$ R_{p} = \frac{F_{p}}{P_{p}} - 1 = \frac{p_{1} r_{1} + p_{2} r_{2}}{P_{p}} = w_{1} r_{1} + w_{2} r_{2} $$

Portfolio Risk

While portfolio return is conceptually unambiguous, defining portfolio risk is less clear cut. In the case of Modern Portfolio Theory, risk is defined as the variance or volatility of the returns, which is certainly consistent with intuition, since high variance implies a high degree of uncertainty. Portfolio returns that are extremely volatile are not only emotionally hard to stomach, but may force an investor to crystallize losses at a very inopportune time due to a requirement to gain access to capital. Long term investors often think of risk in different terms, and in fact Warren Buffet, who is arguably the greatest investor of all time, would probably not consider volatility as an appropriate metric. Instead, he would be more likely to think in terms of the potential for permanent loss of capital.

Few of us have the investment acumen or long horizon of Warren Buffet, so for mere mortals, let us stick with return volatility as our best measure of portfolio risk. While individual asset return volatility is simple to compute, calculating portfolio level risk is less trivial as it involves understanding the interaction of individual asset returns. That is to say, no two assets in a portfolio are likely to be 100% correlated, and therefore combining uncorrelated assets is bound to affect risk in ways that need to be quantified. Let us begin with our definition of portfolio return variance which using the expectation operator is just the usual expression for variance.

$$ \sigma^2 = E[ ( R_{p} - \bar{R}_{p} )^2 ] $$

In this equation \(r_{p}\) represents the return generated by an instance of the portfolio while \(\bar{r}_{p}\) represents the average expected return from this portfolio. To illustrate, consider a two asset portfolio where we expand the above expression to be in terms of the individual assets that make up the portfolio.

$$ \begin{align} \sigma^2 &= E[ ( R_{p} - \bar{R_{p}})^2 ] \\ \sigma^2 &= E[ ( w_{1} r_{1} + w_{2} r_{2} - (w_{1} \bar{r_{1}} + w_{2}\bar{r_{2}}) )^2 ] \\ \sigma^2 &= E[ ( w_{1} ( r_{1} - \bar{r_{1}} ) + w_{2} ( r_{2} - \bar{r_{2}}))^2 ] \\ \sigma^2 &= E[ w_{1}^2 ( r_{1} - \bar{r_{1}} )^2 + w_{2}^2 ( r_{2} - \bar{r_{2}})^2 + 2 w_{1} w_{2} E[(r_{1} - \bar{r_{1}})(r_{2} - \bar{r_{2}})]] \\ \end{align} $$

Since the asset weights are not stochastic in nature, we can take them outside of the expectation operator to yield the following:

$$ \sigma^2 = w_{1}^2 E[( r_{1} - \bar{r_{1}})^2] + w_{2}^2 E[( r_{2} - \bar{r_{2}})^2] + 2 w_{1} w_{2} E[(r_{1} - \bar{r_{1}})(r_{2} - \bar{r_{2}})]] $$

It now becomes clear that the portfolio variance is a function of the individual asset variances as well as their covariance, so we can write the same expression in a more concise manner as shown below. Notice that this expression suggests that if we combine risky assets in a portfolio that have a negative covariance, the overall portfolio risk will be reduced. This is essentially diversification quantified, and it is a central principle of MPT.

$$ \sigma^2 = w_{1}^2 \sigma_{1}^2 + w_{2}^2 \sigma_{2}^2 + 2 w_{1} w_{2} \sigma_{12} $$

While the above derivation is for a 2 asset portfolio, this expression can be generalized to an N asset portfolio and written in matrix form as below. The w term represents an nx1 vector of asset weights, and capital sigma represents the nxn covariance matrix of asset returns, the diagonal elements of which are the individual asset return variances.

$$ \sigma^2 = w^T \Sigma w $$

Sharpe Ratio

The Sharpe Ratio is a widely used performance metric in finance and is named after Nobel Laureate William F. Sharpe who first developed it. The ratio is basically the average return earned in excess of the risk free rate per unit of volatility, and is therefore a standardized way of expressing risk adjusted return. Mathematically, it is defined as follows (where \(R_{p}\) represents portfolio return, \(\bar{R_{f}}\) is the risk free return and \(\sigma_{p}\) is portfolio risk):

$$ S_{p} = \frac{E[ R_{p} - \bar{R_{f}}]}{\sigma_{p}} $$

A Sharpe Ratio can be computed ex-ante based on forecasts of expected risk and return, or otherwise as an ex-post measure based on realized returns. Given that the return measure (the stuff we like) is in the numerator and the risk measure (the stuff we don't like) is in the denominator, the higher the Sharpe Ratio the better.

While it is very widely used, the Sharpe Ratio is not without its detractors. One of the often quoted grievances with the measure is that it treats upside volatility equally with downside volatility. This may be reasonable in a long / short portfolio, but perhaps less so in a long-only portfolio (i.e. no shorting constraint). In order to address this, a variation of the Sharpe Ratio exists called the Sortino Ratio which only includes returns below a certain threshold when computing volatility.

Another potential concern is that the Sharpe Ratio does not distinguish between systematic and non-systematic risk, the latter of which can be diversified away in a well balanced portfolio. The Treynor Ratio attempts to address this by estimating the systematic risk only, and using that as the denominator in the above expression.

Finally, one of the trickiest issues with the Sharpe Ratio is scaling it to different time horizons. For example, how do you compute an annualized Sharpe from daily returns? Most techniques scale risk and return on the assumption that returns are normally distributed, however it is well known that asset returns are not normal and exhibit excess kurtosis and often skewness. For more details on some of the challenges in computing Sharpe Ratios, I high recommend a paper by Andrew W. Lo titled The Statistics of Sharpe Ratios.

Examples

Now that we know how to calculate portfolio return and risk, let us look at how we can apply this knowledge, and more importantly, demonstrate how we can use Modern Portfolio Theory to construct an efficient investment portfolio of risky assets. The examples in the following sections leverage the Morpheus data source adapter for Yahoo Finance, more details of which can be found here. The library is available on Maven Central and can therefore be added to your build tool of choice:

<dependency>
    <groupId>com.zavtech</groupId>
    <artifactId>morpheus-yahoo</artifactId>
    <version>${VERSION}</version>
</dependency>

Two Assets

Consider a two asset portfolio where we have already convinced ourselves that we want to buy Apple and Amazon, but we are not sure how much of our capital to invest in each. We obviously do not know what the future may bring, but let us look back over the past year to see what happened on the assumption that it may help influence our decision. The plot below shows the cumulative returns of both securities over the past year, which clearly demonstrates what a great run they have had. With the benefit of hindsight of course, you would have invested all your capital in Apple as it outperformed Amazon by some margin. Looking forward however, the performance of these stocks could be reversed, so we should probably spread our bets across the two.

The code to generate this plot is as follows:

LocalDate end = LocalDate.now();
LocalDate start = end.minusYears(1);
Array<String> tickers = Array.of("AAPL", "AMZN");

YahooFinance yahoo = new YahooFinance();
DataFrame<LocalDate,String> cumReturns = yahoo.getCumReturns(start, end, tickers);
cumReturns.applyDoubles(v -> v.getDouble() * 100d);

Chart.create().withLinePlot(cumReturns, chart -> {
    chart.title().withText("Cumulative Asset Returns");
    chart.subtitle().withText("Range: (" + start + " to" + end + ")");
    chart.plot().axes().domain().label().withText("Date");
    chart.plot().axes().range(0).label().withText("Return");
    chart.plot().axes().range(0).format().withPattern("0.00'%';-0.00'%'");
    chart.show();
});

To get a sense of the risk / return characteristics of our two asset portfolio, we are going to generate 10,000 random portfolios where we invest different amounts in Apple and Amazon ranging from 0% to 100% of our capital in one asset and the remainder in the other asset. The function below returns a DataFrame of random portfolio weights in N assets that sum to 1 in all cases, and therefore each portfolio represents a fully invested scenario.

/**
 * A function that generates N long only random portfolios with weights that sum to 1
 * @param count     the number of portfolios / rows in the DataFrame
 * @param tickers   the security tickers to include
 * @return          the frame of N random portfolios, 1 per row
 */
DataFrame<Integer,String> randomPortfolios(int count, Iterable<String> tickers) {
    DataFrame<Integer,String> weights = DataFrame.ofDoubles(Range.of(0, count), tickers);
    weights.applyDoubles(v -> Math.random());
    weights.rows().forEach(row -> {
        final double sum = row.stats().sum();
        row.applyDoubles(v -> {
            double weight = v.getDouble();
            return weight / sum;
        });
    });
    return weights;
}

In order to calculate the risk and return characteristics of these 10,000 portfolios, we first need to compute the covariance matrix of the returns between Apple and Amazon, and then also compute the cumulative asset returns over the historical horizon in question. With these quantities, it is fairly simple to compute the risk & return of each portfolio, which can then be plotted on a scatter chart to see how they compare. The resulting plot is shown below, and is followed by the code to generate it. Note that since we are using daily returns to compute the covariance matrix, we need to annualize it, which we do by multiplying by 252 (on the assumption there are 252 trading days in the year). Since the cumulative returns are based on a 1-year look back window, there is no need to annualize the returns.

//Define portfolio count, investment horizon and universe
int count = 10000;
LocalDate end = LocalDate.now();
LocalDate start = end.minusYears(1);
Array<String> tickers = Array.of("AAPL", "AMZN");

//Grab daily returns and cumulative returns from Yahoo Finance
YahooFinance yahoo = new YahooFinance();
DataFrame<LocalDate,String> dayReturns = yahoo.getDailyReturns(start, end, tickers);
DataFrame<LocalDate,String> cumReturns = yahoo.getCumReturns(start, end, tickers);

//Compute asset covariance matrix from daily returns and annualize
DataFrame<String,String> sigma = dayReturns.cols().stats().covariance().applyDoubles(v -> v.getDouble() * 252);
DataFrame<LocalDate,String> assetReturns = cumReturns.rows().last().map(DataFrameRow::toDataFrame).get();

//Generate random portfolios and compute risk & return for each
DataFrame<Integer,String> portfolios = randomPortfolios(count, tickers);
DataFrame<Integer,String>  results = DataFrame.ofDoubles(Range.of(0, count), Array.of("Risk", "Return"));
portfolios.rows().forEach(row -> {
    DataFrame<Integer,String> weights = row.toDataFrame();
    double portReturn = weights.dot(assetReturns.transpose()).data().getDouble(0, 0);
    double portVariance = weights.dot(sigma).dot(weights.transpose()).data().getDouble(0, 0);
    results.data().setDouble(row.key(), "Return", portReturn * 100d);
    results.data().setDouble(row.key(), "Risk", Math.sqrt(portVariance) * 100d);
});

//Plot the results using a scatter plot
Chart.create().withScatterPlot(results, false, "Risk", chart -> {
    chart.title().withText("Risk / Return Profiles For AAPL+AMZN Portfolios");
    chart.subtitle().withText(count + " Portfolio Combinations Simulated");
    chart.plot().axes().domain().label().withText("Portfolio Risk");
    chart.plot().axes().domain().format().withPattern("0.00'%';-0.00'%'");
    chart.plot().axes().range(0).label().withText("Portfolio Return");
    chart.plot().axes().range(0).format().withPattern("0.00'%';-0.00'%'");
    chart.show();
});

There are a few notable observations about the plot as follows:

A rational investor clearly wants to be on the upper part of this curve for a given level of risk. For example, at 17% volatility, you could either have achieved 23.5% return with a portfolio on the lower segment of the curve or close to 37% on the upper part. Which one do you prefer? The upper part of the curve is referred to as the Efficient Frontier, and moreover, there exists a special case on this curve often referred to as the Markowitz Portfolio, which has the highest risk adjusted return of all the candidate portfolios. It is essentially the mean-variance optimal portfolio and can be calculated by maximizing the utility defined by the objective function below, subject to whatever constraints you may impose (in this case we assume long only and fully invested):

$$ U_{p} = w^T r - w^T \Sigma w $$

The \(w\) and \(r\) terms represent nx1 vectors of asset weights and returns respectively, while capital sigma represents the nxn asset covariance matrix. This objective function is essentially saying we like return and do not like risk, and our goal is to select an nx1 vector of asset weights that maximizes this expression given asset returns and a covariance matrix. In this example, we are looking back at a historical scenario so we can calculate returns and the covariance based on realized market prices. In reality, we need to make judgements about the future, and therefore are required to estimate future returns and covariance, which is where things can get tricky. Calculating the Markowitz Portfolio given constraints (as in this case where we impose a long only fully invested constraint such that the elements of W are all positive and sum to 1) is a quadratic optimization problem that requires appropriate software and is beyond the scope of this article. A good commercial package to consider is MOSEK, or for an Open Source solution you may consider OptaPlanner.

Asset Selection

Modern Portfolio Theory is fundamentally about sizing positions of risky assets in a portfolio, it is not about asset selection. Having said that, it can be useful to compare the risk / return profiles of portfolios constructed from different risky assets. In the prior example we had already decided that we wanted to invest in Apple and Amazon, but were there better two asset portfolio combinations we should have considered? The plot below, followed by the code that generated it, is essentially an extension of the prior example where in this case we generate multiple 10,000 portfolio combinations with different asset constituents to see how they compare.

It is clear from the chart that the 5 two asset portfolios in this example have very different risk / return profiles, with the Apple / Amazom combination being the most risky, but certainly having some of the highest returns. With that being said, if your investment objective was to achieve around a 12.0% return (very aspirational in today's world of zero interest rates), your best bet would be to choose the VTI / BND combination as it appears to have the potential to generate this return for less than 5% risk. Compare this to some of the other portfolio combinations for which you need to accept a much higher level of risk to achieve the same return.

//Define portfolio count, investment horizon
int count = 10000;
LocalDate end = LocalDate.now();
LocalDate start = end.minusYears(1);
YahooFinance yahoo = new YahooFinance();

Array<DataFrame<Integer,String>> results = Array.of(
    Array.of("VTI", "BND"),
    Array.of("AAPL", "AMZN"),
    Array.of("GOOGL", "BND"),
    Array.of("ORCL", "KO"),
    Array.of("VWO", "VNQ")
).map(v -> {
    //Access tickers
    Array<String> tickers = v.getValue();
    //Grab daily returns and cumulative returns from Yahoo Finance
    DataFrame<LocalDate,String> dayReturns = yahoo.getDailyReturns(start, end, tickers);
    DataFrame<LocalDate,String> cumReturns = yahoo.getCumReturns(start, end, tickers);
    //Compute asset covariance matrix from daily returns and annualize
    DataFrame<String,String> sigma = dayReturns.cols().stats().covariance().applyDoubles(x -> x.getDouble() * 252);
    DataFrame<LocalDate,String> assetReturns = cumReturns.rows().last().map(DataFrameRow::toDataFrame).get();
    //Generate random portfolios and compute risk & return for each
    String label = String.format("%s+%s", tickers.getValue(0), tickers.getValue(1));
    DataFrame<Integer,String> portfolios = randomPortfolios(count, tickers);
    DataFrame<Integer,String>  riskReturn = DataFrame.ofDoubles(Range.of(0, count), Array.of("Risk", label));
    portfolios.rows().forEach(row -> {
        DataFrame<Integer,String> weights = row.toDataFrame();
        double portReturn = weights.dot(assetReturns.transpose()).data().getDouble(0, 0);
        double portVariance = weights.dot(sigma).dot(weights.transpose()).data().getDouble(0, 0);
        riskReturn.data().setDouble(row.key(), label, portReturn * 100d);
        riskReturn.data().setDouble(row.key(), "Risk", Math.sqrt(portVariance) * 100d);
    });
    return riskReturn;
});

DataFrame<Integer,String> first = results.getValue(0);
Chart.create().<Integer,String>withScatterPlot(first, false, "Risk", chart -> {
    for (int i=1; i<results.length(); ++i) {
        chart.plot().<String>data().add(results.getValue(i), "Risk");
        chart.plot().render(i).withDots();
    }
    chart.plot().axes().domain().label().withText("Portfolio Risk");
    chart.plot().axes().domain().format().withPattern("0.00'%';-0.00'%'");
    chart.plot().axes().range(0).label().withText("Portfolio Return");
    chart.plot().axes().range(0).format().withPattern("0.00'%';-0.00'%'");
    chart.title().withText("Risk / Return Profiles of Various Two Asset Portfolios");
    chart.subtitle().withText(count + " Portfolio Combinations Simulated");
    chart.legend().on().right();
    chart.show();
});

Multiple Assets

So far we have limited our example portfolios to two assets in order to help develop the intuition behind Modern Portfolio Theory. In reality however, real world portfolios are likely to include more assets, although exactly how many are required to achieve a reasonable level of diversification is open to debate. Consider an investable universe of 6 securities represented by broad based low cost ETFs that serve as reasonable proxies for major asset classes. The table below summarizes these candidates.

Ticker Name Provider
VWO Vanguard FTSE Emerging Markets ETF Vanguard Details
VNQ Vanguard REIT ETF Vanguard Details
VEA Vanguard FTSE Developed Markets ETF Vanguard Details
DBC PowerShares DB Commodity Tracking ETF Powershares Details
VTI Vanguard Total Stock Market ETF Vanguard Details
BND Vanguard Total Bond Market ETF Vanguard Details

To get a sense of how the risk / return profiles of portfolios evolve as we include more assets, we can generate 10,000 random portfolios first with 2 assets, then 3 and all the way to 6. In the case of two assets, we expect to see our rotated parabola, but what happens when you have more degrees of freedom in the portfolio? The plot below is the answer. As we go beyond two assets, the scatter becomes more pronounced, and while risk is generally reduced due to the fact that the assets are not perfectly correlated, return also suffers somewhat. Having said that, the Efficient Frontiers of the more diversified portfolios appear to provide a better risk / return trade off than the two asset portfolio.

The code to generate the above plot of 5 lots of 10,000 portfolios with various assets is as follows:

//Define portfolio count, investment horizon
int count = 10000;
LocalDate end = LocalDate.now();
LocalDate start = end.minusYears(1);
YahooFinance yahoo = new YahooFinance();

Array<DataFrame<Integer,String>> results = Array.of(
    Array.of("VWO", "VNQ"),
    Array.of("VWO", "VNQ", "VEA"),
    Array.of("VWO", "VNQ", "VEA", "DBC"),
    Array.of("VWO", "VNQ", "VEA", "DBC", "VTI"),
    Array.of("VWO", "VNQ", "VEA", "DBC", "VTI", "BND")
).map(v -> {
    //Access tickers
    Array<String> tickers = v.getValue();
    //Grab daily returns and cumulative returns from Yahoo Finance
    DataFrame<LocalDate,String> dayReturns = yahoo.getDailyReturns(start, end, tickers);
    DataFrame<LocalDate,String> cumReturns = yahoo.getCumReturns(start, end, tickers);
    //Compute asset covariance matrix from daily returns and annualize
    DataFrame<String,String> sigma = dayReturns.cols().stats().covariance().applyDoubles(x -> x.getDouble() * 252);
    DataFrame<LocalDate,String> assetReturns = cumReturns.rows().last().map(DataFrameRow::toDataFrame).get();
    //Generate random portfolios and compute risk & return for each
    String label = String.format("%s Assets", tickers.length());
    DataFrame<Integer,String> portfolios = randomPortfolios(count, tickers);
    DataFrame<Integer,String>  riskReturn = DataFrame.ofDoubles(Range.of(0, count), Array.of("Risk", label));
    portfolios.rows().forEach(row -> {
        DataFrame<Integer,String> weights = row.toDataFrame();
        double portReturn = weights.dot(assetReturns.transpose()).data().getDouble(0, 0);
        double portVariance = weights.dot(sigma).dot(weights.transpose()).data().getDouble(0, 0);
        riskReturn.data().setDouble(row.key(), 1, portReturn * 100d);
        riskReturn.data().setDouble(row.key(), 0, Math.sqrt(portVariance) * 100d);
    });
    return riskReturn;
});

DataFrame<Integer,String> first = results.getValue(0);
Chart.create().<Integer,String>withScatterPlot(first, false, "Risk", chart -> {
    for (int i=1; i<results.length(); ++i) {
        chart.plot().<String>data().add(results.getValue(i), "Risk");
        chart.plot().render(i).withDots();
    }
    chart.plot().axes().domain().label().withText("Portfolio Risk");
    chart.plot().axes().domain().format().withPattern("0.00'%';-0.00'%'");
    chart.plot().axes().range(0).label().withText("Portfolio Return");
    chart.plot().axes().range(0).format().withPattern("0.00'%';-0.00'%'");
    chart.title().withText("Risk / Return Profiles of Portfolios With Increasing Assets");
    chart.subtitle().withText(count + " Portfolio Combinations Simulated");
    chart.legend().on().bottom();
    chart.show();
});

Robo-Advisor

A fairly recent innovation in the investment management space relates to what are called Robo-Advisors, which are essentially online investment solutions aimed mostly at retail investors. They are called Robo-Advisors because they automate the construction of portfolios using software based on an investor's risk appetite and their investment objective, which they assess by posing a number of questions via their website. There are already many players in this space, and most of them construct well-balanced portfolios consisting of 5-7 assets, all of which are broad based and low cost Exchange Traded Funds.

To avoid any suggestion that I am endorsing or recommending these services, I am consciously avoiding naming names, but I visited the website of one of the larger advisors and proceeded to complete the questionnaire after which it proposed the portfolio in the table below. The purpose of this discussion is not to make any judgement on how good this portfolio is versus other potential investments, but really to get a sense of how efficient the proposed portfolio is from a risk / return stand point.

Ticker Name Weight Provider Details
VTI Vanguard Total Stock Market ETF 35% Vanguard Details
VEA Vanguard FTSE Developed Markets ETF 21% Vanguard Details
VWO Vanguard FTSE Emerging Markets ETF 16% Vanguard Details
VTEB Vanguard Tax-Exempt Bond ETF 15% Vanguard Details
VIG Vanguard Dividend Appreciation ETF 8% Vanguard Details
XLE Energy Select Sector SPDR ETF 5% State Street Details

Ramdom Portfolios

Ignoring the proposed weightings for the moment, consider generating 10,000 long-only fully invested random portfolios involving these assets, and then computing the resulting equity curves. This will give us a sense of the various scenarios we can generate with this asset universe, and in particular allow us to understand the degree of dispersion in outcomes. The plot below illustrates 10,000 equity curves based on the past 1 year returns, suggesting a return spread ranging from approximately 2.5% all the way to 18.15%, which is pretty enormous.

The code to generate the above plot is as follows:

int portfolioCount = 10000;
Range<LocalDate> range = Range.of(LocalDate.now().minusYears(1), LocalDate.now());
Array<String> tickers = Array.of("VTI", "BND", "VWO", "VTEB", "VIG", "XLE");
DataFrame<Integer,String> portfolios = randomPortfolios(portfolioCount, tickers);
DataFrame<LocalDate,String> performance = getEquityCurves(range, portfolios);
Chart.create().withLinePlot(performance.applyDoubles(v -> v.getDouble() * 100d), chart -> {
    chart.title().withText(portfolioCount + " Equity Curves (Past 1 Year Returns)");
    chart.subtitle().withText("Robo-Advisor Universe: VTI, BND, VWO, VTEB, VIG, XLE");
    chart.plot().axes().domain().label().withText("Date");
    chart.plot().axes().range(0).label().withText("Portfolio Return");
    chart.plot().axes().range(0).format().withPattern("0.00'%';-0.00'%'");
    chart.show();
});

The example uses the following function in order to compute the equity curves for the DataFrame of 10,000 random portfolios. This function expects an mxn frame of portfolio weighting configurations where m is the number of portfolios and n the number of assets. The resulting DataFrame has txm dimensions where t is the number of dates, and the m columns are labelled P0, P1 through to P(m).

/**
 * Calculates equity curves over a date range given a frame on initial portfolio weight configurations
 * @param range         the date range for historical returns
 * @param portfolios    MxN DataFrame of portfolio weights, M portfolios, N assets
 * @return              the cumulative returns for each portfolio, TxM, portfolios labelled P0, P1 etc...
 */
DataFrame<LocalDate,String> getEquityCurves(Range<LocalDate> range, DataFrame<Integer,String> portfolios) {
    final YahooFinance yahoo = new YahooFinance();
    final Iterable<String> tickers = portfolios.cols().keyArray();
    final DataFrame<LocalDate,String> cumReturns = yahoo.getCumReturns(range.start(), range.end(), tickers);
    final Range<String> colKeys = Range.of(0, portfolios.rowCount()).map(i -> "P" + i);
    return DataFrame.ofDoubles(cumReturns.rows().keyArray(), colKeys, v -> {
        double totalReturn = 0d;
        for (int i=0; i<portfolios.colCount(); ++i) {
            final double weight = portfolios.data().getDouble(v.colOrdinal(), i);
            final double assetReturn = cumReturns.data().getDouble(v.rowOrdinal(), i);
            totalReturn += (weight * assetReturn);
        }
        return totalReturn;
    });
}

Proposed Portfolio

Given the proposed portfolio weights presented earlier, we can use the past 1 year of returns for the assets in question to assess what the risk and return of this portfolio would have been had we invested a year ago. The code below performs this analysis and suggests that the portfolio returned a very respectable 14.9% for 7.0% risk, which is pretty phenomenal (a 2 Sharpe portfolio over the past year). The code to generate these results is as follows:

//Defines investment horizon & universe
LocalDate end = LocalDate.now();
LocalDate start = end.minusYears(1);
//Define investment universe and weights
Array<String> tickers = Array.of("VTI", "VEA", "VWO", "VTEB", "VIG", "XLE");
//Define DataFrame of position weights suggested by Wealthfront
DataFrame<String,String> portfolio = DataFrame.of(tickers, String.class, columns -> {
    columns.add("Weights", Array.of(0.35d, 0.21d, 0.16d, 0.15d, 0.08d, 0.05d));
});
//Grab daily returns and cumulative returns from Yahoo Finance
YahooFinance yahoo = new YahooFinance();
DataFrame<LocalDate,String> dayReturns = yahoo.getDailyReturns(start, end, tickers);
DataFrame<LocalDate,String> cumReturns = yahoo.getCumReturns(start, end, tickers);
//Compute asset covariance matrix from daily returns and annualize
DataFrame<String,String> sigma = dayReturns.cols().stats().covariance().applyDoubles(x -> x.getDouble() * 252);
DataFrame<LocalDate,String> assetReturns = cumReturns.rows().last().map(DataFrameRow::toDataFrame).get();
//Generate DataFrame of portfolio weights
double portReturn = portfolio.transpose().dot(assetReturns.transpose()).data().getDouble(0, 0);
double portVariance = portfolio.transpose().dot(sigma).dot(portfolio).data().getDouble(0, 0);
IO.println(String.format("Portfolio Return: %s", portReturn));
IO.println(String.format("Portfolio Risk: %s", Math.sqrt(portVariance)));

Now that we know how the currently proposed portfolio performed over the past year, let us generate 100,000 random portfolios in these 6 assets to get a sense of the overall risk / return profile of this universe, and see how the proposed portfolio compares. The plot below suggests that the proposed portfolio (green dot) is indeed pretty efficient, and may also suggest that this particular Robo-Advisor's future expectations are not that different from the past year.

One may wonder why the proposed portfolio is not exactly on the Efficient Frontier, and there are several explanations for this. The first is that the Robo-Advisor proposed weights are for a forward looking portfolio, and our example is using past 1 year returns to test its efficiency. Secondly, there are an infinite number of ways of estimating an ex-ante covariance matrix, which may involve exponentially smoothing returns (perhaps using different half-lives to estimate diagonal versus off-diagonal terms), and perhaps applying shrinkage to off-diagonal terms to improve stability. In our example, we have the benefit of hindsight and simply compute an ex-post covariance matrix, doing nothing fancy at all. Finally, the Robo-Advisor may impose risk constraints (such as capping any one position to be no more than 35% of the portfolio perhaps), which may penalize the strategy versus a less constrained instance, but which may be a very sensible compromise.

The code to generate this plot is as follows:

//Define portfolio count, investment horizon and universe
int count = 100000;
LocalDate end = LocalDate.now();
LocalDate start = end.minusYears(1);
Array<String> tickers = Array.of("VTI", "VEA", "VWO", "VTEB", "VIG", "XLE");

//Grab daily returns and cumulative returns from Yahoo Finance
YahooFinance yahoo = new YahooFinance();
DataFrame<LocalDate,String> dayReturns = yahoo.getDailyReturns(start, end, tickers);
DataFrame<LocalDate,String> cumReturns = yahoo.getCumReturns(start, end, tickers);

//Compute asset covariance matrix from daily returns and annualize
DataFrame<String,String> sigma = dayReturns.cols().stats().covariance().applyDoubles(v -> v.getDouble() * 252);
DataFrame<LocalDate,String> assetReturns = cumReturns.rows().last().map(DataFrameRow::toDataFrame).get();

//Generate random portfolios and compute risk & return for each
DataFrame<Integer,String> portfolios = randomPortfolios(count, tickers);
DataFrame<Integer,String>  results = DataFrame.ofDoubles(Range.of(0, count), Array.of("Risk", "Random"));
portfolios.rows().forEach(row -> {
    DataFrame<Integer,String> weights = row.toDataFrame();
    double portReturn = weights.dot(assetReturns.transpose()).data().getDouble(0, 0);
    double portVariance = weights.dot(sigma).dot(weights.transpose()).data().getDouble(0, 0);
    results.data().setDouble(row.key(), "Random", portReturn * 100d);
    results.data().setDouble(row.key(), "Risk", Math.sqrt(portVariance) * 100d);
});

//Create DataFrame with risk / return of proposed Wealthfront portfolio
DataFrame<Integer,String> proposed = DataFrame.of(Range.of(0, 1), String.class, cols -> {
    cols.add("Risk", Array.of(0.068950 * 100d));
    cols.add("Chosen", Array.of(0.1587613 * 100d));
});

//Plot the results using a scatter plot
Chart.create().withScatterPlot(results, false, "Risk", chart -> {
    chart.title().withText("Risk / Return Profile For Wealthfront Portfolio");
    chart.subtitle().withText(count + " Portfolio Combinations Simulated");
    chart.plot().<String>data().add(proposed, "Risk");
    chart.plot().render(1).withDots();
    chart.plot().axes().domain().label().withText("Portfolio Risk");
    chart.plot().axes().domain().format().withPattern("0.00'%';-0.00'%'");
    chart.plot().axes().range(0).label().withText("Portfolio Return");
    chart.plot().axes().range(0).format().withPattern("0.00'%';-0.00'%'");
    chart.legend().on().right();
    chart.show();
});

Efficiency

The previous section established that the proposed portfolio was reasonably efficient in the context of the past 1 year of returns (how efficient it will be over the next year is of course impossible to know, but all else being equal, it seems a decent configuration). In this section, we further consider its relative efficiency by looking at the equity curve of the best and worst portfolios that we find in our 100,000 random candidates (based on their Sharpe Ratios). In addition, we overlay the equity curve of the S&P500 by using the SPY Exchange Traded Fund as a proxy.

The plot below shows that the proposed portfolio actually outperformed what is labelled as the Best portfolio in pure return space, but not in risk-adjusted terms. That is, it yields a lower return per unit of risk than the best portfolio, and the fact that it outperformed in return space is just a fluke. The worst portfolio is a genuine shocker, and in fact spent much of the past year going nowhere before bouncing back starting in August.

The code to generate this plot is as follows, and leverages the getEquityCurves() function discussed earlier.

int portfolioCount = 1000;
Range<LocalDate> range = Range.of(LocalDate.now().minusYears(1), LocalDate.now());
Array<String> tickers = Array.of("VTI", "VEA", "VWO", "VTEB", "VIG", "XLE");
Array<Double> proposed = Array.of(0.35d, 0.21d, 0.16d, 0.15d, 0.08d, 0.05d);
DataFrame<Integer,String> portfolios = randomPortfolios(portfolioCount, tickers);
portfolios.rowAt(0).applyDoubles(v -> proposed.getDouble(v.colOrdinal()));
//Compute risk / return / sharpe for random portfolios
DataFrame<Integer,String> riskReturn = calcRiskReturn(range, portfolios).rows().sort(false, "Sharpe");
//Capture portfolio keys for chosen, best and worst portfolio based on sharpe
DataFrame<Integer,String> candidates = portfolios.rows().select(0,
    riskReturn.rows().first().get().key(),
    riskReturn.rows().last().get().key()
);
//Compute equity curves for chosen, best and worst
DataFrame<LocalDate,String> equityCurves = getEquityCurves(range, candidates).cols().mapKeys(col -> {
    switch (col.ordinal()) {
        case 0: return "Chosen";
        case 1: return "Best";
        case 2: return "Worst";
        default: return col.key();
    }
});
//Capture returns of S&P 500
YahooFinance yahoo = new YahooFinance();
DataFrame<LocalDate,String> spy = yahoo.getCumReturns(range.start(), range.end(), "SPY");

//Plot the equity curves
Chart.create().withLinePlot(equityCurves.applyDoubles(v -> v.getDouble() * 100d), chart -> {
    chart.title().withText("Best/Worst/Chosen Equity Curves (Past 1 Year Returns) + SPY");
    chart.subtitle().withText("Robo-Advisor Universe: VTI, VEA, VWO, VTEB, VIG, XLE");
    chart.plot().<String>data().add(spy.times(100d));
    chart.plot().axes().domain().label().withText("Date");
    chart.plot().axes().range(0).label().withText("Return");
    chart.plot().axes().range(0).format().withPattern("0.00'%';-0.00'%'");
    chart.plot().style("Chosen").withColor(Color.BLACK).withLineWidth(1.5f);
    chart.plot().style("Best").withColor(Color.GREEN.darker().darker()).withLineWidth(1.5f);
    chart.plot().style("Worst").withColor(Color.RED).withLineWidth(1.5f);
    chart.plot().style("SPY").withColor(Color.BLUE);
    chart.legend().on();
    chart.show();
});

The implementation of calcRiskReturn() in this example is as follows:

/**
 * Returns a DataFrame containing risk, return and sharpe ratio for portfolios over the date range
 * @param range         the date range for historical returns
 * @param portfolios    the DataFrame of portfolio weights, one row per portfolio configuration
 * @return              the DataFrame with risk, return and sharpe
 */
DataFrame<Integer,String> calcRiskReturn(Range<LocalDate> range, DataFrame<Integer,String> portfolios) {
    YahooFinance yahoo = new YahooFinance();
    Array<String> tickers = portfolios.cols().keyArray();
    DataFrame<LocalDate,String> dayReturns = yahoo.getDailyReturns(range.start(), range.end(), tickers);
    DataFrame<LocalDate,String> cumReturns = yahoo.getCumReturns(range.start(), range.end(), tickers);
    //Compute asset covariance matrix from daily returns and annualize
    DataFrame<String,String> sigma = dayReturns.cols().stats().covariance().applyDoubles(x -> x.getDouble() * 252);
    DataFrame<LocalDate,String> assetReturns = cumReturns.rows().last().map(DataFrameRow::toDataFrame).get();
    //Prepare 3 column DataFrame to capture results
    DataFrame<Integer,String>  riskReturn = DataFrame.ofDoubles(
        portfolios.rows().keyArray(),
        Array.of("Risk", "Return", "Sharpe")
    );
    portfolios.rows().forEach(row -> {
        DataFrame<Integer,String> weights = row.toDataFrame();
        double portReturn = weights.dot(assetReturns.transpose()).data().getDouble(0, 0);
        double portVariance = weights.dot(sigma).dot(weights.transpose()).data().getDouble(0, 0);
        riskReturn.data().setDouble(row.key(), "Return", portReturn * 100d);
        riskReturn.data().setDouble(row.key(), "Risk", Math.sqrt(portVariance) * 100d);
        riskReturn.data().setDouble(row.key(), "Sharpe", portReturn / Math.sqrt(portVariance));
    });
    return riskReturn;
}