How to compute options theta using QuantLib

March 30, 2024
Facebook logo.
Twitter logo.
LinkedIn logo.
Get this code in Google Colab

How to compute options theta using QuantLib

Calculating the Greeks like theta for American options is tough because they can be exercised before they expire. Instead of a single formula like Black-Scholes, we need to use numerical methods. Today we’ll look at theta which is critical for options traders as you’ll see below.

The good news?

QuantLib can do it in a few lines of Python code. It’s a library that offers a suite of tools for pricing options using many different methods. And in today’s newsletter, we’re going to walk through it step-by-step.

Let’s go!

How to compute options theta using QuantLib

Options theta represents the sensitivity of the option's price to the passage of time. It’s expressed as the change in the option's price for a one-day decrease in the time to expiration. In financial terms, theta can be considered the "time decay" of an option's value.

Theta is a critical concept for options traders to understand.

For traders holding long positions in options, a negative theta signifies a loss in value with each passing day. This loss in value can erode potential profits. This is especially true as the option approaches expiration.

If you’re short, theta can work in your favor.

For those holding short positions, a negative theta is beneficial. It can make it cheaper to close out a position or allow the option to expire worthless. In both cases maximizing the income received from the initial sale of the option.

Let’s see how to compute options theta with QuantLib.

Imports and set up

We’ll start with importing NumPy for some math, OpenBB for data, and QuantLib to calculate theta.

1import numpy as np
2import QuantLib as ql
3from openbb import obb
4import warnings
5warnings.filterwarnings("ignore")
6obb.user.preferences.output_type = "dataframe"

Using OpenBB, grab the options chains to find at-the-money (ATM) strikes, expirations, and historical price data.

1symbol = "AAPL"
2chains = obb.derivatives.options.chains(symbol=symbol)
3prices = obb.equity.price.historical(symbol=symbol, provider="yfinance")
4expiration = chains.expiration.unique()[5]
5strikes = chains.query("`expiration` == @expiration").strike.to_frame()

Note we’re picking the 6th nearest expiration date. You can pick whichever expiration you’d like.

Finally, let’s set up our variables for QuantLib using the market data.

1underlying_price = prices.close.iat[-1]
2strike_price = (
3    strikes
4    .loc[
5        (strikes-underlying_price)
6        .abs()
7        .sort_values("strike")
8        .index[0]
9    ].strike
10)
11volatility = prices.close.pct_change().std() * np.sqrt(252)
12maturity = ql.Date(
13    expiration.day,
14    expiration.month,
15    expiration.year,
16)
17dividend_yield = 0.0056
18risk_free_rate = 0.05
19calculation_date = ql.Date.todaysDate()
20ql.Settings.instance().evaluationDate = calculation_date

We use pandas to find the strike price that’s closest to the last traded price. Then we find the last closing price and the annualized volatility. QuantLib expects it’s own date objects which we create using the expiration we selected. Finally, we hard code the dividend yield, risk free rate, and set the evaluation date to today.

Market environment set up

QuantLib works by first establishing the market environment. While this might seem overkill for a simple vanilla call option, it is necessary for more advanced option types. QuantLib expects four “handles” to compute the options values.

First, we create a handle to a simple quote object which takes the underlying asset's price for use in option pricing models.

1spot_handle = ql.QuoteHandle(
2    ql.SimpleQuote(underlying_price)
3)

Next, we build the yield term structure which in this case is a flat forward curve using the risk free rate we defined above.

1yield_handle = ql.YieldTermStructureHandle(
2    ql.FlatForward(
3        calculation_date, 
4        risk_free_rate, 
5        ql.Actual365Fixed()
6    )
7)

We do the same for the dividend yield. QuantLib also supports discrete dividend dates.

1dividend_handle = ql.YieldTermStructureHandle(
2    ql.FlatForward(
3        calculation_date, 
4        dividend_yield, 
5        ql.Actual365Fixed()
6    )
7)

Finally, we build a handle to the the volatility surface. In our example we assume a constant volatility over time and across strike prices.

1volatility_handle = ql.BlackVolTermStructureHandle(
2    ql.BlackConstantVol(
3        calculation_date, 
4        ql.NullCalendar(), 
5        volatility, 
6        ql.Actual365Fixed()
7    )
8)

Set up the options pricing engine

We can now build the Black-Scholes-Merton process which models the dynamics of the underlying asset.

1bs_process = ql.BlackScholesMertonProcess(
2    spot_handle, 
3    dividend_handle, 
4    yield_handle, 
5    volatility_handle
6)

Using the Black-Scholes-Merton process, we can build our option pricer.

1engine = ql.BinomialVanillaEngine(bs_process, "crr", steps=1000)

This code sets up a binomial tree pricing engine for our vanilla option using the Cox-Ross-Rubinstein (CRR) model with 1000 time steps. The engine is based on the specified Black-Scholes-Merton process.

Value the option

We’re (finally) ready to define our options and get the Greeks.

1exercise = ql.AmericanExercise(calculation_date, maturity) 
2call_option = ql.VanillaOption(
3    ql.PlainVanillaPayoff(ql.Option.Call, strike_price),
4    exercise
5)
6call_option.setPricingEngine(engine)
7
8call_theta = call_option.theta() / 365

We define the American style options exercise and use it to initialize a vanilla option with a plain vanilla payoff. This setup creates the call option. We then set the pricing engine we created above and get the theta.

Note QuantLib returns the annualized theta so to get the theta per day, we need to divide by 365.

Next steps

While this example values a plain vanilla option using constant volatility, QuantLib supports many features to better reflect reality. Try using a BlackVarianceCurve if you want to specify the at-the-money volatility with respect to time or BlackVarianceSurface if you want to specify the smile as well.

Man with glasses and a wristwatch, wearing a white shirt, looking thoughtfully at a laptop with a data screen in the background.