© Carlos Oliveira 2020
C. OliveiraOptions and Derivatives Programming in C++20https://doi.org/10.1007/978-1-4842-6315-0_13

13. Monte Carlo Methods

Carlos Oliveira1 
(1)
Seattle, WA, USA
 

Among programming techniques used for trading equity markets, Monte Carlo simulation has a special place due to factors such as its wide applicability and easy implementation. These methods can be used to implement strategies for market analysis such as price forecasting, or to validate options trading strategies, for example.

A great advantage of the Monte Carlo methods is the fact that they can be used to study complex events without the need to solve complicated mathematical models and equations. Using the idea of simulation through the use of random numbers, Monte Carlo methods offer the ability to study a large class of events, which would otherwise be difficult to analyze using exact techniques.

This chapter provides an introduction to stochastic methods and how they be used as part of simulation-based algorithms applied to options pricing. Here are a few of the topics that will be covered in this chapter:
  • Random number generation: Generating random numbers is a basic step in creating algorithms that exploit stochastic behavior. Monte Carlo methods require the use of effective random number generation routines, which will be discussed in this chapter.

  • Probability distributions: Monte Carlo algorithms are based on the properties of stochastic events. Many of these events occur according to well-known probability distributions. In C++, it is possible to generate numbers according to many popular probability distributions, as you will learn.

  • Random walks: A random walk is a stochastic process where a certain quantity can randomly change with equal probability to positive or negative side. This makes random walk very useful for modeling prices in financial markets, as well as for simulating trading strategies.

  • Stochastic models for options pricing: Another application of random walks is in the determination of option prices. Using a stochastic method for this purpose is useful if you want to avoid the use of a more complex exact or approximate model, such as the algorithms described in the previous chapter.

Introduction to Monte Carlo Methods

A Monte Carlo algorithm is a computational procedure that uses random numbers to simulate and study complex events. It is based on the idea that you can analyze the results of an event by repeating it several times in different ways, with the help of a computer or other technique to generate random numbers.

This idea behind Monte Carlo methods is not new, having been used for as long as probability methods have been studied. For example, a well-known randomized procedure to determine the area of a geometric shape is to throw darts at the figure. After a while, you can count the percentage of darts inside the shape and use that percentage to determine the area.

Despite their simplicity, Monte Carlo methods may be time-consuming, and they require a large number of repetitions to achieve their goals. The recent development of fast computers, however, made it possible to use such methods in an increasing number of situations, making them practical and capable of finding solutions for problems where explicit mathematical analysis is very difficult.

In general, Monte Carlo methods have been used for the solution of mathematical and computational problems where it is difficult to perform direct observations. Algorithms based on Monte Carlo methods use simulation strategies to determine values that normally occur as the result of random events in several areas, including the financial markets. In fact, the application of Monte Carlo to finance methods is widespread. You will find many algorithms used in the analysis of options and derivatives that exploit Monte Carlo techniques, for example:
  • Options pricing: It is possible to use randomized algorithms to determine the prices of options and other derivatives.

  • Trade strategy analysis: Monte Carlo methods can be used to test different trade strategies using simulated prices. This type of analysis is invaluable, since it allows you to test trading techniques on a large amount of data that is independent of the existing market observations.

  • Analysis of bonds and other fixed income investments: Bonds and their derivatives are tied to fluctuations of interest rates over different time horizons. An effective way to study the behavior of bonds is to construct stochastic models and use them to perform an analysis.

  • Portfolio analysis: Another area where Monte Carlo methods are useful is when studying a portfolio of investments. The stochastic algorithm allows analysts to vary the rate of exposure to diverse economic scenarios and try to determine the best allocation for a portfolio.

In the next few sections, you will first learn the tools necessary to design and implement Monte Carlo algorithms using the C++ language. You will also see examples of how these tools can be used to analyze options and related instruments.

Random Number Generation

The first topic that is addressed is random number generation. True random numbers are not possible to achieve in digital computers, but there are several techniques to create sequences of pseudo-random numbers. These methods have been made available through the standard C++ libraries, as will be covered in this section.

