Investing in stocks is a popular way for individuals to grow their wealth over time. When you buy a stock, you are purchasing a small piece of ownership in a company. As a shareholder, you have the potential to earn money through capital appreciation (when the stock price increases) and dividends (a portion of the company's profits distributed to shareholders). The stock market allows investors to buy and sell shares of publicly traded companies.

To invest in stocks, you typically need to open a brokerage account with a financial institution. You can then research and select stocks that align with your investment goals and risk tolerance. It's important to diversify your portfolio by investing in a variety of companies across different sectors to minimize risk. While stocks have historically provided higher returns compared to other investments like bonds, they also carry more risk due to market volatility.

Then there are options. Options are a type of financial derivative that give the holder the right, but not the obligation, to buy or sell an underlying stock at a predetermined price (called the strike price) on or before a specific date (the expiration date). It is important to note "or before a specific date" because this is a uniquely American attribute of options. European options can only be exercised on the expiration date. This American quirk is why the Black-Scholes model is not perfect for American options. The Bjerksund-Stensland model is a better fit for American options. We will possibly look at Bjerkund-Stensland in a future post, but for our purposes, we will mostly ignore the American quirk.

There are two types of options:

  1. Call Options: A call option gives the holder the right to buy the underlying asset at the strike price. Investors buy call options when they believe the price of the underlying asset will increase. If the price rises above the strike price, the investor can exercise their right to buy the asset at the lower strike price and sell it at the higher market price, making a profit.
  2. Put Options: A put option gives the holder the right to sell the underlying asset at the strike price. Investors buy put options when they believe the price of the underlying asset will decrease. If the price drops below the strike price, the investor can buy the asset at the lower market price and exercise their right to sell it at the higher strike price, making a profit.

Options are often used for hedging, speculation, and income generation. They offer leverage, allowing investors to control a larger position with a smaller investment. However, options trading carries significant risks, as options can expire worthless if the underlying asset doesn't move in the anticipated direction. It's important to understand the risks and rewards of options trading before getting started. None of this is investment advice. Consult a financial advisor before making any investment decisions.

In the context of stock options, "the Greeks" refer to a set of risk measures that quantify the sensitivity of an option's price to various factors. These measures are named after Greek letters and help options traders and investors understand and manage the risks associated with their options positions. The main Greeks are:

  1. Delta (Δ): Delta measures the rate of change in an option's price with respect to changes in the underlying asset's price. It represents how much the option's price is expected to move for a $1 change in the underlying stock price. Call options have positive delta (between 0 and 1), while put options have negative delta (between -1 and 0).
  2. Gamma (Γ): Gamma measures the rate of change in an option's delta with respect to changes in the underlying asset's price. It indicates how much the option's delta is expected to change for a $1 change in the underlying stock price. Options with high gamma are more sensitive to price changes in the underlying asset.
  3. Theta (Θ): Theta measures the rate of change in an option's price with respect to the passage of time, assuming all other factors remain constant. It represents how much the option's price is expected to decay per day as it approaches expiration. Options lose value over time due to time decay, and theta is typically negative.
  4. Vega (ν): Vega measures the rate of change in an option's price with respect to changes in the implied volatility of the underlying asset. It represents how much the option's price is expected to change for a 1% change in implied volatility. Options with higher vega are more sensitive to changes in volatility.
  5. Rho (ρ): Rho measures the rate of change in an option's price with respect to changes in interest rates. It represents how much the option's price is expected to change for a 1% change in the risk-free interest rate. Rho is typically positive for call options and negative for put options.

Understanding and monitoring the Greeks is essential for options traders to make informed decisions about their positions. The Greeks help traders assess the potential risks and rewards of their options trades, as well as how their positions might be affected by changes in the underlying asset's price, time to expiration, volatility, and interest rates. Traders can use this information to adjust their positions, implement hedging strategies, and manage their overall risk exposure.

Most trading platforms and options analysis tools provide real-time data on the Greeks for individual options and option strategies. As such, most people do not need to calculate the Greeks by hand. However, it is important to understand the concepts behind the Greeks and how they influence options pricing and behavior. By mastering the Greeks, options traders can enhance their trading strategies and improve their overall performance in the options market.

But, what if you want to calculate the Greeks yourself? The Black-Scholes model is a mathematical formula used to price options and calculate the Greeks. It assumes that the underlying asset follows a lognormal distribution and that the option can only be exercised at expiration. The Black-Scholes model provides theoretical values for call and put options based on the current stock price, strike price, time to expiration, risk-free interest rate, and implied volatility.

Before we get started, let's define an important intermediate function, calculate_d1, that calculates the d1 parameter for many of the Greeks.

The d1 parameter, also known as the d+ parameter, is a component of the Black-Scholes option pricing model used to calculate various Greeks, such as delta, gamma, and vega. It represents the number of standard deviations the logarithm of the stock price is above the logarithm of the present value of the strike price.

The formula for d1 is:

d1=ln(SK)+(r+σ22)TσT

Where:

  • S is the current price of the underlying asset
  • K is the strike price of the option
  • r is the risk-free interest rate
  • σ is the volatility of the underlying asset
  • T is the time to expiration of the option (in years)

The d1 parameter is used in the calculation of the cumulative standard normal distribution function N(x) and the standard normal probability density function N'(x) in the Black-Scholes formulas for various Greeks.

For example, in the delta formula:

Δ=N(d1)

And in the gamma formula:

Γ=N'(d1)SσT

The d1 parameter helps to determine the sensitivity of the option price to changes in the underlying asset price, volatility, and time to expiration. It is a crucial component in understanding and calculating the Greeks for options trading and risk management.

/**
 * Calculate the d1 parameter for the Black-Scholes model.
 * 
 * Parameters
 * ----------
 * s : float
 *     The current price of the underlying asset.
 * k : float
 *     The strike price of the option.
 * t : float
 *     The time to expiration of the option in years.
 * r : float
 *     The risk-free interest rate.
 * sigma : float
 *     The volatility of the underlying asset.
 * q : float
 *     The dividend yield of the underlying asset.
 * 
 * Returns
 * -------
 * float
 *     The d1 parameter.
 */
fn calculate_d1(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64) -> f64 {
    (f64::ln(s / k) + (r - q + 0.5 * sigma.powi(2)) * t) / (sigma * f64::sqrt(t))
}

Delta Calculation

The delta of an option measures the rate of change in the option's price with respect to changes in the underlying asset's price. It represents how much the option's price is expected to move for a $1 change in the underlying stock price. The delta of a call option is between 0 and 1, while the delta of a put option is between -1 and 0. The formula for calculating the delta of a call option using the Black-Scholes model is:

Δ=N(ln(SK)+(r+σ22)TσT)

Where:

  • Δ is the delta of the option
  • N(x) is the standard normal cumulative distribution function
  • S is the current price of the underlying asset
  • K is the strike price of the option
  • r is the risk-free interest rate
  • σ is the volatility of the underlying asset
  • T is the time to expiration of the option (in years)

Rust Implementation

/**
 * Calculate the delta of an approximate American option using the Black-Scholes model.
 * 
 * Parameters
 * ----------
 * s : float
 *     The current price of the underlying asset.
 * k : float
 *     The strike price of the option.
 * t : float
 *     The time to expiration of the option in years.
 * r : float
 *     The risk-free interest rate.
 * sigma : float
 *     The volatility of the underlying asset.
 * q : float
 *     The dividend yield of the underlying asset.
 * option_type : str
 *     The type of option. Use 'call' for a call option and 'put' for a put option.
 * 
 * Returns
 * -------
 * float
 *     The delta of the option.
 */
#[pyfunction]
fn black_scholes_delta(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64, option_type: &str) -> PyResult<f64> {
    let d1 = calculate_d1(s, k, t, r, sigma, q) as f64;
    let norm = Normal::new(0.0, 1.0).unwrap();

    let delta = match option_type {
        "call" => f64::exp(-q * t) * norm.cdf(d1),
        "put" => -f64::exp(-q * t) * norm.cdf(-d1),
        _ => return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid option type. Use 'call' or 'put'."))
    };

    Ok(delta)
}

Gamma Calculation

The gamma of an option measures the rate of change in the option's delta with respect to changes in the underlying asset's price. It indicates how much the option's delta is expected to change for a $1 change in the underlying stock price. The formula for calculating the gamma of an option using the Black-Scholes model is:

Γ=N'(ln(SK)+(r+σ22)TσT)SσT

Where:

  • Γ is the gamma of the option
  • N'(x) is the standard normal probability density function
  • S is the current price of the underlying asset
  • K is the strike price of the option
  • r is the risk-free interest rate
  • σ is the volatility of the underlying asset
  • T is the time to expiration of the option (in years)

Rust Implementation

/**
 * Calculate the gamma of an approximate American option using the Black-Scholes model.
 * 
 * Parameters
 * ----------
 * s : float
 *     The current price of the underlying asset.
 * k : float
 *     The strike price of the option.
 * t : float
 *     The time to expiration of the option in years.
 * r : float
 *     The risk-free interest rate.
 * sigma : float
 *     The volatility of the underlying asset.
 * q : float
 *     The dividend yield of the underlying asset.
 * 
 * Returns
 * -------
 * float
 *     The gamma of the option.
 */
#[pyfunction]
fn black_scholes_gamma(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64) -> f64 {
    let d1 = calculate_d1(s, k, t, r, sigma, q) as f64;
    let norm_dist = Normal::new(0.0, 1.0).unwrap();
    let gamma = norm_dist.pdf(d1) * E.powf(-q * t) / (s * sigma * f64::sqrt(t));

    gamma
}

Theta Calculation

The theta of an option measures the rate of change in the option's price with respect to the passage of time, assuming all other factors remain constant. It represents how much the option's price is expected to decay per day as it approaches expiration. The formula for calculating the theta of an option using the Black-Scholes model is:

Θ=-SN'(ln(SK)+(r+σ22)TσT)2T

Where:

  • Θ is the theta of the option
  • N'(x) is the standard normal probability density function
  • S is the current price of the underlying asset
  • K is the strike price of the option
  • r is the risk-free interest rate
  • σ is the volatility of the underlying asset
  • T is the time to expiration of the option (in years)

Rust Implementation

/**
 * Calculate the theta of an approximate American option using the Black-Scholes model.
 * 
 * Parameters
 * ----------
 * s : float
 *     The current price of the underlying asset.
 * k : float
 *     The strike price of the option.
 * t : float
 *     The time to expiration of the option in years.
 * r : float
 *     The risk-free interest rate.
 * sigma : float
 *     The volatility of the underlying asset.
 * q : float
 *     The dividend yield of the underlying asset.
 * option_type : str
 *     The type of option. Use 'call' for a call option and 'put' for a put option.
 * 
 * Returns
 * -------
 * float
 *     The theta of the option.
 */

#[pyfunction]
fn black_scholes_theta(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64, option_type: &str) -> PyResult<f64> {
    let d1 = calculate_d1(s, k, t, r, sigma, q) as f64;
    let d2 = d1 - sigma * f64::sqrt(t);
    let norm = Normal::new(0.0, 1.0).unwrap();

    let theta = match option_type {
        "call" => {
            - (s * sigma * f64::exp(-q * t) * norm.pdf(d1)) / (2.0 * f64::sqrt(t))
            - r * k * f64::exp(-r * t) * norm.cdf(d2)
            + q * s * f64::exp(-q * t) * norm.cdf(d1)
        },
        "put" => {
            - (s * sigma * f64::exp(-q * t) * norm.pdf(d1)) / (2.0 * f64::sqrt(t))
            + r * k * f64::exp(-r * t) * norm.cdf(-d2)
            - q * s * f64::exp(-q * t) * norm.cdf(-d1)
        },
        _ => return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid option type. Use 'call' or 'put'."))
    };

    // Convert to per-day theta
    Ok(theta / 365.0)
}

Vega Calculation

The vega of an option measures the rate of change in the option's price with respect to changes in the implied volatility of the underlying asset. It represents how much the option's price is expected to change for a 1% change in implied volatility. The formula for calculating the vega of an option using the Black-Scholes model is:

ν=SN'(ln(SK)+(r+σ22)TσT)

Where:

  • ν is the vega of the option
  • N'(x) is the standard normal probability density function
  • S is the current price of the underlying asset
  • K is the strike price of the option
  • r is the risk-free interest rate
  • σ is the volatility of the underlying asset
  • T is the time to expiration of the option (in years)

Rust Implementation

/**
 * Calculate the vega of an approximate American option using the Black-Scholes model.
 * 
 * Parameters
 * ----------
 * s : float
 *     The current price of the underlying asset.
 * k : float
 *     The strike price of the option.
 * t : float
 *     The time to expiration of the option in years.
 * r : float
 *     The risk-free interest rate.
 * sigma : float
 *     The volatility of the underlying asset.
 * q : float
 *     The dividend yield of the underlying asset.
 * 
 * Returns
 * -------
 * float
 *     The vega of the option.
 */
#[pyfunction]
fn black_scholes_vega(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64) -> PyResult<f64> {
    let d1 = calculate_d1(s, k, t, r, sigma, q) as f64;
    let norm = Normal::new(0.0, 1.0).unwrap();

    let vega = s * f64::exp(-q * t) * norm.pdf(d1) * f64::sqrt(t);

    Ok(vega)
}

Rho Calculation

The rho of an option measures the rate of change in the option's price with respect to changes in interest rates. It represents how much the option's price is expected to change for a 1% change in the risk-free interest rate. The formula for calculating the rho of an option using the Black-Scholes model is:

ρ=KTN'(ln(SK)+(r+σ22)TσT)

Where:

  • ρ is the rho of the option
  • N'(x) is the standard normal probability density function
  • S is the current price of the underlying asset
  • K is the strike price of the option
  • r is the risk-free interest rate
  • σ is the volatility of the underlying asset
  • T is the time to expiration of the option (in years)

Rust Implementation

/**
 * Calculate the rho of an approximate American option using the Black-Scholes model.
 * 
 * Parameters
 * ----------
 * s : float
 *     The current price of the underlying asset.
 * k : float
 *     The strike price of the option.
 * t : float
 *     The time to expiration of the option in years.
 * r : float
 *     The risk-free interest rate.
 * sigma : float
 *     The volatility of the underlying asset.
 * q : float
 *     The dividend yield of the underlying asset.
 * option_type : str
 *     The type of option. Use 'call' for a call option and 'put' for a put option.
 * 
 * Returns
 * -------
 * float
 *     The rho of the option.
 */
#[pyfunction]
fn black_scholes_rho(s: f64, k: f64, t: f64, r: f64, sigma: f64, q: f64, option_type: &str) -> PyResult<f64> {
    let d1 = calculate_d1(s, k, t, r, sigma, q) as f64; 
    let d2 = d1 - sigma * f64::sqrt(t);
    let norm = Normal::new(0.0, 1.0).unwrap();

    let rho = match option_type {
        "call" => k * t * f64::exp(-r * t) * norm.cdf(d2),
        "put" => -k * t * f64::exp(-r * t) * norm.cdf(-d2),
        _ => return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid option type. Use 'call' or 'put'."))
    };

    Ok(rho)
}

Notes on Implied Volatility

Implied volatility is the volatility value that makes the theoretical option price calculated using the Black-Scholes model equal to the observed market price of the option. It represents the market's expectation of future volatility and is a crucial input in options pricing models. Traders use implied volatility to assess the relative value of options and make informed trading decisions. For my purposes, I use an IV that is provided by my market data subscription. I do not calculate it myself. However, here is a Rust implementation of the Black-Scholes formula for implied volatility.

Rust Implementation

use std::f64::consts::PI;

fn implied_volatility(stock_price: f64, strike_price: f64, risk_free_rate: f64, time_to_expiry: f64, option_price: f64, option_type: &str) -> f64 {
    let mut implied_vol = 0.5;
    let max_iterations = 100;
    let precision = 0.00001;

    for _ in 0..max_iterations {
        let d1 = (f64::ln(stock_price / strike_price) + (risk_free_rate + implied_vol.powi(2) / 2.0) * time_to_expiry) / (implied_vol * time_to_expiry.sqrt());
        let d2 = d1 - implied_vol * time_to_expiry.sqrt();
        let normal_cdf_d1 = (1.0 + erf(d1 / 2.0_f64.sqrt())) / 2.0;
        let normal_cdf_d2 = (1.0 + erf(d2 / 2.0_f64.sqrt())) / 2.0;
        let call_price = stock_price * normal_cdf_d1 - strike_price * f64::exp(-risk_free_rate * time_to_expiry) * normal_cdf_d2;
        let put_price = strike_price * f64::exp(-risk_free_rate * time_to_expiry) * (1.0 - normal_cdf_d2) - stock_price * (1.0 - normal_cdf_d1);

        let price_diff = match option_type {
            "call" => call_price - option_price,
            "put" => put_price - option_price,
            _ => panic!("Invalid option type. Use 'call' or 'put'."),
        };

        if price_diff.abs() < precision {
            break;
        }

        implied_vol -= price_diff / (stock_price * time_to_expiry.sqrt() * normal_pdf(d1));
    }

    implied_vol
}

fn normal_pdf(x: f64) -> f64 {
    f64::exp(-x.powi(2) / 2.0) / (2.0 * PI).sqrt()
}

/**
 * This function approximates the error function using a polynomial approximation.
 */
fn erf(x: f64) -> f64 {
    // Approximation of the error function
    let a1 = 0.254829592;
    let a2 = -0.284496736;
    let a3 = 1.421413741;
    let a4 = -1.453152027;
    let a5 = 1.061405429;
    let p = 0.3275911;

    let sign = if x < 0.0 { -1.0 } else { 1.0 };
    let x = x.abs();

    let t = 1.0 / (1.0 + p * x);
    let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * f64::exp(-x.powi(2));

    sign * y
}

If you are wondering where the magic numbers in erf come from, the constants used in the approximation of the error function (erf) in the code example are derived from a specific approximation formula known as the "Abramowitz and Stegun approximation" or the "Rational Chebyshev approximation." This approximation was introduced by Milton Abramowitz and Irene Stegun in their book "Handbook of Mathematical Functions with Formulas, Graphs, and Mathematical Tables" (1964).

The approximation formula is as follows:

erf(x)1-(a1*t+a2*t2+a3*t3+a4*t4+a5*t5)*exp(-x2)

where:

t=11+px

and the constants are:

a1 = 0.254829592
a2 = -0.284496736
a3 = 1.421413741
a4 = -1.453152027
a5 = 1.061405429
p = 0.3275911

These constants were determined by fitting the approximation formula to the actual values of the error function over a specific range. The approximation is accurate to within ±1.5 × 10^-7 for all real values of x.

It's important to note that this approximation is just one of many possible approximations for the error function. Other approximations with different constants and formulas may also be used, depending on the required accuracy and performance trade-offs.

In most cases, it is recommended to use a well-tested and optimized implementation of the error function provided by standard libraries or numerical computation libraries, rather than implementing the approximation yourself, unless you have specific requirements or constraints. I used Anthropic's "Claude Opus-3" large language model to generate the constants for the approximations.

Conclusion

The Greeks are essential tools for options traders to understand and manage the risks associated with their options positions. By calculating the Greeks using the Black-Scholes model, traders can estimate and assess the potential risks and rewards of their options trades and make informed decisions about their positions.

Here is a repository for the Rust implementation of the Black-Scholes model and the calculation of the Greeks for options trading. The repository contains the functions discussed in this article.