JavaScriptData VizMath

Simulate Stocks with Javascript

February 1, 2021 Pixel heartPixel heartPixel heart 9 minute read
Simulate Stocks with Javascript

The stock market is an extremely complex system influenced by a huge number of variables, so we can never predict with any certainty what will happen in the future.

But that doesn't mean we can't make projections to assist with risk management!

We learned how to do Monte Carlo simulations in javascript, now lets put it to some practical use.

Monte Carlo simulations are very useful in risk management. We are aware we can't predict the future, but if we have enough data from the past, we can create a reasonable model for the future.

An important feature of Monte Carlo simulations in particular is that we can run them many thousands of times, each resulting in a different outcome. Then, we can determine risk by considering how often failure occurs.

Get Statistics on Historical Data

The first step in creating a model in Monte Carlo simulations is to gather historical data.

There are many options for using an API to access historical stock market data. However, that's not our focus. To simplify, we will be using yahoo finance's historical data download options.

To start, let's use Google's historical data. We can download the last 2 full years of daily closing prices for Google as a CSV file.

There are two statistics we need to get from our data for our simulation model: the mean and standard deviation of the daily change of our stock price.

import { deviation, mean } from 'd3';

function getStockStats(data) {
  let stockDayPcntChange = [];

  const data2019 = data.filter(
    data => new Date(data.Date).getFullYear() === 2019
  );

  // Start at second day
  for (let i = 1; i < data2019.length; i++) {
    const curDayPrice = data2019[i].Close;
    const prevDayPrice = data2019[i - 1].Close;
    stockDayPcntChange.push((curDayPrice - prevDayPrice) / prevDayPrice);
  }

  return {
    meanDailyChange: mean(stockDayPcntChange),
    stdDevDailyChange: deviation(stockDayPcntChange)
  };
}

D3 provides functions that make this very straightforward. All we need to do is pass in an array of the daily market changes to get the statistics we need. We'll get the statistics for only 2019 so that we can use 2020 to compare against our simulations.

Now we've got all the data and statistics we need, let's get to simulating!

Creating a Monte Carlo Model for Stocks

In order to simulate our stock using the Monte Carlo method, we'll need to create a model. For simpler examples like rolling dice, just the random generation of a number between 1 and 6 was sufficent. However, just randomly generating any value for a stock price would not create a useful simulation. We need to utilize historical data to create a realistic simulation of future prices, which is where our statistics come in.

One of the most commonly used models for projecting stock prices is Geometric Brownian motion (GBM).

St=S0e(μσ22)t+σWtS_t = S_0 e^{(\mu - \frac{\sigma^2}{2}) t + \sigma W_t}

StS_t is our stock price at time "t", S0S_0 at the initial time, μ\mu is the mean, and σ\sigma is the standard deviation.

In brief, GBM allows us to specify a "drift" for the stock, which we get from its mean. The drift represents the general direction the stock is moving. So, if a stock has been increasing consistently throughout our historical period, it will have a positive mean price change, with a magnitude depending on how much it increased on average. This is μσ22\mu - \frac{\sigma^2}{2} in our formula.

Even though our mean represents the general historical trend of the stock, we know it is completely unrealistic for a stock to increase a static amount each time day. We need to inject some of the volatility that is characteristic of the stock market that we know and love. For this we use σWt\sigma W_t.

WtW_t is a Wiener process, also known as brownian motion. Essentially, we need to add a random amount of volatility (positive or negative), a random shock, to a stock's average drift at every interval, and for this we can use a Wiener process. We can determine the magnitude of this volatility using the standard deviation of our stock data. The higher the standard deviation, the more volatile the stock, and the more the stock price can vary day to day, regardless of its average directional "drift".

It's easiest to see how this works by doing a calculation.

Create a Stock Price Projection

Let's use our Google data to create a projection for a single day, the first day in 2020.

function projectStockPrice(currPrice, meanDailyChange, stdDevDailyChange) {
  const drift = meanDailyChange - (stdDevDailyChange * stdDevDailyChange) / 2;
  const randomShock = stdDevDailyChange * normSinv(Math.Random());
  return currPrice * Math.exp(drift + randomShock);
}