For C++ programmers, the main source of random number generation routines is the <random> header file provided by the standard library. With these functions, you can generate pseudo-random numbers that are well tested and that can be accessed through an easy interface.

The first thing to learn about random number generation in the standard library is the concept of generators. A generator can be viewed as a source of pseudo-random bits, that is, an algorithm that is capable of returning numbers that are uniformly random. The C++ library offers a small number of generators that can be used by programmers. Here are some of the available generators:
  • Mersenne twister: This is one of the most popular generators. It is based on an algorithm that uses Mersenne prime numbers as the period length of the sequence of pseudo-random numbers. The Mersenne twister algorithm is considered to be one of the best general-purpose generators of random numbers, and it is frequently used in applications.

  • Linear congruential engine: This engine is based on a traditional algorithm that uses simple addition, multiplication, and module operations to produce numbers that have pseudo-random properties. This generator is indicated when you need fast sequences of random numbers, due to its efficiency. However, the linear congruential algorithm is known to generate numbers that possess some correlation.

  • Subtract with carry: This is still another algorithm that is used to generate random numbers in the standard library. The algorithm is called lagged Fibonacci, and it uses a numeric sequence that has properties that are similar to the famous Fibonacci sequence.

These generators represent three of the most common ways to generate random numbers. Other techniques for random number generation have also been proposed in the scientific literature. Table 13-1 shows some of the most commonly used algorithms for random number generation.
Table 13-1

Algorithm for Pseudo-random Number Generation

Algorithm

Description

Linear congruential

Traditional method that uses modulo arithmetic.

Inversive congruential

Uses the modular multiplicative inverse to generate new elements in the sequence.

Mersenne twister

Method developed in 1997; uses Mersenne primes to generate random numbers.

WELL generators

Well Equidistributed Long-Period Linear, based on the application for operations on a binary field.

XorShift generators

Fast method that uses exclusive-or operations to generate new random numbers.

Linear feedback shift

Method that uses a linear function over the existing sequence of values to generate the next random number.

Park-Miller generator

A linear congruential generator that uses multiplicative groups of integers under the modulo operation.

The second part of the random generation library in C++ is the use of engine instantiations. These instantiations can be viewed as a concrete implementation of a generic algorithm. For example, consider the Mersenne twister engine, which is implemented as a template called mersenne_twister_engine . The easiest way to use this engine is to apply an instantiation such as the minstd_rand (minimal standard pseudo-random number) generator. This particular instantiation is defined by the C++ standard as
typedef linear_congruential_engine<
          uint_fast32_t,
          48271,
          0,
          2147483647> minstd_rand;
The linear_congruential_engine is a common random generator engine that is implemented by the standard library. A list of known engine instantiations in the C++ standard library is presented in Table 13-2. You can choose one of these instantiations as a generator for your own algorithm, or you can create a new instantiation.
Table 13-2

A List of Generator Instantiations Available on the Standard Library

Generator Instantiation

Parameters

default_random_engine

Random engine that is provided as a default option by the library implementation.

knuth_b

Defined as typedef shuffle_order_engine <minstd_rand0,256> knuth_b;.

minstd_rand

Minimal standard generator; it is an instantiation of linear_congruential_engine.

minstd_rand0

Similar to the engine described previously, with particular parameters.

mt19937

Mersenne twister generator.

mt19937_64

Mersenne twister generator for 64-bit types.

ranlux24

Uses the subtract-with-carry generator and returns values that use a 24-bit representation.

Note

Random number generators can be freely instantiated in the standard library. However, you should rarely need to define a new instantiation, unless you have good knowledge about how the parameters for each generator work together. A careful study of parameters is usually necessary to create a new generator, since they are based on statistical properties that have been determined after careful analysis made by researchers in the area.

The generators and their instantiations can be thought of as the original source for pseudo-random bits. Once you have defined a source, it is possible to generate random numbers according to a given probability distribution, as you will see in the next section.

Probability Distributions

A probability distribution is family of functions that defines the parameters for a stochastic process. For example, the simplest distribution of random numbers is the uniform distribution, where each value is generated with equal probability in a given range. A particular case of the uniform distribution is Uniform[0,1], where each number is randomly generated with equal probability in the range between 0 and 1.

