Pricing Options With Haskell

In finance, an option is a contract to either buy or sell one share of a stock for a certain price, the strike price, at an agreed upon day in the future. When one buys a call option, they make money when the price of the stock is above the strike price on the expiration date. Inversely, the buyer of a put option makes money when the price of the stock is below the strike price on the expiration date. During this contract transaction, the buyer of the option pays a premium to the seller of the option. This premium is the value that these theoretical models produce. If the contract would end up losing money for the buyer of the option, the buyer would not exercise it. In this situation, the buyer of the option only loses the amount of money they paid for the contract. In the real world, option premiums are simply controlled by the markets.

Valuing options is important for individuals and trading firms to hedge risk as well as make a directional play on a stock for much less capital than it would cost to purchase the stock outright.

My goal for this project was to explore different option pricing models, compare their results, and look into other applications of these models. The three pricing techniques I implemented were the Black-Scholes model, a Monte Carlo simulation, and the Binomial model.

To implement the models, I had to have a sense of the effect that certain attributes of an option contract had on the value of the option. More volatile stocks are generally valued higher than those with lower volatility because there is more risk associated with more volatile stocks. Likewise, an option with an expiration date farther into the future is valued higher than an option with a quicker expiration date because of the risk associated with time. I also learned the motivation for using each model. For instance, the Black-Scholes model is the most widely used model but has the most amount of limitations. Monte Carlo simulations are used for pricing exotic options as well as other options with multiple degrees of uncertainty. Lastly, the Binomial method is used similarly to the Black-Scholes model, as it has the same restrictions. However, both the Black-Scholes and Binomial models can be extended to price other types of options.

All implementations and tests can be found on my Github.

Black-Scholes Model

The Black-Scholes Model was the simplest model to implement for this project. While simple, the model isn’t always the best choice to price an option, as it relies on multiple assumptions:

  • Volatility and interest rates are known and remain constant for the duration of the option
  • The stock does not pay dividends for the duration of the option
  • There are no transaction costs
  • The option can only be exercised on the expiration date (European Option)

Regardless of its limitations, the model has a simple set of inputs and only one output.

Inputs

  • Type of option (Call or Put)
  • Current price of the underlying
  • Strike price of the option
  • Risk-free interest rate
  • Time until expiration (in years)
  • Daily volatility

Output

  • The current value of the option

Implementation

Pseudocode for the Black-Scholes model can be written as follows:

Black-Scholes model pseudocode

For call options, the left half of the formula (stock * normcdf(d1)) models what the exerciser of the option receives. This is a probability-weighted value, as normcdf is the normal standard cumulative distribution function that always returns a value from 0 to 1. The latter half of the formula models what the exerciser of the option pays. This is both a time-discounted value as well as a probability-weighted value. The model accounts for the fact that more volatile stocks will have more valuable options by increasing the value of d1 for a more volatile stock. Likewise, less volatile stock options will decrease the value of d1, thus decreasing the value of the left half of the formula and the option as a whole.

For put options, the formula is very similar. The left half models what the exerciser of the option receives. This value is time-discounted because the buyer of the option chose the strike price in advance. The right half of the formula models what the exerciser of the option pays, as they are obligated to buy one share of stock at its current price.

In Haskell, the Black-Scholes model was fairly easy to implement, as it is a pure mathematical function. A data type Option stores all inputs to the model in order to organize an individual option as well as clarify the inputs to the formula. Because the Black-Scholes model is a pure function, it should be implemented in a pure way — a function called with the same parameters always returns the same result. The pseudocode could be implemented sequentially within the IO Monad, but it is better to use where clauses as such:

where clauses in place of the IO Monad

The Black-Scholes model was the most efficient and most accurate model of the three. It didn’t require the use of lists, matrices, or recursion as the other models did. However, the model’s limitations for the type of options it can handle have prompted others to extend the model. Extensions have accounted for dividend-paying stocks, illiquid stocks, and pricing American options — options that can be exercised at any time. Regardless of its limitations, it is still the most widely used model to price options due to its simplicity and extensibility.

Monte Carlo Simulation