const last2019Price = 1337.02002;
const stockStats = getStockStats(data);
const first2020Price = projectStockPrice(
  last2019Price,
  stockStats.meanDailyChange,
  stockStats.stdDevDailyChange
);

Here we just grab the last closing price in 2019 as our starting point, calculate the stats for our 2019 data as before, then plug this into the equation. The equation produces a drift from the mean, positive in this case since Google's stock price went up on average in 2019, then adds a random shock by adding a price fluctuation a random amount of standard deviations from the mean.

One thing you may notice in this code that is unfamiliar is the normSinv function call. This is the inverse of the standard normal cumulative distribution.

We can't just generate a random number and multiply it by the standard deviation, because we know a larger number of standard deviations is much less likely than a smaller number. A naive random number generator produces a random value with a uniform distribution. In other words, if we generate a random number from 1 to 6, 1 and 3 are equally likely to show up.

However, we know getting a value 3 standard deviations from the mean is extremely unlikely, while 1 standard deviation is fairly likely. In a normal distribution, we know that 68.2% of values fall within 1 standard deviation of the mean, 95.4% of values fall within 2, and 99.7% fall within 3. So we have a less than 0.3% chance of getting a value that is over 3 standard deviations from the mean.

To address this shortcoming of random number generators, we can use the inverse standard normal distribution, which allows us to get a (positive or negative) amount of standard deviations from a random value from 0 to 1, using the appropriate distribution. So, getting 3 standard deviations would require a "random number" roll of greater than 0.99, which is very unlikely, as expected.

Going back to our stock example, if Math.Random() had produced a value of 0.89, the inverse normal distribution function would return 1.227 standard deviations. This would produce a projected stock value of $1,363.56 for the first day of 2021. If the random value was 0.21, it would return -0.806 standard deviations. This would project a stock value of $1,321.99, which is actually lower than the prior value ($1,337.02) despite the positive mean drift of Google's stock. This reflects the high volatility of stocks.

Unfortunately, there is no popular library available which implements the inverse normal distribution function in Javascript that I could find. I was able to implement this function myself using Peter John Acklman's pseudocode algorithm. The specifics of the implementation is beyond the scope of this post, but feel free to use my javascript implementation if you need it.

Monte Carlo Simulation of Stock Prices for a Year

Now that we've got all the pieces in place, let's create our Monte Carlo simulation for Google's 2020 stock prices. Since we would like to compare our simulation to the actual 2020 values, let's generate the same amount of values as we have actuals (252, since the market is closed weekends).

function project2020Prices(data) {
  const data2019 = data.filter(
    data => new Date(data.Date).getFullYear() === 2019
  );

  const last2019Price = data2019[data2019.length - 1].Close;

  const data2020 = data.filter(
    data => new Date(data.Date).getFullYear() === 2020
  );

  const projection2020 = [];

  for (let i = 0; i < data2020.length; i++) {
    const priorPrice = i === 0 ? last2019Price : projection2020[i - 1].Close;

    projection2020.push({
      Date: data2020[i].Date,
      Close: projectStockPrice(
        priorPrice,
        stockStats.meanDailyChange,
        stockStats.stdDevDailyChange
      )
    });
  }

  return projection2020;
}

Try generating a few projections below for Google's 2020 stock prices and see how they compare to the its 2020 actuals.

If you've run the simulation a bunch of times, you'll note there is a wide range of possibilities shown by the projections. We can get a projection either substantially higher than 2020's actuals, about in line with the actuals, or substantially lower. Interestingly, even with the unprecedentedly steep and rapid COVID-19 market drop in early 2020, we can still see the simulation produce a drop even steeper!

Therein lies the beauty of using Monte Carlo simulations for risk management. You get a large range of possibilities on which you can base decision making. Of course, simply repeatedly pushing simulate to see the various scenarios is only useful to demonstrate the concept.

Visualizing Many Stock Simulations

Let's take it a step further and run a bunch of simulations and display them all, so we can see a broad swath of projections all at once.

Now we can really appreciate the range of possibilities. However, as we begin to increase the number of projections, we become quite limited by what the human eye can discern. Monte Carlo simulations are most useful when you create a huge amount of simulations. If we just throw 100,000 projections on a graph, we won't be able to make much sense of it since it basically becomes a solid block of color (not to mention the rendering inefficiencies).