There are a small number of probability distributions that occur very frequently in the analysis of natural events. These common distributions, which have been studied in several branches of stochastic analysis, are now available as part of the C++ <random> header in standard library. For examples of two common probability distributions, see Figure 13-1 (which shows the normal distribution with mean 0 and standard deviation 1), Figure 13-2 (which shows the exponential distribution with mean 1), and Figure 13-3 (which shows the Chi-squared distribution).
../images/371837_2_En_13_Chapter/371837_2_En_13_Fig1_HTML.jpg
Figure 13-1

Probabilities defined by the normal distribution, with mean 0 and standard deviation 1

../images/371837_2_En_13_Chapter/371837_2_En_13_Fig2_HTML.jpg
Figure 13-2

Probabilities defined by the exponential distribution, with mean 1

Consider the most common case of generating uniform random integer numbers in a particular range. This can be easily handled in the standard library by using the std::uniform_int_distribution template . This template is capable of creating integer numbers that have uniform distribution as given by the two parameters: the initial and maximum values. Here is an example of how to code a function that returns such random integer numbers:
#include <iostream>
#include <random>
using std::cout;
using std::endl;
std::default_random_engine generator;
int get_uniform_int(int max)
{
    if (max < 1)
    {
        cout  << "invalid parameter max " << max << endl;
        throw std::runtime_error("invalid parameter max");
    }
    std::uniform_int_distribution<int> uint(0,max);
    return uint(generator);
}
../images/371837_2_En_13_Chapter/371837_2_En_13_Fig3_HTML.jpg
Figure 13-3

Probabilities defined by the Chi-squared distribution

The first step is to define a generator to use as the source of random bits. This is done by instantiating an engine (done at the file scope). The std::default_random_engine is the default generator selected by the compiler’s implementation. It should be a reasonable choice, unless you want to be very specific about the generator for your code.

The get_uniform_int function generates a random integer between 0 and max, where max is a parameter passed to the function. The function first checks if the parameter is valid and throws an exception when that is not the case. The function then uses the parameter to create an object of type uniform_int_distribution. This object receives two parameters that define the distribution: the minimum and maximum values. The resulting object is then used to generate the random number itself.

Note

Traditional C and C++ code used to rely on the rand function to generate random integer numbers. This usage is now deprecated because the algorithm used in rand() is known to have weaknesses. In particular, the idea of using the expression (rand() % N) to generate random integer numbers in the range 0 to N-1 has been proved to be unreliable. Even though the numbers seem random enough for most applications, it fails when you try to perform more complex statistical analysis.

The sequence of steps to use the random number generators and distributions are therefore summarized as follows:
  • Find a suitable random engine and a corresponding generator according to the needs of your application.

  • Select a generator instantiation based on the random engine you selected previously. If you don’t have any specific requirements, the default_random_engine could be used.

  • Select a random distribution according to the needs of your application. A common distribution is the uniform, which produces numbers with the same probability in a given range.

  • Create an object of the type determined by the probability distribution. In the previous example, you used uniform_int_distribution as the object type.

  • The resulting object can now be called to generate pseudo-random numbers, once the generator object is passed as the single parameter for the call. This makes it possible to use generators of different types or, more commonly, generators that are used for a specific function of a thread.

Using Common Probability Distributions

This section will show a few examples of common probability distributions and how they can be used in C++. As mentioned, random numbers can be generated according to different probability functions. These families of functions are grouped according to the parameters and shape of the distribution.

One of the simplest probability distributions is the Bernoulli distribution. This is a family of probability distributions that model a yes/no scenario, an event that has only two results. The only parameter for this distribution is the probability of the yes result. The simplest example of this type of model is a coin toss, with parameter 0.5, representing a fair probability of heads or tails.