A Monte Carlo Simulation is generally used to predict or estimate values with a random element to them. The process requires running a model thousands to millions of times with a different random variable and averaging the results to hopefully return an accurate result. Monte Carlo methods are used frequently in finance, artificial intelligence in gaming, mathematics, and the sciences. The simulation works well for pricing options because multiple random price paths can be generated in order to calculate and average option payoffs at expiry. Because the simulation provides the price path for the duration of the option, Monte Carlo methods can be used to price Asian options — options that are priced based on the average price of the stock for the duration of the option. Asian options and other exotic options cannot be priced using the Black-Scholes model because it does not provide estimated prices for each day. The general process for pricing an Asian option is as follows:

  1. Generate many random price paths for the duration of the option
  2. Average each individual price path
  3. Calculate option payoffs from averages
  4. Average all payoffs and discount to current time

This simulation required similar inputs to calculate the same output as the Black-Scholes model.

Inputs

  • Type of option (Call or Put)
  • Current price of the underlying
  • Strike price of the option
  • Time until expiration (in years)
  • Expected volatility
  • Constant drift (Expected return)

Output

  • The current value of the option

Implementation

Beginning with the first step of the simulation, multiple price paths must be generated. In an imperative language, it might look something like this:

Pseudocode for generating a random price path

This function would produce one random price walk for a stock. The randsamp() is a random sample from the standard normal distribution to account for uncertainty in market movements. For a Monte Carlo simulation, thousands of these paths would be created. Again, a for loop could be used in an imperative language for this effect. In Haskell, generating the price paths looks a little different due to the fact that random sampling is an inherently impure process. The process looks something like this:

Generating a random price path with Haskell

Because there are no for loops in Haskell, I recursively call genPricePath with the next price passed into it to add the new price to the path.

A similar concept is applied to generate the correct number of price paths for the simulation:

Generating the correct number of price paths for the simulation

Each price path is generated and recursively added to an aggregate list of paths. Haskell’s list structure works well for making sense of large sets of data as well as performing computations on the data.

The next step in the Monte Carlo process is to average each individual price path. Using the Math.Statistics package and the map function, this is only one line of code:

let averages = map Stats.mean allPaths

Next, calculating option payoffs from the list of averages is equally as simple:

Calculating payoffs from a list of average stock prices

For call options, the option payoff is equal to stockAverage-strike. For put options, the option payoff is equal to strike-stockAverage.

After averaging all of the payoffs, the last step of the process is to discount the result to the current time. This is done with using the normal time-discounting procedure:

let discounted = payoff / (1 + riskFree)**(time)

As expected from the nature of Monte Carlo simulations, this simulation became seemingly more accurate as the number of trials increased. However, the time it took to run the simulation didn’t scale very well with the number of trials. Generating an option value using ten thousand price paths took approximately thirty seconds which is on the verge of being called inefficient. Overall, the Monte Carlo simulation seemed to accurately value Asian options. It also provided a foundation to explore pricing options with other types of uncertainty.

Binomial Model

The Binomial Model is similar to the Monte Carlo simulation in the way that multiple price paths are generated. They differ in how those price paths are generated — Monte Carlo being with a random probability and the Binomial model with consistent up and down price movement factors. The price paths are modeled by a trinomial tree as shown:

Trinomial tree price path Source: Joly-Stroebel

Each node movement upwards is a positive change in the stock price while each node movement downwards is a negative change in the stock price. The up and down price movement factors are most affected by the stock’s volatility. A more volatile stock will likely have greater changes in price over a given time period.

The Binomial model has a simple procedure, but the implementation was fairly difficult. The process is as follows:

  1. Generate a price tree to expiry from the current stock price
  2. Calculate payoffs at the final nodes (where n = 3 in the above figure)
  3. Calculate the option value of the first node through backwards induction

The model has similar inputs and output as the previous models but can account for dividend-paying stocks unlike the Black-Scholes model.

Inputs

  • Type of option (Call or Put)
  • Current price of the underlying
  • Strike price of the option
  • Time until expiration (in years)
  • Risk-free interest rate
  • Volatility
  • Dividend yield (percentage)
  • Height of the price tree

Output

  • The current value of the option

Implementation

