Python Multiprocessing and Monte Carlo Option Pricing
Contributed by a student who took the introductory Python class with John Downs.
In this blog post, I demonstrate the use of Python’s multiprocessing package. The package allows for execution of Python code in a parallel manner through multiple interpreter processes. Popular implementations of Python do not support multithreading in an optimal way, because of the global interpreter lock preventing multiple threads from executing code at once. Multiple processes of the python interpreter work around this limitation. The downside, however, is that data in memory cannot be shared by more than one process. The multiprocessing package relies on the pickle package to distribute data across different processes.
To demonstrate the speedup that can be achieved by executing code in parallel, I use an example from computational finance. It turns out that financial options, and in particular call options (contracts that give the holder the right but not the obligation to purchase an asset in the future at a pre-specified price), can be priced by averaging simulated possible payoffs.
The code below implements a European equity CallOption class that contains two methods: PriceBS and PriceMC. Once an option object is created by specifying the required parameters (current stock price, stock price volatility, strike price, maturity of option, and the riskfree rate), its price can be obtained by calling one of the two methods. PriceBS provides an exact solution to the Black-Scholes differential equation, which governs options prices under some common assumptions (stock price follows a Brownian motion diffusion process). PriceMC provides a simulation based (Monte Carlo) approximation to the price computed by averaging the option’s payoff across simulated path of the stock price.
The PriceMC function is a good candidate for parallel execution, because it requires simulating thousands or millions of possible stock price paths. Because there is no dependence between paths, each can be simulated separately from the others.
I note that this example is only for illustrative purposes. In practice, given that obtaining an accurate solution requires simulating millions of paths, this is not a practical method to use for pricing options without making the underlying algorithm more efficient.
The code snippet below shows the class implementation including a constructor and the PriceBS function, which implements the Black-Scholes formula.
The code below shows the PriceMC function. This function simulates N paths of the stock price process. The average payoff of the option is then calculated and stored in the sum_payoff variable. Note that by keeping a running sum, memory requirements are modest since the N paths are not stored in memory simultaneously.
Code below shows a vectorized implementation of PriceMC using numpy. This code is much faster that the running sum implementation. However, significant memory is required for large values of N making this approach somewhat impractical. A hybrid approach that optimizes the speed and memory tradeoff would certainly be worth exploring.
The code below implements the multiprocessing module code and tests execution speed for a variety of scenarios for N and number of processes. This code was run on a Digital Ocean droplet with 20 virtual processors.
Note that the map function is used to run code contained in the eval_price function in parallel as shown below.
The results of the experiment are shown below. The speedup is noticeable but not linear in the number of processes. Likewise, accuracy generally improves as N increases.