In the next code example, the function coin_toss_experiment returns a vector of Boolean values, representing the result of a set of fair coin tosses.
#include <iostream>
#include <random>
#include <vector>
using std::cout;
using std::endl;
using std::vector;
std::default_random_engine generator;
vector<bool> coin_toss_experiment(int num_experiments)
{
    if (num_experiments < 1)
    {
        cout  << "invalid number of experiments "
              << num_experiments << endl;
        throw std::runtime_error("invalid number of experiments");
    }
    std::bernoulli_distribution bernoulli(0.5);
    vector<bool> results;
    for (int i=0; i<num_experiments; ++i)
    {
        results.push_back(bernoulli(generator));
    }
    return results;
}

In this code, the first step is to use a generator, which in this case is std::default_random_engine allocated in the file scope, so it is available during the lifetime of the application. The coin_toss_experiment function initially checks the validity of the parameter num_experiments, which gives the number of tries in this random experiment.

The function then allocates a new object from the Bernoulli distribution, with parameter 0.5, which indicates that the yes/no event occurs with even probability for each side. The random values are then generated in the loop, where the bernoulli returns Boolean values according to the desired distribution behavior. The values are stored in a vector<bool> container.

Another common distribution that is used to model natural events is the Poisson distribution. This distribution arises commonly when observing the number of events that occur in a period of time, under the assumption that these events are independent. For example, the number of customers arriving at a coffee shop during a given period could be modeled as a Poisson distribution. The mathematical expression used to model the probability distribution of such events is given by
$$ p(k)=frac{lambda^k{e}^{-k}}{k!} $$

Here, k is the number events that are observed, and λ is the parameter that determines the results of the experiment, which can be interpreted as the average number of events occurring in the given time period.

In the C++ standard library, the Poisson distribution is made available through the std:: poisson_distribution template . The parameter for this distribution is the mean, usually represented as the mathematical variable λ as in the previous equation.

The following is an example that can be used to analyze the number of customers buying in a particular store in a time period. For instance, financial analysts perform this type of study when they need to study the buying patterns at a particular business. The code defines a function named num_customers_experiment:
#include <iostream>
#include <random>
#include <vector>
using std::cout;
using std::endl;
using std::vector;
vector<int> num_customers_experiment(double mean, int max, int ntries)
{
    std::default_random_engine generator;
    vector<int> occurrences(max, 0);
    std::poisson_distribution<int> poisson(mean);
    for (int i=0; i<ntries; ++i)
    {
        int result = poisson(generator);
        if (result < max) {
            occurrences[result] ++;
        }
    }
    return occurrences;
}

The num_customers_experiment function can generate a sequence of random values based on the Poisson distribution and return a histogram of these values, that is, for each value, it returns the number of times this value was observed.

The algorithm is similar to what you have seen before with the Bernoulli distribution. The first part is used to define the random generator, and it creates an object of type std::poisson_distribution. The parameter passed represents the mean of the distribution.

The for loop in the algorithm is used to build the histogram. At each step, a number is generated according to the Poisson distribution. Then, if the resulting number is less than the parameter max, that value is incremented in the list of occurrences.

The num_customers_experiment function is used in the next code fragment to print the results of the calculation. These numbers have been saved and used to create the chart displayed in Figure 13-4, which shows the observations between 0 and 20 and the corresponding number of observations for 200 trials.
../images/371837_2_En_13_Chapter/371837_2_En_13_Fig4_HTML.jpg
Figure 13-4

Histogram of the data returned by function num_customers_experiment

int test_experiment()
{
    auto data = num_customers_experiment(10.5, 20, 200);
    for (int i=0; i<int(data.size()); ++i)
    {
        cout << " event " << i << " occurred "  << data[i] << " times" << endl;
    }
}

The next example shows how to generate and use random values drawn from the normal distribution. The normal distribution, also known as Gaussian distribution, is one of the most common probability distributions used to model real-world data. It is employed in data analysis, in areas ranging from drug design to sociology. The normal distribution represents the distribution of values that are naturally measured in populations. For example, the heights of people living in a particular geographical area follow the normal distribution.

The bell-shaped probability graph of the normal distribution is determined by the Gaussian equation, which takes as parameters the mean and the standard deviation of a random variable. The equation is given by
$$ p(x)=frac{1}{sigma sqrt{2pi }}exp left(-frac{{left(x-mu 
ight)}^2}{2{sigma}^2}
ight) $$

