examples/notebooks/tutorials/option-greeks.ipynb
<a href="https://colab.research.google.com/github/pathwaycom/pathway/blob/main/examples/notebooks/tutorials/option-greeks.ipynb" target="_parent"></a>
In the cell below, we install Pathway into a Python 3.10+ Linux runtime.
If you are running in Google Colab, please run the colab notebook (Ctrl+F9), disregarding the 'not authored by Google' warning.
The installation and loading time is less than 1 minute.
%%capture --no-display
!pip install --prefer-binary pathway
import logging
logging.basicConfig(level=logging.CRITICAL)
Option Greeks are essential tools in financial risk management, measuring an option's price sensitivity. In this article, you'll learn to compute Option Greeks using the Black/Black 76 model, a variant of the more known Black-Scholes model, and Databento's real-time and historical market data APIs. You'll compute the key Option Greeks, Delta, Gamma, Theta, Vega, and Rho, updating these values in real-time with Pathway to match the real-time data provided by Databento.
You can find the sources of the entire project in our public repository.
Pathway is a Python data processing framework for analytics and AI pipelines over data streams. It’s the ideal solution for real-time processing use cases like computing the Option Greeks. Pathway comes with an easy-to-use Python API, syntax that is simple and intuitive, and you can use the same code for both batch and streaming processing. Pathway is powered by a scalable Rust engine based on Differential Dataflow and performing incremental computation.
Databento is a market data provider aiming at making access to institutional-grade financial data simpler and faster. By providing the data directly without third-parties, Databento provides market data APIs with low latency and without data loss. Their simple Python APIs are an ideal data source to perform real-time financial analysis using Pathway. Don't hesitate to browse Databento catalog to see all the available data.
Options are financial derivatives that give the holder the right, but not the obligation, to buy or sell an underlying asset at a specified price within a certain period. On the other hand, futures are contracts that obligate the parties involved to buy or sell an asset at a predetermined price on a specified future date. Options are commonly used in various financial strategies, including hedging and speculation.
In this tutorial, you are going to manipulate options on futures.
There are two kind of options: (1) call options and (2) put options. A call option gives the holder the right, but not the obligation, to buy the underlying asset at a specified price (the strike price) before or at the option's expiration date. On the other hand, a put option gives the holder the right, but not the obligation, to sell the underlying asset at the strike price before or at the expiration date.
In options trading, Option Greeks are metrics used to assess the sensitivity of option prices and provide detailed, quantifiable measures of various risk factors. Using Option Greeks, traders and risk managers make more informed, strategic decisions, enhancing their ability to manage risk and optimize returns.
Each Option Greek measures the sensisibility of option prices to different factors:
These metrics help traders and risk managers understand and hedge the risks associated with options positions. You can learn more about Option Greeks here .
First, let's introduce some notations. Let's define $d_1, d_2$ to help us define the Greeks. Here,
$$ d_1 = \frac{ln(\frac{F}{K}) + (0.5 \cdot \sigma^2)T}{\sigma \sqrt{T}}, d_2 = d_1 - \sigma\sqrt{T} $$
The Option Greeks are defined using $d_1$ and $d_2$. Formula changes depending on whether the option is a call or a put.
Our article is inspired by this article from Databento, which computes the volatility for options for the front-month contract. You can check out their page for more insight.
Let's get started!
First, you need financial data. Stock market data is usually public and can be accessed using APIs. Databento provides simple and fast Python APIs to access market data. You can signup and get free credits. You will obtain an API key with your account: save it, you will need it to access the data.
To continue, make sure to install all the needed packages.
!pip install pathway databento pandas scipy numpy python-dotenv
Let's start by importing Pathway and Databento:
#_MD_SHOW_import databento as db
import pathway as pw
# To use advanced features with Pathway Scale, get your free license key from
# https://pathway.com/features and paste it below.
# To use Pathway Community, comment out the line below.
pw.set_license_key("demo-license-key-with-telemetry")
.env fileYou need to use the API key to access Databento's data. You can create an .env file and set an environment variable or paste the API key directly into the code.
To use the .env file, you first need to create the file, and then copy the key directly in the file:
API_KEY = "********"
Then, you need to use the os package to load the variable:
import os
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.environ.get("API_KEY")
#_MD_SHOW_client = db.Historical(API_KEY)
Let's start with static data and switch to streaming once everything is ready. Both Databento and Pathway make switching to streaming very easy.
Let's focus on E-mini S&P 500 futures contracts whose associated symbol is ES.
The data is from the Globex exchange platform of the CME Group.
Options are represented by a specific formatting: root symbol + month + year then type + strike price.
Let's take ESM4 C2950 as an example:
ES, it is a E-mini S&P 500 future contract.ESM4 is called the underlying asset of the of the option, it is the E-mini S&P 500 futures contract that expires in June 2024.
The data is separated into two datasets: the definitions and the orders.
Definitions provides reference information about each instrument. For options, you are going to use those:
This data is static by nature and do not change over time.
In addition to the definitions, you also need the state of the market. You will use the order book. For options, you will need:
You won't need the times of the order as only the orders from the requested time slot will be received. The book order data is dynamic by nature as new orders arrive over time. However, this article will focus on querying historical data: past orders from a given time period. This data is static as it is data from a given time period: all the orders of this time period are known and included in the data.
The CME Globex data can be found in the GLBX.MDP3 dataset and the definition schema. This provides us with useful information about our options, such as the option type or expiration time of the future.
db_dataset = "GLBX.MDP3" # CME Globex MDP 3.0
db_def_schema = "definition" # this provides us the reference information of each instrument
db_def_symbols = ["ES.OPT"] # all options whose root symbol is ES
For historical data, the start time and duration are also needed:
import pandas as pd
start_time = pd.Timestamp("2024-04-04T17:00:00", tz="Us/Central")
data_duration = pd.Timedelta(days=1)
Let's define a custom Pathway connector, to read the data directly from Databento's API. First, you need to declare the schema of the data:
class DefinitionInputSchema(pw.Schema):
ts_recv: int # time in ns when the data was received
raw_symbol: str # symbol of option
expiration: int # expiration time of the option
instrument_class: str # type of option
strike_price: float # see below for what it means
underlying: str # symbol of the first underlying instrument
instrument_id: int # An identifier of the option
Now, you need to define a ConnectorSubject that will define how the data is read and ingested by Pathway.
The market data is accessed in the ConnectorSubject using Databento's time series:
class DefinitionSubject(pw.io.python.ConnectorSubject):
def run(self):
# First, get Databento's data:
data = client.timeseries.get_range(
dataset=db_dataset,
schema=db_def_schema,
symbols=db_def_symbols,
stype_in=db.SType.PARENT,
start=start_time,
end=start_time + data_duration,
)
# Now, ingest the data into Pathway:
for row in data:
# Get the attributes you are interested in
ts_recv = getattr(row, "ts_recv")
raw_symbol = getattr(row, "raw_symbol")
expiration = getattr(row, "expiration")
instrument_class = getattr(row, "instrument_class")
strike_price = getattr(row, "strike_price") / 1e9
underlying = getattr(row, "underlying")
instrument_id = getattr(row, "instrument_id")
# Transmit the data
self.next(
ts_recv=ts_recv,
raw_symbol=raw_symbol,
expiration=expiration,
instrument_class=instrument_class,
strike_price=strike_price,
underlying=underlying,
instrument_id=instrument_id,
)
You can read more about how to access Databento data in Databento's Documentation.
Now you can create the table using the Python connector:
table_es = pw.io.python.read(DefinitionSubject(), schema=DefinitionInputSchema)
Currently, your table contain all the options.
You need to keep only the one you are interested in.
Among all the futures corresponding to the associated future ES, let's focus on ESM4.
In practice you may be interested in the front-month symbol, the one that has the closest expiration date.
front_month_symbol = 'ESM4'
Let's filter the data to only keep the options associated with ESM4 futures.
To only compute the Greeks for the correct options, let's filter out all those whose underlying instrument is different from ESM4:
table_esm4 = table_es.filter(pw.this.underlying==front_month_symbol)
As a safeguard, let's also filter on the instrument_class to make sure the value is either C (CALL) or P (PUT):
table_esm4 = table_esm4.filter((pw.this.instrument_class == 'C') | (pw.this.instrument_class == 'P'))
Now that you have the options, you need to find the associated orders. The prices, used to compute the volatility, are obtained by averaging all the bids and ask prices. Hence, mid-price is the correct term.
This data will be obtained from the mbp-1 schema (Market by price), which provides every event that updates the top price.
To limit data usage, you can query only the symbols present in your data:
table_symbols = table_esm4.reduce(symbol_tuple = pw.reducers.tuple(pw.this.raw_symbol))
symbol_list = [front_month_symbol] + list(pw.debug.table_to_pandas(table_symbols)['symbol_tuple'][0])
As an example, let's only query over a 2 minutes time interval, to limit the number of data to extract and process. Similarly to the definition schema, you will use a custom connector to read the data from Databento:
db_price_schema = "mbp-1"
class MBP1InputSchema(pw.Schema):
raw_symbol: str # the symbol of the option
bid_px: float # the bid price
ask_px: float # the ask price
# Only 2 minutes
query_data_duration = pd.Timedelta(minutes=2)
class MBP1Subject(pw.io.python.ConnectorSubject):
def run(self):
data = client.timeseries.get_range(
dataset=db_dataset,
schema=db_price_schema,
start=start_time,
end=start_time + query_data_duration,
symbols=symbol_list
)
# Databento's instrument map, which will help us get the symbols from the row data
instrument_map = db.common.symbology.InstrumentMap()
instrument_map.insert_metadata(data.metadata)
for row in data:
symbol = instrument_map.resolve(row.instrument_id, row.pretty_ts_recv.date())
levels = getattr(row, "levels")
# INT64_MAX is the mark for unknown bid/ask prices
if levels[0].bid_px > (1 << 63) - 10 or levels[0].ask_px > (1 << 63) - 10:
continue
# Prices unit is actually 1e-9
bid_px = levels[0].bid_px / 1e9
ask_px = levels[0].ask_px / 1e9
self.next(
raw_symbol=symbol,
bid_px=bid_px,
ask_px=ask_px,
)
table_mbp1 = pw.io.python.read(MBP1Subject(), schema=MBP1InputSchema)
To compute the average prices for all bids, you can use a simple groupby/reduce:
table_mbp1 = table_mbp1.groupby(pw.this.raw_symbol).reduce(
raw_symbol=pw.this.raw_symbol,
option_midprice=(pw.reducers.avg(pw.this.bid_px) + pw.reducers.avg(pw.this.ask_px)) / 2,
)
Now, you want to add the data from the table_esm4 to this table_mbp1.
To do so, you need to join the two table on the raw_symbol values:
table_prices = table_esm4.join(
table_mbp1,
pw.left.raw_symbol == pw.right.raw_symbol
).select(
*pw.left, # Adding all the columns from table_esm4
option_midprice=pw.right.option_midprice,
)
Now that the data is ready, you can compute the Option Greeks. To do so, you need several values, as you have seen in the Generic Formulas section. Recall that you need:
This is a bit more complicated, as it is usually determined by the market and is taken from government securities considered free of default risk. This can be considered more of an input parameter given by the user.
Let's use $0.043$ as the value for this interest rate, based on government available data.
interest_rate = 0.043
In the Black/Black 76 Model, the Option Greeks are the options of the front-month contract, this value will be fixed per each front-month contract.
In this case, it will be the stock price/last price of the ESM4 option. You can access rows by value using ix_ref:
table_prices = table_prices.with_columns(
future_price=table_mbp1.ix_ref(front_month_symbol).option_midprice
)
Using the expiration column, you can compute the time to expiration $T$ which is used in our formulas. Because of how the formulas are defined, it is expressed in years.
Be careful, the unit of expiration is nanoseconds.
# Compute the time to expiration, has to be in years
@pw.udf
def compute_time_to_expiration(expiration_time: int) -> float:
return (expiration_time - int(start_time.timestamp() * 1e9)) / (1e9 * 86400 * 365)
table_texp = table_prices.with_columns(
time_to_expiration=compute_time_to_expiration(pw.this.expiration)
)
The volatility represents the market's expectation of the future volatility of the underlying asset over the life of the option. Unlike historical volatility, which measures past price fluctuations, implied volatility is derived from the market price of the option itself.
Using the Black Model, you can infer volatility using known data. The option price will be estimated as being the average of the prices associated to that option symbol. You need to compute $\sigma$ (the volatility) so that the price calculated in the Black Model is the same as the estimated option price. It comes down to finding the root of a polynomial, which will be done using scipy.
What follows next is just the previous formulas being translated into Python.
First, you need to define the function to compute the option price in the Black Model, having computed the volatility, $\sigma$.
import math
def compute_price(
F: float,
K: float,
T: float,
sigma: float,
r: float=interest_rate,
is_call: bool=True
) -> float:
d1 = (math.log(F / K) + (sigma**2 / 2) * T) / (sigma * math.sqrt(T))
d2 = d1 - sigma * math.sqrt(T)
sign = 2 * int(is_call) - 1
return math.exp(-r * T) * sign * (norm.cdf(sign * d1) * F - norm.cdf(sign * d2) * K)
Now, solve the equation by finding the roots of
$$ BlackPrice(\sigma) - Meanprice = 0 $$
using scipy's root finding function. To mark the non-convergence/non-existence of a root, return $None$.
import scipy
from scipy.stats import norm
@pw.udf
def compute_volatility(
F: float,
K: float,
T: float,
_is_call: bool,
option_midprice: float
) -> float | None:
result = scipy.optimize.root_scalar(
lambda sigma: option_midprice - compute_price(
F=F,
K=K,
T=T,
sigma=sigma,
is_call=_is_call
),
x0=0.0001,x1=0.8
)
return result.root if result.converged else None
Now, let's compute the $d_1$, $d_2$ defined as before. Let's define those functions as Pathway user-defined function using the pw.udf decorator. Another alternative would be to declare the functions as simple Python functions and apply them to the columns using pw.apply.
@pw.udf
def compute_d1(
F: float,
K: float,
T: float,
sigma: float,
r: float = interest_rate,
) -> float:
return (math.log(F / K) + (sigma**2 / 2) * T) / (sigma * math.sqrt(T))
@pw.udf
def compute_d2(
F: float,
K: float,
T: float,
sigma: float,
r: float = interest_rate
) -> float:
return (math.log(F / K) + (sigma**2 / 2) * T) / (sigma * math.sqrt(T)) - sigma * math.sqrt(T)
You can now define the functions to compute the Option Greeks using $F$, $K$, $T$, $\sigma$, $d_1$, $d_2$, and $\rho$:
@pw.udf
def compute_delta(F: float, K: float, T: float, sigma: float, d1: float, d2: float, is_call: bool, r: float = interest_rate) -> float:
return math.exp(-r * T) * norm.cdf(d1) if is_call \
else -math.exp(-r * T) * norm.cdf(-d1)
@pw.udf
def compute_gamma(F: float, K: float, T: float, sigma: float, d1: float, d2: float, is_call: bool, r: float = interest_rate) -> float:
return math.exp(-r * T) * norm.pdf(d1) / (F * sigma * math.sqrt(T))
@pw.udf
def compute_theta(F: float, K: float, T: float, sigma: float, d1: float, d2: float, is_call: bool, r: float = interest_rate) -> float:
return (-F * sigma * norm.pdf(d1) / (2 * math.sqrt(T)) - r * K * math.exp(-r * T) * norm.cdf(d2) + r * F * math.exp(-r * T) * norm.cdf(d1)) / 252 if is_call \
else (-F * sigma * norm.pdf(d1) / (2 * math.sqrt(T)) + r * K * math.exp(-r * T) * norm.cdf(-d2) - r * F * math.exp(-r * T) * norm.cdf(-d1)) / 252# per day
@pw.udf
def compute_vega(F: float, K: float, T: float, sigma: float, d1: float, d2: float, is_call: bool, r: float = interest_rate) -> float:
return F * norm.pdf(d1) * math.sqrt(T) * math.exp(-r * T) / 100
@pw.udf
def compute_rho(F: float, K: float, T: float, sigma: float, d1: float, d2: float, is_call: bool, r: float = interest_rate) -> float:
return -T * math.exp(-r * T) * (F * norm.cdf(d1) - K * norm.cdf(d2)) / 100 if is_call \
else -T * math.exp(-r * T) * (K * norm.cdf(-d2) - F * norm.cdf(-d1)) / 100
All you need to do is to use the UDFs you have just defined.
First, let's add a column to determine if the option is a call or a put:
table_texp = table_texp.with_columns(is_call=pw.this.instrument_class == 'C')
Now, you can start computing the Options Greeks. First, let's start with the implied volatility, pre-filtering.
table_volatility_unfiltered = table_texp.with_columns(
volatility=compute_volatility(
pw.this.future_price,
pw.this.strike_price,
pw.this.time_to_expiration,
pw.this.is_call,
pw.this.option_midprice
)
)
Filter out entries were volatility couldn't be computed.
table_sigma = table_volatility_unfiltered.filter(pw.this.volatility.is_not_none())
Having filtered the volatility table, you can now compute the useful variables $d_1, d_2$.
table_d1d2 = table_sigma.with_columns(
d1=compute_d1(pw.this.future_price, pw.this.strike_price, pw.this.time_to_expiration, pw.this.volatility),
d2=compute_d2(pw.this.future_price, pw.this.strike_price, pw.this.time_to_expiration, pw.this.volatility)
)
And finally, you have everything necessary to compute the Option Greeks.
table_greeks = table_d1d2.select(
ts_recv=pw.this.ts_recv,
instrument_id=pw.this.instrument_id, # option identifier
delta=compute_delta(F=pw.this.future_price, K=pw.this.strike_price, T=pw.this.time_to_expiration,
sigma=pw.this.volatility, d1=pw.this.d1, d2=pw.this.d2, is_call=pw.this.is_call),
gamma=compute_gamma(F=pw.this.future_price, K=pw.this.strike_price, T=pw.this.time_to_expiration,
sigma=pw.this.volatility, d1=pw.this.d1, d2=pw.this.d2, is_call=pw.this.is_call),
theta=compute_theta(F=pw.this.future_price, K=pw.this.strike_price, T=pw.this.time_to_expiration,
sigma=pw.this.volatility, d1=pw.this.d1, d2=pw.this.d2, is_call=pw.this.is_call),
vega=compute_vega(F=pw.this.future_price, K=pw.this.strike_price, T=pw.this.time_to_expiration,
sigma=pw.this.volatility, d1=pw.this.d1, d2=pw.this.d2, is_call=pw.this.is_call),
rho=compute_rho(F=pw.this.future_price, K=pw.this.strike_price, T=pw.this.time_to_expiration,
sigma=pw.this.volatility, d1=pw.this.d1, d2=pw.this.d2, is_call=pw.this.is_call),
)
Now that you have successfully computed the Option Greeks, you can output the results to your favorite system. Pathway supports many different connectors.
As an example, you might want to send the results to a CSV file, using Pathway CSV output connector:
pw.io.csv.write(table_greeks, "./options-greeks.csv")
Now, all you need it to run the computation:
#_MD_SHOW_pw.run()
The Options Greeks will be computed and stored in the option-greeks.csv file.
For a more user-friendly output, you can also output the data to a dashboard to visualize your results. As an example, you can easily set up a dashboard using Streamlit:
You can find the sources to obtain this dashboard in our public GitHub repository.
Pathway has a unified engine capable of processing both static and streaming data, making it easy to transition from one mode to the other.
You can easily make the book orders dynamic by updating the input connector ConnectorSubject (MBP1Subject) to simulate real-time data streaming by adding a time.sleep() function call after each next call.
This small modification introduces a delay between data points, emulating the arrival of new data over time.
The updated source is available in our public GitHub repository.
In this case, Pathway will update the results every time the input changes, at the reception of new data point from the mbp-1 data for example.
Furthermore, you can use Databento live APIs to obtain the market live data for the book orders and have the Option Greeks updated in real-time as the live data is ingested. This way, you can make full use of the streaming mode.
Congratulations! You now are able to compute the implied volatility and the Option Greeks using Databento to extract the historical market data and Pathway to process it. Pathway is the ideal tool for quantitative projects, allowing you to compute complex financial metrics like Options Greek in real-time. If you are interested, check our example about Bollinger Bands or reach out to us on Discord!
We would like to express our gratitude to Databento for their valuable help and support in the creation of this article.