Simulating Risk of a Portfolio of Securities#

We simulate different stocks and check if the simulations align with portfolio theory. In particular, we check if by diversifying a portfolio of stocks, we do indeed get the L-curve when we plot risk vs number of stocks as predicted by the theory.

In other words, in this simulation, will we see this curve that is used in standard finance textbooks?

To explore this, we simulated N stock prices via geometric Brownian motion to represent the stock market. We simulated random correlations between stock prices as well.

The portfolio is made up of \(K < N\) randomly selected stocks. We then assume we have a portfolio of \(K\) stocks whith equal fund allocations so each stock will have an allocation of \(1/K\) of the total portfolio. We then compute the portfolio return standard deviation (risk) two ways:

A) Using the formula \(\sigma^2_{p} = \sum p_i * p_j * \sigma_{ij}\) for stock combinations \(i\) and \(j\) where if \(i = j\) then \(\sigma_{ij} = \sigma{ii} = \sigma^2_{i}\) which is the variance of returns of stock \(i\), else for \(i \neq j\), \(\sigma_{ij}\) is the covariance of returns between stocks \(i\) and \(j\)

B) We get the expected portfolio return (average of the individual returns of the K stocks) and compute the standard deviation and variance

We then plot the corresponding portfolio risk \(\sigma_p\) vs \(K\) and check if we will see the:

  • L-curve where risk, \(\sigma_p\) goes down as \(K\) goes up, which is fundamental in portfolio theory (shift from unique/specific risk to market/systematic risk)

  • Whether the theoretical computation A) will match the empirical computation B) of \(\sigma_p\)

Spoiler: Yes we see the L-curve and yes we A) exactly matches B). The math of portfolio theory checks out, supporting the idea how diversification reduces overall risk!

We define the functions#

Hide code cell source
def generate_stocks(n_stocks=5, 
                    period=365 * 10, # say X years, 1 hour granularity
                    plot=False,
                    seed=42):

    # random walks for stock simulation
    np.random.seed(seed)

    price_matrix = np.zeros(shape=(n_stocks, period))
    z_matrix = np.zeros(shape=(n_stocks, period))
    stds = np.random.uniform(0.01, 0.05, size=n_stocks) # daily
    mus = stds/10000 # make returns proportional to std

    if plot:
        plt.figure(figsize=(7, 5))
    i = 0

    # First stock
    while True:
        P0 = 1 #
        delta_t = 1 # 1 day

        # brownian motion
        z1 = np.random.normal(0, 1, size=period)
        mu, std = mus[i], stds[i]
        returns = np.exp((mu - (std ** 2) / 2) * delta_t + std * np.sqrt(delta_t) * z1)
        prices = P0 * np.cumprod(returns)

        # make sure gain
        if prices[-1] > prices[0]:
            price_matrix[i, :] = prices       
            z_matrix[i, :] = z1         
            plt.plot(prices, linewidth=1, alpha=0.75)
            i += 1
            break

    # rest of the stocks
    while True:
        # generate correlated stock
        corr_coeff = np.random.uniform(-0.8, 0.8)

        # find a random stock from the existing list
        i2 = np.random.choice(range(0, i))
        mu, std = mus[i2], stds[i2]    
        z1 = z_matrix[i2, :]
        z2 = np.random.normal(0, 1, size=period) # random noise for this stock    

        zb = corr_coeff * z1 + np.sqrt(1 - corr_coeff ** 2) * z2
        returns = np.exp((mu - (std ** 2) / 2) * delta_t + std * np.sqrt(delta_t) * zb)
        prices = P0 * np.cumprod(returns)

        # make sure gain
        if prices[-1] > prices[0]:
            price_matrix[i, :] = prices        
            plt.plot(prices, linewidth=1, alpha=0.75)
            i += 1

        if i >= n_stocks:
            break

    if plot:
        plt.legend()
        plt.xlabel("time")
        plt.ylabel("price")
        plt.title("Simulated stock prices in the stock market")
        plt.tight_layout()
        plt.show()

    return price_matrix