In this equation, μ is the mean value of these numbers, and σ is the standard deviation, which is a measure of the variability of these random values.

In the following code example, you will see how to generate numbers that follow the normal distribution. The get_normal_observations function returns a list of numbers that have been generated according to the normal distribution according to the parameters mean and stdev.
#include <iostream>
#include <random>
#include <vector>
#include <assert.h>
using std::cout;
using std::endl;
using std::vector;
vector<double> get_normal_observations(int n, double mean, double stdev)
{
    std::default_random_engine generator;
    vector<double> values;
    std::normal_distribution<double> normaldist(mean, stdev);
    for (int i=0; i<n; ++i)
    {
        values.push_back(normaldist(generator));
    }
    return values;
}

The next function, test_normal, can be used to verify the correctness of this code. The idea of this function is to use the generated values so that it can create a histogram of the normal-distributed data. The first step of the algorithm is to call the get_normal_observations function and save the returned data. The next step is to get some information about the received data, such as the minimum and maximum values. This is done using the std::minmax_element function , which returns a pair of iterators pointing to the minimum and maximum values in the given range.

The algorithm creates a vector with elements corresponding to “bins,” that is, smaller ranges where each observation is recorded. The size of each such bin is stored as the variable h. The first loop then determines the number of elements in each such range so that a histogram can be calculated.

The second loop is responsible for printing the results of the histogram. Each value is printed along with the starting point of the corresponding range.
void test_normal()
{
    vector<double> nv = get_normal_observations(1000, 8, 2);
    auto res = std::minmax_element(nv.begin(), nv.end());
    double min = *(res.first);
    double max = *(res.second);
    int N = 100;
    double h = (max - min)/double(N);
    vector<int> values(N, 0);
    for (int i=0; i<int(nv.size()); ++i)
    {
        double v = nv[i];
        int pos = int((v - min) / h);
        if (pos == N) pos--; // avoid the highest element
        values[pos]++;
    }
    for (int i=0; i<N; ++i)
    {
        cout << min + (i*h) << " " << values[i] << endl;
    }
}
The values created in this way have been plotted and are displayed in Figure 13-5. The horizontal axis represents the value of each observation. The vertical axis represents the number of occurrences of each observation.
../images/371837_2_En_13_Chapter/371837_2_En_13_Fig5_HTML.jpg
Figure 13-5

Histogram of values observed using the normal distribution with mean 8

Creating Random Walks

One of the main applications of stochastic processes in finance is the study of prices under random variations. This random process is called a random walk, since it implies that changes happen at random as time passes. A random walk model can be used to simulate market conditions and investigate the behavior of trade strategies, portfolios, and market participants in general. In this section, you see how to create a simple random walk using some of the facilities provided by C++.

A random walk can be designed with the use of a few simple rules that determine the price fluctuations. Notice the exact rules used depend on the kind of market that you need to simulate and the exact conditions that need to be replicated. In this example, I use a few computational commands that will simplify the task; the framework can be readily extended to implement more complex scenarios.

The random walk starts at an initial price given as a parameter to the algorithm. At each step, there are three possibilities for the random walk:
  • A price decrease, which occurs with probability 1/3.

  • A price increase, also happening with probability 1/3.

  • The price remains unchanged.

The amount of increase or decrease is given by a parameter called stepSize.

These rules are implemented in the RandomWorkModel class. The class has an interface that exposes two member functions. getWalk returns a vector with a set of steps in the random walk.
//
//  RandomWalk.hpp
#ifndef RandomWalk_hpp
#define RandomWalk_hpp
#include <vector>
// Simple random walk for price simulation
class RandomWalkModel {
public:
    RandomWalkModel(int size, double start, double step);
    RandomWalkModel(const RandomWalkModel &p);
    ~RandomWalkModel();
    RandomWalkModel &operator=(const RandomWalkModel &p);
    std::vector<double> getWalk();
private:
    int random_integer(int max);
    int m_numSteps;      // number of steps
    double m_stepSize;   // size of each step (in percentage)
    double m_startPrice; // starting price
} ;
#endif /* defined(__FinancialSamples__RandomWalk__) */
The class interface also contains the following member variables:
  • The number of steps, m_numSteps, determines the number of steps (time) in the random walk.

  • The initial price is defined by the m_stepSize member variable.

  • The starting price is defined by the m_startPrice member variable.

