Building A Monte Carlo Method Stock Price Simulator With Geometric Brownian Motion And Bootstrap Sampling
By: Zhijing Eu
Tags: Stock Market, Time Series Analysis, Python, Monte Carlo, Time Series Forecasting

Building A Monte Carlo Method Stock Price Simulator With Geometric Brownian Motion And Bootstrap Sampling Zhijing Eu Follow Jan 14 · 12 min read

Image Source : https://knowyourmeme.com/memes/stonks

Summary

I built a web app using Python Flask that allows you to simulate future stock price movements using a method called Monte Carlo simulations with the choice of two ‘flavours’ : Geometric Brownian Motion (GBM) and Bootstrapped Sampling.

While I may include some programming code below, this article is not going to be a ‘code-along’ tutorial and focuses more on the underlying theory behind GBM and Bootstrap Sampling.

However the native *.*py and a Jupyter Notebook version of the same code is available on this GitHub link below if you want to dig deeper.

Warning ! I am not an investment guru of any sort so I strongly suggest you do NOT use this article as the (sole) basis for your investment decisions.

Outline

Geometric Brownian Motion

You may be familiar with Brownian Motion from high school physics. That is the seemingly random motion of air particles as they collide with each other. While we might not be able to characterize the behavior of a single particle, physics and statistics gives us the ability to still describe the likelihoods of where the overall system will end up.

Image Source : Wikipedia