The first decision I had to make in regards to implementation was choosing how to structure the tree in Haskell. I ended up using a matrix package that stores rows as vectors in a list structure. The first row of the matrix represents a positive change in the stock’s price while the first column represents a negative change in the stock’s price. More generally, a move to the right is a positive change, and a move downwards is a negative change.

The first step is to generate the price tree and store it in a matrix structure. Because of the layout of the matrix, the matrix can be iteratively filled in. The only inputs needed to complete the price tree are the current stock price, the number of ups and downs, and the positive price movement factor. Filling in one cell of the matrix is done like so:

Calculate a single cell of the stock price path

The map function can then be used to iteratively fill out the entire matrix:

priceTree = map (genPrice steps option) (pairs [0..(steps — 1)])

The pairs function returns a list of tuples of each ordered pair in the matrix. For example, pairs [0..2] would return [(0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2)]. After mapping genPrice over the ordered pairs, it can be turned into a matrix like so:

priceMatrix = fromList steps steps $ priceTree

The matrix structure allows quicker indexing than a normal list of lists structure in Haskell.

The next step is to calculate the option value from the final price nodes. This can be done iteratively again using the map function to map over a list of the final nodes’ ordered pairs:

Generating an option value matrix with expiry nodes filled in

Once the option value matrix has all final nodes filled in, the last step in the process is to use backwards induction to fill in the option value at all prior nodes. The purpose of backwards induction is to discount the payoffs at all expiry nodes to the current day. The recursive formula is as follows:

Vn = e^(-rate * deltaT) * (prob * valueUp + (1-prob) * valueDown)

In Haskell, the recursive formula is fairly straightforward:

However, without memoization — storing the values of computed functions for later reuse — this formula is sluggish for trees of height twenty-two or greater. The greater the tree-height, the greater accuracy of the model, so memoization had to be implemented. I eventually turned to a package called UglyMemo to add memoization using a little Haskell magic. After struggling copius amounts, the solution ended up being simple:

optionMemo = memo optionVal

Internally, memo is storing all calls to optionVal in a Map structure. The Map structure is a key-value store, so the function parameters are stored as the key and the function result is stored as the value for each call.

With this optimization, the option value can be calculated using tree-heights of 150+. The improvement in tree-height then improved the accuracy of the model to within under 1% of the Black-Scholes model.

Takeaways

While all models produced the expected result, the Black-Scholes model produced it the quickest. The other models aren’t slow now, but both the Binomial model and Monte Carlo simulation went through multiple iterations for optimization. The first big idea I took away from this project is that when things seem to be slower than they should be, something is usually wrong. It seems like common sense now, but it was never something I paid attention to as a programmer. This was especially prevalent in the first iteration of the Binomial model. Recursively calling the optionVal function over four million times without memoizing function calls was not a viable solution to quickly retrieve an accurate option value.

Next, I learned the importance of testing. Writing software without tests is like riding a bike for the first time but without a helmet. Writing tests helps hedge against unexpected errors. Testing in a type-safe language like Haskell can seem counterintuitive, but it required me to think of and protect against many uses of the software. Testing option scenarios like a checking the value of a worthless option or even simply testing a put option helped hedge against errors in my code.

Another takeaway for me was simply thinking in a functional way. Any pseudocode or implementations I learned from were written in an imperative way. Sequential programming can be used in Haskell, but it certainly isn’t best practice. Using lists to store all data is also possible but possibly not as clear as using other data structures. Although some Haskellers only care about conciseness, I tried to keep style as well as expressiveness in mind when writing my Haskell code.

Possible Extensions

Though all models worked as they should, I wish I would have investigated some of the other uses of them.

One possible extension I had in mind was to write a parser of online option data in order to find undervalued options. An undervalued option would present a good opportunity to buy that option. Undervalued options occur most when buyers and sellers have seen low price activity for a stock over a given period of time.

Another extension that would be worthwhile to explore would be using the models to value equity stake in a company. Early hires in a company are usually given stock options in addition to their salaries. Deciding how much money in stock options is fair for both parties would be interesting to look into. One would have to account for the growth rate of the company as well as how much those stock options would be worth in the future (similar to a strike price).

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.