These member variables are initialized in the constructor of RandomWalkModel, as shown in this code listing:
//
//  RandomWalk.cpp
#include "RandomWalk.hpp"
#include <cstdlib>
#include <iostream>
#include <random>
using std::vector;
using std::cout;
using std::endl;
std::default_random_engine engine;
RandomWalkModel::RandomWalkModel(int size, double start, double step)
: m_numSteps(size),
  m_stepSize(step),
  m_startPrice(start)
{
}
RandomWalkModel::RandomWalkModel(const RandomWalkModel &p)
: m_numSteps(p.m_numSteps),
  m_stepSize(p.m_stepSize),
  m_startPrice(p.m_startPrice)
{
}
RandomWalkModel::~RandomWalkModel()
{
}
RandomWalkModel &RandomWalkModel::operator=(const RandomWalkModel &p)
{
    if (this != &p)
    {
        m_numSteps = p.m_numSteps;
        m_stepSize = p.m_stepSize;
        m_startPrice = p.m_startPrice;
    }
    return *this;
}
The random numbers needed by this code are generated using the random_integer member function . This function just uses the standard library random number generator std::default_random_engine. It also uses the uniform distribution returning integer values, as provided by the std::uniform_distribution template class.
int RandomWalkModel::random_integer(int max)
{
    std::uniform_int_distribution<int> unif(0, max);
    return unif(engine);
}
The random walk sequence is generated by the member function getWalk. The algorithm has a single loop that repeats the price generation according to the m_numSteps variable. Inside the loop, the code selects a random integer between 0 and 2. Depending on the result, the code makes a decision to increase, decrease, or leave the price unchanged. Each price is then added to a vector, and the vector is returned at the end of the function.
std::vector<double> RandomWalkModel::getWalk()
{
    vector<double> walk;
    double prev = m_startPrice;
    for (int i=0; i<m_numSteps; ++i)
    {
        int r = random_integer(3);
        cout << r << endl;
        double val = prev;
        if (r == 0) val += (m_stepSize * val);
        else if (r == 1) val -= (m_stepSize * val);
        walk.push_back(val);
        prev = val;
    }
    return walk;
}
This code can be tested using the test_random_walk function. This function simply creates a RandomWalkModel object with 200 steps, starting at the $30 price and with steps of $0.01.
int test_random_walk()
{
    RandomWalkModel rw(200, 30, 0.01);
    vector<double> walk = rw.getWalk();
    for (int i=0; i<walk.size(); ++i)
    {
        cout << ", " << walk[i];
    }
    cout << endl;
    return 0;
}
The random walk generated by the test_random_walk function was saved, and using that data, I plotted the results, as shown in Figure 13-6. Notice that, although this model is very simple, the results are not very different from what is observed in the market. Using this kind of synthetic data, you can test trading strategies and determine if they are profitable in such randomized scenarios.
../images/371837_2_En_13_Chapter/371837_2_En_13_Fig6_HTML.jpg
Figure 13-6

A random walk generated by the RandomWalkModel class with starting price of $30

Conclusion

In this chapter, I introduced a few examples of Monte Carlo techniques, which can be used to solve complex problems through simulation of random events. These methods are based on the use of pseudo-random values as a tool for the probabilistic analysis of events. Such models also support the simulation of complex mathematical models, including the evolution of stock prices, as well as their options and related derivative instruments.

In the preceding sections, you learned about the building blocks of Monte Carlos methods. First, you learned how to generate pseudo-random numbers using the C++ standard library. The random numbers can also be generated according to a predefined probability distribution. The C++ standard library contains some of the best-known probability distributions, which makes it easy to integrate these features into user applications.

You also saw how to implement a simple random walk model. In a random walk, values change by small increments in either negative or positive directions. The random walk model can be used to analyze several financial instruments, ranging from fixed income instruments to equities and derivatives.

The next chapter will cover additional library functions and classes that are commonly used to analyze and develop solutions for options and derivatives.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.123.120