Much in the same way, the Geometric Brownian Motion is a model of an assets returns where the price (or returns) of the asset / shares / investment can be modelled as a random walk (I.e a process where changes in stock prices have the same distribution and are independent of each other. Therefore, it assumes the past movement or trend of a stock price or market cannot be used to predict its future movement (Source : Investopedia)

Rather than dive headlong into some reasonably complex stochastic differential equations , an intuition of how it works is

Source : Adapted from https://www.investopedia.com/articles/07/montecarlo.asp

where:

Sₜ=the stock price

dSₜ=the change in stock price

μ=the expected return

σ=the standard deviation of returns

Wₜ=the Wiener Process random variable where increments in t time have a normal distribution N that is centered at zero (i.e Wₜ -W₀~ N(0,t) )

dₜ=the elapsed time period​

Essentially, the GBM model allow us to model future prices based a combination of a “drift” that is driven by the average (i.e mean of the log returns) and a “shock” which is random but can be still be characterized by the volatility (i.e the standard deviation of the log returns).

However it should be noted the model makes a few KEY assumptions:

The logarithmic change of the stock price is normally distributed *

Volatility (i.e Standard Deviation of the Log Returns) is constant

Expected return (i.e Mean of the Log Returns) is independent of to stock performance

(*You may be wondering why we are using returns instead of prices — it’s because returns are scale free (being in % terms rather than absolute values) and often have more ‘stable’ statistical properties (e.g constant mean & variance). Furthermore log returns have the advantage over simple returns because the log of a log-normally distributed random variable will be normally distributed. These three posts (1) , (2) and (3) from statsexchange go into a lot more detail.)

However as we shall see later , these conditions may not always hold true so the validity of any predictions need to be evaluated very carefully.

Monte Carlo Simulations

As mentioned before, I already have a separate article on this topic but in a nutshell : Monte Carlo simulations are a method of making predictions by repeatedly running models that have random variables and aggregating all the results to understand the range of outcomes.

Therefore although each iteration of the GBM forecast will be slightly different, we can make multiple forecasts and aggregate all the results to see overall range of potential price changes within the desired time frame.

Ten different Iterations For a ~3 Week Forecast Prices of NFLX Shares Based On 1 Yr Of Historical Data. (Note : I used a backtest period 7 days hence the overlap between the actual vs forecasted prices)

The 10th , 90th Percentile and Median Forecast Prices Of NFLX For The Same 10 Iterations

As to be expected, the further into the future, the wider the range (i.e the P10 to P90) of possible prices. (As a rule of thumb , there is an academic paper that says that GBM works best for forecasting when limited to max 2 week lookahead)

Simulating A Single Stock Using GBM

The web app was built by extending some code built by another author as per the article below where I combined it with Python Flask to allow users to select the stock counter and the desired historical data range and forecast duration via a website.

The code looks a bit like below where there is a function to extract the stock prices from Yahoo Finance using pandasDataReader

import pandas as pd

from pandas_datareader import data import numpy as np, numpy.random

from numpy import mean import random

import matplotlib.pyplot as plt from datetime import datetime from scipy.stats import norm

from scipy.stats import kstest

from scipy.stats import skew

from scipy.stats import kurtosis

from scipy import stats def extract_prices(start_date,end_date,symbols,backtestduration=0):

dim=len(symbols)

for symbol in symbols:

dfprices = data.DataReader(symbols, start=start_date, end=end_date, data_source='yahoo')

dfprices = dfprices[['Adj Close']]

dfprices.columns=[' '.join(col).strip() for col in dfprices.columns.values] for i in range(0,len(symbols)):

noOfShares.append(portfolioValPerSymbol[i]/priceAtEndDate[i])

noOfShares=[round(element, 5) for element in noOfShares]

listOfColumns=dfprices.columns.tolist()

dfprices["Adj Close Portfolio"]=dfprices[listOfColumns].mul(noOfShares).sum(1)



print(f"Extracted {len(dfprices)} days worth of data for {len(symbols)} counters with {dfprices.isnull().sum().sum()} missing data")



return dfprices

After which , another function converts the prices into Log Returns

def calc_returns(dfprices,symbols):

dfreturns=pd.DataFrame()

columns = list(dfprices)

mean=[]

stdev=[]

for column in columns:

dfreturns[f'Log Daily Returns {column}']=np.log(dfprices[column]).diff()

mean.append(dfreturns[f'Log Daily Returns {column}'][1:].mean())

stdev.append(dfreturns[f'Log Daily Returns {column}'][1:].std())

dfreturns=dfreturns.dropna()



if len(dfreturns.columns)==1:

df_mean_stdev=pd.DataFrame(list(zip(symbols,mean,stdev)),columns =['Stock', 'Mean Log Daily Return','StdDev Log Daily Return'])

else:

df_mean_stdev=pd.DataFrame(list(zip(symbols+["Portfolio"],mean,stdev)),columns =['Stock', 'Mean Log Daily Return','StdDev Log Daily Return'])



return dfreturns ,df_mean_stdev

These Log Returns fed as input into another function that estimates the Mean , Standard Deviation which is then pushed along with the initial price to a function that fits the input into the equation discussed in the previous section

def GBMsimulatorUniVar(So, mu, sigma, T, N):

"""

Parameters

So: initial stocks' price

mu: expected return

sigma: volatility

Cov: covariance matrix

T: time period

N: number of increments

""" dim = np.size(So)

t = np.linspace(0., T, int(N))

S = np.zeros([dim, int(N)])

S[:, 0] = So

for i in range(1, int(N)):

drift = (mu - 0.5 * sigma**2) * (t[i] - t[i-1])

Z = np.random.normal(0., 1., dim)

diffusion = sigma* Z * (np.sqrt(t[i] - t[i-1]))

S[:, i] = S[:, i-1]*np.exp(drift + diffusion)

return S, t

There is an important caveat though to the validity of the results. Within the code, a best fit normal distribution is estimated from the historical data but what is more relevant is how “normal” the returns are.

Therefore I’ve bolted on a number of statistical tests for normality that I will not go into detail in this article. Suffice to say, these tests generate a statistic and a p- value that can be tested against a chosen significance level to check the ‘normality’ (*) of the historical returns data. Specifically, I’ve implemented two tests , a Kolmogorov Smirnov Test and a Shapiro Wilk Test with a significance level of 5%. (Why 5% ? Here read this)

(* Strictly speaking , it’s not exactly ‘checking’ per se but I’d rather not get into the mechanics of how Hypothesis Testing works for now)

Unfortunately running the numbers of a few different shares (using about a anywhere from 6mths to a year’s worth of historical data) shows that the returns are not always “normal” as per the arbitrary example below for the stock of CRM (Salesforce) and NFLX (Netflix) over Jan 2019 to Jul 2020.