Portfolio Optimization using Gradient Descent

You should do 60/40 allocation in stocks/bond.
You should diversify your portfolio.
You should buy this, you should buy that.

Okay, those are the things we heard a lot when we start to invest.
But what is the mathematically optimal portfolio where you get the best returns while taking the least risk?

How to measure how is your portfolio performing?

If you measure solely based on return, just leverage your positions then you can get what you wanted.

But like a lot of things in life, things are not that simple.
In investment, you care about the risk you take also.
In this context, the proxy to risk is the volatility of an asset.
Though there are controversy to the measure of risk using volatility, but that is the most easily obtained quantitative metric that we can get to measure risk.

Hence, the way to measure your portfolio performance is the return of investment per unit of risk.
Or as we call it, the Sharpe ratio

\begin{equation}
\text{Sharpe Ratio} = \frac{E[R_p - R_f]}{\sqrt{Var[R_p - R_f]}}
\end{equation}

Optimization using gradient descent method

Consider that we have a 3 fund portfolio,
The return of a portfolio will be the weighted sum of the return minus the risk free rate return

Total Return = w_1R_1 + w_2R_2 + w_3R_3 - R_f

Where the volatility is the weigthed sum of an asset’s volatility and the covariance between assets

Volatility =\sqrt{w_1^2 \sigma_1^2 .w_2^2 \sigma_2^2. w_3^2 \sigma_3^2  + 2w_1w_2cov(1,2) + 2w_2w_3cov(2,3) + 2w_1w_3cov(1,3)}

Hence,

\text f(w) = \frac{w_1R_1 + w_2R_2 + w_3R_3 - R_f}{\sqrt{w_1^2 \sigma_1^2 .w_2^2 \sigma_2^2. w_3^2 \sigma_3^2  + 2w_1w_2cov(1,2) + 2w_2w_3cov(2,3) + 2w_1w_3cov(1,3)}}

Idea of gradient descent

Like climbing a mountain, the goal of gradient descent is to maximize/minimize a function.
To achieve that, we need to know how steep are we at right now, or in mathematical terms, the gradient.

We’ll need to get the first derivative of the Sharpe ratio with respect to weights.

Naturally, I’ll skip this part because I’m not good at typing in LaTeX.

Talk is cheap, show me the code

In this post, I am optimizing a three fund portfolio which consists of
Vanguard Total World Stock Index Fund ETF (VT)
Vanguard Total Bond Market Index Fund ETF (BND)
SPDR Bloomberg 1-3 Month T-Bill ETF (BIL)

import math
import numpy as np

R_1 =  0.09005572157459407
R_2 =  0.011467362280556559
R_3 =  0.0041235900715872464
R_free = 0.0275 # using maybank 1 month FD rate here

# weights
w_1 = 0.6
w_2 = 0.2
w_3 = 0.2

cov_11 =  0.00013121288625519365
cov_22 = 7.225186779674805e-06
cov_33 =  2.4716643519128892e-08

cov_12 =  2.31881732577847e-07
cov_13 =  -4.1092654882446956e-08
cov_23 =  1.3072393870561457e-08

portfolio_expected_return = R_1 * w_1 + R_2 * w_2 + R_3 * w_3
portfolio_volatility = math.sqrt( pow(w_1,2)*cov_11 + pow(w_2,2)*cov_22 + pow(w_3,2)*cov_33 + 2*w_1*w_2*cov_12 + 2*w_1*w_3*cov_13 + 2*w_2*w_3*cov_23) 

objective_function = portfolio_expected_return / portfolio_volatility

df_dw1 = -1 * ( (R_1 - 0.5*(2*w_1*pow(cov_11,2) + 2*w_2*cov_12 + 2*w_3*cov_13)) * portfolio_expected_return ) / pow(portfolio_volatility,2)
df_dw2 = -1 * ( (R_2 - 0.5*(2*w_2*pow(cov_22,2) + 2*w_1*cov_12 + 2*w_3*cov_23)) * portfolio_expected_return ) / pow(portfolio_volatility,2)
df_dw3 = -1 * ( (R_3 - 0.5*(2*w_3*pow(cov_33,2) + 2*w_2*cov_23 + 2*w_1*cov_13)) * portfolio_expected_return ) / pow(portfolio_volatility,2)

def get_df_dw1(v_w_1, v_w_2, v_w_3):
    portfolio_expected_return = R_1 * v_w_1 + R_2 * v_w_2 + R_3 * v_w_3
    portfolio_volatility = math.sqrt( pow(v_w_1,2)*cov_11 + 
                                      pow(v_w_2,2)*cov_22 +
                                      pow(v_w_3,2)*cov_33 +
                                      2*v_w_1*v_w_2*cov_12 +
                                      2*v_w_1*v_w_3*cov_13 +
                                      2*v_w_2*v_w_3*cov_23) 

    return -1 * ( (R_1 - 0.5*(2*w_1*pow(cov_11,2) + 2*w_2*cov_12 + 2*w_3*cov_13)) * portfolio_expected_return ) / pow(portfolio_volatility,2)