When we're running many thousands of simulations, we become far less interested in the specifics of an individual simulation. Instead, we'd like to get a general picture of the overall tendencies of the simulations.

A better way to visualize a large amount of simulations is to split them into quantiles. What we're really after is an answer to a few questions.

  • What is most likely to happen to this stock next year?
  • What is the most likely worst case scenario?
  • What is the most likely best case scenario?

These are the questions quantiles are well suited to answer.

function getQuantiles(series, yAccessor, quantiles) {
  const transposed = transpose(series).map(d =>
    d.map(dr => yAccessor(dr)).sort(ascending)
  );
  const quantileData = [];
  for (let i = 0; i < quantiles.length; i++) {
    const quantileNum = quantiles[i];
    quantileData.push(transposed.map(d => quantile(d, quantileNum)));
  }
  return quantileData;
}
const projectionQuantiles = getQuantiles(projections, d => d.Close, [
  0.1,
  0.25,
  0.5,
  0.75,
  0.9
]);

D3 doesn't include way to get quantiles for matrix data, but it does have a matrix transpose function. Our array of projections for the stock price for 2020 is actually a matrix, since it's an array (a stock price for each trading day of the year) of arrays (each full one year projection).

To get the quantile lines, what we really need is to calculate quantiles along each day of all the projections. If we transpose our projection matrix, we will get an array containing projected stock prices for each trading day across all of our various projections.

It's best to understand transformation by visualizing it. We're going from a row for each projected year of data:

$1,367.36$1,360.66$1,394.20$1,358.75$1,380.83$1,403.27$1,377.21$1,418.61$1,461.26\begin{matrix} \$1,367.36 & \$1,360.66 & \$1,394.20\\ \$1,358.75 & \$1,380.83 & \$1,403.27\\ \$1,377.21 & \$1,418.61 & \$1,461.26 \end{matrix}

To a row for each date of the year across the projections:

$1,367.36$1,358.75$1,377.21$1,360.66$1,380.83$1,418.61$1,394.20$1,403.27$1,461.26\begin{matrix} \$1,367.36 & \$1,358.75 & \$1,377.21\\ \$1,360.66 & \$1,380.83 & \$1,418.61\\ \$1,394.20 & \$1,403.27 & \$1,461.26 \end{matrix}

After transposition, we simply call D3's quantile function on each row to get the quantile stock price for each trading day of the year across all the projections, repeating for each quantile we want to calculate. Now we can graph the quantiles.

Actual
90th Percentile
75th Percentile
50th Percentile
25th Percentile
10th Percentile

Now we can see at a glance where we are likely to be in 2020. Since we chose a worst-case quantile of 0.1 and best case of 0.9, we're saying that 80% of all scenarios fall between our top and bottom quantiles.

Of course, you can see some of the actual projections still do fall well above or below our outer quantiles. You can extend this depending on your risk tolerances and preferences. For example, you can make the bounds 0.05 and 0.95, which would make 90% of scenarios fall between our outer bounds, and so on.

The 0.25 and 0.75 bounds are essentially our "most likely" bounds, between which 50% of scenarios will fall. The 0.5 quantile just tells us the "median" case, which we can think of the most typical case.

You'll see that despite the "COVID Drop" which Google experienced along with most of the stock market, its final 2020 price still tends to fall right around our median projection. Interesting!

Conclusion

It's been a bit of a journey, but it was well worth it! We've now learned how to create Monte Carlo simulations with javascript, and we learned how to create stock price projections with them.

These types of analyses are often done using languages which have extensive libraries that enable advanced data analytics, like Python (using numpy and pandas) or R. One thing that frustrated me when I practiced data analytics using Python was the inability to extensively customize your charts and make them interactive. It felt like a whole dimension of data exploration and visualization was missing.

But with javascript, D3, and some creativity, we can do pretty much anything just as well as with Python, and the look and interactivity of our visualizations is limited only by our imagination!

Found this article useful? Click to share, discuss and spread the word!! 🎉

Webmentions ()

No comments yet. Start the conversation! Your post will show up here.