Hide code cell source
# Get only K random stocks from the universe of stocks
def compute_portfolio_risk(price_matrix, portfolio_size, plot=False, seed=42):
    np.random.seed(seed)
    inds = np.random.choice(range(price_matrix.shape[0]), size=portfolio_size, replace=False)
    price_mtx_trunc = price_matrix[inds, :]
    return_mtx = (price_mtx_trunc[:, 1:] - price_mtx_trunc[:, :-1]) / price_mtx_trunc[:, :-1]

    # histograms
    if plot:
        fig, axs = plt.subplots(int(np.sqrt(portfolio_size)), int(np.sqrt(portfolio_size)), figsize=(10, 6), sharex=True)
        axs = axs.flatten()        
        for i, ax in zip(range(portfolio_size), axs):
            ax.set_title(f"Stock {i}")
            ax.hist(return_mtx[i, :], bins=20)
        plt.suptitle("Distribution of daily returns per stock")
        plt.tight_layout()
        plt.show()  

    # compute daily stdev
    stdevs = np.std(return_mtx, axis=1)

    if plot:
        plt.figure(figsize=(6, 4))
        plt.bar([str(i) for i in range(portfolio_size)], stdevs)
        plt.xlabel("stock")
        plt.ylabel("daily return standard dev")
        plt.suptitle("Standard deviation (risk) per stock")
        plt.tight_layout()   
        plt.show()     

    # build the covariance matrix (daily returns)
    cov_mtx = np.cov(return_mtx)

    if plot:
        sns.heatmap(cov_mtx, cmap='viridis')
        plt.title("Covariance matrix")
        plt.tight_layout()
        plt.show()

    # get market portfolio variance, as computed by the theory
    # Assume same proportion of allocation per stock
    portfolio_var_theory = np.sum(cov_mtx * (1/portfolio_size)**2)
    portfolio_std_theory = np.sqrt(portfolio_var_theory)

    # compute the actual variance from the data
    return_w_prop = return_mtx * 1/portfolio_size # allocation of returns
    return_w_prop = return_w_prop.sum(axis=0) # sum returns across all stocks (returns can be summed up)
    portfolio_std_actual = np.std(return_w_prop) # compute stdev of returns
    portfolio_var_actual = portfolio_std_actual**2    

    if plot:
        # plot the performance of the portfolio
        plt.plot(np.cumprod(1+return_w_prop))
        plt.xlabel("Time")
        plt.ylabel("Cumulative return")
        plt.title("Actual portfolio cumulative return")
        plt.tight_layout()
        plt.show()

    if plot:
        print(f"portfolio_var_theory {portfolio_var_theory:.6f}")
        print(f"portfolio_var_actual {portfolio_var_actual:.6f}")
        print()
        print(f"portfolio_std_theory {portfolio_std_theory:.6f}")        
        print(f"portfolio_std_actual {portfolio_std_actual:.6f}")        

    return portfolio_std_theory, portfolio_std_actual

We randomly generate N stocks representing the entire stock market#

# generate a universe of stocks
n_stocks = 500
price_matrix = generate_stocks(n_stocks=n_stocks, period=365 * 10, plot=True, seed=42)
/var/folders/r0/vzjvg_0x34l0y0xm0pz7bd100000gn/T/ipykernel_64180/423271303.py:62: UserWarning: No artists with labels found to put in legend.  Note that artists whose label start with an underscore are ignored when legend() is called with no argument.
  plt.legend()
../_images/ecc5e6c633b8c016d57a35cf330039ef0031741c3b302a613d1947af98e2e18b.png

We visualize the risks and returns of a portfolio of 9 random stocks#

portfolio_var = compute_portfolio_risk(price_matrix, portfolio_size=9, plot=True, seed=42)      
../_images/499b5ede07336a8de58f6d6ae4a4e6ba2120fbc8df55be7c189ce93c3b5b6d0d.png ../_images/b715611904b257d093cae11406996d7e17ff42d07f7b2a1a54e8d586093a9d21.png ../_images/e3a8b584f09029745fb753d7652b191baaf2ea1b35cac1549f821e0958f9143c.png ../_images/d5b360bdb9e9f18d1dd7fad14c759b85c1320d94f830ade20afcdb6e77864a8b.png
portfolio_var_theory 0.000079
portfolio_var_actual 0.000079

portfolio_std_theory 0.008890
portfolio_std_actual 0.008889

We visualize portfolio risk vs portfolio size#

  • We find that diversification reduces the risk of the portfolio to approach the risk of the entire market

  • The theoretical computation using stock return covariances matches exactly the direct computation of first getting the total return of the portfolio and getting its standard deviation

Hide code cell source
# Plot the portfolio std vs count stocks
portfolio_sizes = range(10, 500)
portfolio_stds_theory = []
portfolio_stds_actual = []
for portfolio_size in portfolio_sizes:
    portfolio_std_theory, portfolio_std_actual = compute_portfolio_risk(price_matrix, portfolio_size=portfolio_size, plot=False, seed=None)
    portfolio_stds_theory.append(portfolio_std_theory)
    portfolio_stds_actual.append(portfolio_std_actual)
Hide code cell source
fig, axs = plt.subplots(1, 2, figsize=(10, 5), sharey=True)
axs[0].plot(portfolio_sizes, portfolio_stds_theory)
axs[0].set_ylabel("Stdev of a portfolio of equal-proportion stocks")
axs[0].set_xlabel("Count of randomly selected stocks")
axs[0].set_title("Theory: Using the formula")

axs[1].plot(portfolio_sizes, portfolio_stds_actual)
axs[1].set_ylabel("Stdev of a portfolio of equal-proportion stocks")
axs[1].set_xlabel("Count of randomly selected stocks")
axs[1].set_title("Actual: Computing Stdev directly")
plt.suptitle("Portfolio risk vs count stocks based on theoretical and direct computation")
plt.tight_layout()
../_images/501e0093b9b212f644aa6a94348454881befce36dbb601621633785c5a169254.png