def get_df_dw2(v_w_1, v_w_2, v_w_3):
    portfolio_expected_return = R_1 * v_w_1 + R_2 * v_w_2 + R_3 * v_w_3
    portfolio_volatility = math.sqrt( pow(v_w_1,2)*cov_11 + 
                                      pow(v_w_2,2)*cov_22 +
                                      pow(v_w_3,2)*cov_33 +
                                      2*v_w_1*v_w_2*cov_12 +
                                      2*v_w_1*v_w_3*cov_13 +
                                      2*v_w_2*v_w_3*cov_23) 

    return -1 * ( (R_2 - 0.5*(2*w_2*pow(cov_22,2) + 2*w_1*cov_12 + 2*w_3*cov_23)) * portfolio_expected_return ) / pow(portfolio_volatility,2)

def get_df_dw3(v_w_1, v_w_2, v_w_3):
    portfolio_expected_return = R_1 * v_w_1 + R_2 * v_w_2 + R_3 * v_w_3
    portfolio_volatility = math.sqrt( pow(v_w_1,2)*cov_11 + 
                                      pow(v_w_2,2)*cov_22 +
                                      pow(v_w_3,2)*cov_33 +
                                      2*v_w_1*v_w_2*cov_12 +
                                      2*v_w_1*v_w_3*cov_13 +
                                      2*v_w_2*v_w_3*cov_23) 

    return -1 * ( (R_3 - 0.5*(2*w_3*pow(cov_33,2) + 2*w_2*cov_23 + 2*w_1*cov_13)) * portfolio_expected_return ) / pow(portfolio_volatility,2)

def project(x):
    total = x.sum()
  
    return [round(x[0]/total, 2), round(x[1]/total, 2), round(x[2]/total, 2)]


def gradient_descent():
    x = np.array([w_1, w_2, w_3])

    # initial 
    alpha = 1
    learning_rate = np.array([alpha, alpha, alpha])
    gradient = [df_dw1, df_dw2, df_dw3]

    print("\n====BEFORE OPTIMIZATION=====")
    print("| asset                   VT  BND  BIL")
    print("| initial weights      ", w_1, w_2, w_3)
    print("| portfolio return     ", portfolio_expected_return)
    print("| portfolio volatility ", portfolio_volatility) 
    print("| sharpe ratio         ", (portfolio_expected_return - R_free)/ portfolio_volatility)
    print("| obj func gradient    ", gradient)
    print("============================")

    i = 0
    while (True):
        # plug gradient into x_k+1 = x_k - alpha * gradient
        x = x - learning_rate*np.array([gradient[0], gradient[1], gradient[2]])
        # update gradient
    
        gradient = np.array([get_df_dw1(x[0], x[1], x[2]), get_df_dw2(x[0], x[1], x[2]), get_df_dw3(x[0], x[1], x[2])])
        i += 1
        # iterate many times or
        # alternatively, stop when gradient is near 0
        if i==50000:
            break

    (vw_1, vw_2, vw_3) = project(x)
    new_portfolio_expected_return = R_1 * vw_1 + R_2 * vw_2 + R_3 * vw_3
    new_portfolio_volatility = math.sqrt( pow(vw_1,2)*cov_11 + pow(vw_2,2)*cov_22 + pow(vw_3,2)*cov_33 + 2*vw_1*vw_2*cov_12 + 2*vw_1*vw_3*cov_13 + 2*vw_2*vw_3*cov_23) 
    print("\n====AFTER OPTIMIZATION======")
    print("| asset                   VT  BND  BIL")
    print("| optimized weight     ", vw_1, vw_2, vw_3)
    print("| portfolio return     ", new_portfolio_expected_return)
    print("| portfolio volatility ", new_portfolio_volatility) 
    print("| sharpe ratio         ", (new_portfolio_expected_return - R_free)/ portfolio_volatility)
    print("| obj func gradient    ", gradient)
    print("============================")

gradient_descent()

Output:


====BEFORE OPTIMIZATION=====
| asset                   VT  BND  BIL
| initial weights       0.6 0.2 0.2
| portfolio return      0.057151623415185204
| portfolio volatility  0.006897352416612769
| sharpe ratio          4.298986281136967
| obj func gradient     [-108.18693435263714, -13.775960933899709, -4.953835084916042]
============================

====AFTER OPTIMIZATION======
| asset                   VT  BND  BIL
| optimized weight      0.85 0.11 0.04
| portfolio return      0.07797371679212968
| portfolio volatility  0.009743174905879868
| sharpe ratio          7.317839330720598
| obj func gradient     [-0.02504764 -0.00318944 -0.00114692]
============================

The starting allocation is 60/20/20 for VT/BND/BIL portfolio, with Sharpe ratio of 4.298, which is not too bad! After optimization, it’s 85/11/4, with Sharpe ratio raised to 7.318, with a slight rise in portfolio volatility but big rise in portfolio return!

Afterthoughts

  • We are optimizing a portfolio with assumption that asset return and volatility is constant over time, which is simply not true!
  • Though with that being said, this still serves some reference value, as Mark Twain said, History never repeats itself, but it does often rhyme

Leave a Reply

Your email address will not be published. Required fields are marked *

Verified by MonsterInsights