Generating Income with a Protective Put Strategy

Generating Weekly Income with Protective Put Options

Preamble

Several posts back, I mentioned that would likely write a post about an options strategy that generates consistent income along with providing protection from downside risk. This write-up is just that. Just remember that options are not for the faint of heart and they do carry considerable risks if not managed correctly.

Generating Weekly Income with Protective Put Options

Introduction

Put options can be used as a strategy for generating weekly income while providing downside protection. This white paper will discuss the concept of buying a long-term put option with a strike price slightly below the current share price and simultaneously selling short-term put options with a strike price close to the current share price. This strategy is known as a "Protective Put" or "Married Put" strategy. It can also be classified as a type of calendar spread, specifically a "Diagonal Put Calendar Spread."

Strategy Overview

The strategy involves the following steps:

  1. Buy a put option with a strike price slightly below the current share price and an expiration date 30 to 120 days in the future, possibly after the next earnings release.
  2. Sell a put option with a strike price as close to the current share price as possible and an expiration date within the next week.
  3. Ensure that the premium collected from selling the put option exceeds the cost of buying the long-term put option.
  4. Repeat the process of selling weekly put options to generate income while holding the long-term protective put.

Diagonal Put Calendar Spread

The protective put strategy can be classified as a diagonal put calendar spread because it involves buying and selling put options with different expiration dates and strike prices on the same underlying stock.

The main characteristics of this diagonal put calendar spread are:

  1. Different expiration dates: The bought put has a longer expiration date than the sold put.
  2. Different strike prices: The bought put has a lower strike price than the sold put.
  3. Income generation: The premium collected from the sold put is used to offset the cost of the bought put, generating potential income.
  4. Downside protection: The long-term bought put provides protection against significant price drops in the underlying stock.

By repeatedly selling short-term puts while holding the long-term protective put, the investor is essentially employing a series of diagonal put calendar spreads to generate weekly income and maintain downside protection.

Hypothetical Example

Suppose XYZ stock is currently trading at $100 per share. An investor could implement the strategy as follows:

  • Buy a put option with a strike price of $95 and an expiration date 60 days in the future for a premium of $2 per share.
  • Sell a put option with a strike price of $100 and an expiration date 7 days in the future for a premium of $1 per share.

In this example, the investor would collect a net credit of $1 per share ($1 premium from selling the put minus $2 paid for the long-term put). If the stock price remains above $100 at expiration of the short-term put, the investor keeps the premium and can repeat the process by selling another weekly put option.

Characteristics for Success

To successfully implement this strategy, consider the following characteristics of the options and underlying stock:

  1. Implied Volatility: Look for stocks with higher implied volatility, as this can lead to higher premiums for the sold put options.
  2. Liquidity: Choose stocks and options with high liquidity to ensure easy entry and exit of positions.
  3. Earnings Releases: Consider the timing of earnings releases when selecting the expiration date for the long-term protective put.
  4. Strike Prices: Select strike prices that balance the potential for income generation with the desired level of downside protection.

Repeating the Strategy

By selling weekly put options, investors can generate a consistent income stream. Each week, a new put option is sold, and the premium is collected. If the stock price remains above the strike price of the sold put at expiration, the option expires worthless, and the investor keeps the premium. This process can be repeated weekly, providing a regular income.

Risk Management and Possibility of Assignment

The long-term protective put serves as a hedge against significant downside risk. If the stock price drops below the strike price of the protective put, the investor has the right to sell the shares at the higher strike price, limiting their potential losses. However, it is important to note that this strategy caps the potential upside, as the short-term sold puts will be assigned if the stock price rises significantly.

When selling put options, there is always a risk of being assigned shares if the stock price drops below the strike price of the sold put at expiration. If this occurs, the investor is obligated to purchase the shares at the agreed-upon strike price, regardless of the current market price.

Assignment Scenarios

  1. Stock Price Slightly Below Strike Price: If the stock price is only slightly below the strike price at expiration, the investor may choose to accept the assignment and purchase the shares. They can then hold the shares and sell covered call options to generate additional income or wait for the stock price to recover before selling the shares.

  2. Stock Price Significantly Below Strike Price: If the stock price has dropped significantly below the strike price, the investor may face substantial losses upon assignment. In this case, the investor has a few options:

a. Accept the assignment and sell the shares immediately to limit further losses. b. Hold the shares and sell covered call options to generate income while waiting for the stock price to recover. c. If the investor believes the stock price will continue to decline, they may choose to sell the shares and close the position to avoid additional losses.

Managing Assignment Risk

To manage the risk of assignment, investors can implement the following strategies:

  1. Protective Put: By purchasing a long-term protective put, the investor has the right to sell the shares at the strike price of the protective put, limiting their potential losses if assigned.

  2. Rolling the Put: If the stock price is approaching the strike price of the sold put near expiration, the investor can "roll" the put by buying back the sold put and simultaneously selling a new put with a later expiration date and/or a lower strike price. This allows the investor to maintain the income stream and potentially avoid assignment.

  3. Diversification: Diversifying across multiple stocks and sectors can help mitigate the impact of assignment on any single position.

  4. Position Sizing: Properly sizing positions based on the investor's risk tolerance and portfolio size can help manage the overall impact of assignment.

Tax Implications

When assigned shares through a put option, the investor's cost basis in the stock is equal to the strike price of the put. Any subsequent sale of the shares will result in a capital gain or loss, depending on the sale price relative to the cost basis. It is essential to consider the tax implications of assignment and consult with a tax professional for guidance.

Risks and Limitations

While the protective put strategy can generate income and provide downside protection, there are risks and limitations to consider:

  1. Limited Upside: The short-term sold puts limit the potential profit if the stock price rises significantly.
  2. Assignment Risk: If the stock price drops below the strike price of the sold put, the investor may be assigned and required to purchase the shares at the agreed-upon price.
  3. Time Decay: The value of the long-term protective put will decrease over time due to time decay, which can erode the overall profitability of the strategy.

Conclusion

The protective put strategy, involving buying a long-term put and selling short-term puts, can be an effective way to generate weekly income while providing downside protection. This strategy can also be classified as a diagonal put calendar spread, as it involves buying and selling put options with different expiration dates and strike prices on the same underlying stock. By carefully selecting the underlying stock, strike prices, and expiration dates, investors can potentially benefit from this options strategy. However, it is crucial to understand and manage the associated risks, including limited upside potential and assignment risk. If assigned, investors must evaluate their options based on the specific circumstances and their market outlook. Understanding and effectively managing assignment risk is crucial for successfully implementing the protective put strategy for generating weekly income.

Monte Carlo Method of Pricing Options

Pricing American Options with the Monte Carlo Method

Introduction to Monte Carlo Simulation

Monte Carlo simulation is a powerful computational technique that relies on repeated random sampling to solve complex problems. It is widely used in various fields, including finance, physics, and engineering. The core idea behind Monte Carlo simulation is to generate a large number of random scenarios and then analyze the results to gain insights into the problem at hand.

To illustrate the concept, let's consider a simplified example of estimating the value of π (pi) using Monte Carlo simulation. Imagine a square with sides of length 1 and a circle inscribed within the square. The circle's radius is 0.5, and its area is π/4. By randomly generating points within the square and counting the number of points that fall inside the circle, we can estimate the value of π.

Here's a step-by-step breakdown of the process:

  1. Generate a large number of random points (x, y) within the square, where both x and y are between 0 and 1.
  2. For each point, calculate the distance from the point to the center of the square (0.5, 0.5) using the formula:
distance=(x-0.5)2+(y-0.5)2
  1. If the distance is less than or equal to 0.5 (the radius of the circle), count the point as inside the circle.
  2. After generating all the points, calculate the ratio of points inside the circle to the total number of points.
  3. Multiply the ratio by 4 to estimate the value of π.

As the number of generated points increases, the estimated value of π will converge to its true value. This example demonstrates how Monte Carlo simulation can be used to solve problems by generating random scenarios and analyzing the results.

Pricing American Options with Monte Carlo Simulation

American options are financial contracts that give the holder the right, but not the obligation, to buy (call option) or sell (put option) an underlying asset at a predetermined price (strike price) on or before the expiration date. Unlike European options, which can only be exercised at expiration, American options can be exercised at any time prior to expiration. This early exercise feature makes pricing American options more challenging than pricing European options.

Monte Carlo simulation can be used to price American options by simulating the price paths of the underlying asset and determining the optimal exercise strategy at each time step. Here's a high-level overview of the process:

  1. Generate a large number of price paths for the underlying asset using a suitable stochastic process, such as Geometric Brownian Motion (GBM). Each price path represents a possible future scenario. The price path can be simulated using the following formula:
St=S0exp((r-σ22)t+σtZ)

where: - St is the stock price at time t - S0 is the initial stock price - r is the risk-free interest rate - σ is the volatility of the stock - t is the time step - Z is a standard normal random variable

  1. At each time step along each price path, compare the intrinsic value of the option (the payoff from immediate exercise) with the continuation value (the expected value of holding the option).
  2. If the intrinsic value is greater than the continuation value, the option should be exercised, and the payoff is recorded.
  3. If the continuation value is greater than the intrinsic value, the option should be held, and the simulation continues to the next time step.
  4. At expiration, the option is exercised if it is in-the-money (i.e., the stock price is above the strike price for a call option or below the strike price for a put option).
  5. The average of the discounted payoffs across all price paths provides an estimate of the option price.

To determine the continuation value at each time step, a common approach is to use the Least Squares Monte Carlo (LSM) method, which involves regressing the discounted future payoffs on a set of basis functions of the current state variables (e.g., stock price and time to expiration).

One of the advantages of using Monte Carlo simulation for pricing American options is its flexibility in handling complex payoff structures and multiple underlying assets. However, it can be computationally intensive, especially for high-dimensional problems or when a large number of price paths is required for accurate results.

In practice, implementing the Monte Carlo method for pricing American options involves several technical considerations, such as choosing an appropriate stochastic process for the underlying asset, selecting suitable basis functions for the LSM algorithm, and applying variance reduction techniques to improve the efficiency of the simulation.

Despite its challenges, Monte Carlo simulation remains a valuable tool for pricing American options and has been widely adopted in the financial industry. Its ability to handle complex scenarios and provide insights into the option's behavior under various market conditions makes it an indispensable technique in the field of quantitative finance.

Implementing the Monte Carlo Method for American Option Pricing

To implement the Monte Carlo method for pricing American options, we need to follow a step-by-step approach. Here's a more detailed breakdown of the process:

  1. Define the parameters of the American option, such as the strike price (K), expiration time (T), risk-free interest rate (r), and volatility (σ).

  2. Generate a large number of price paths for the underlying asset using the Geometric Brownian Motion (GBM) model. Each price path consists of discrete time steps from the current time to the expiration time. The stock price at each time step can be calculated using the following formula:

St+Δt=Stexp((r-σ22)Δt+σΔtZt)

where: - St+Δt is the stock price at time t + Δt - St is the stock price at time t - Δt is the size of the time step - Zt is a standard normal random variable at time t

  1. At each time step along each price path, calculate the intrinsic value of the option. For a call option, the intrinsic value is given by:
max(St-K,0)

For a put option, the intrinsic value is given by:

max(K-St,0)
  1. Apply the Least Squares Monte Carlo (LSM) method to estimate the continuation value at each time step. This involves the following substeps:
  2. Select a set of basis functions that depend on the current stock price and time to expiration. Common choices include polynomials, Laguerre polynomials, or exponential functions.
  3. For each in-the-money price path at the current time step, calculate the discounted future payoffs from continuing to hold the option.
  4. Regress the discounted future payoffs on the basis functions to obtain an estimate of the continuation value as a function of the current stock price.

  5. Compare the intrinsic value with the estimated continuation value at each time step. If the intrinsic value is greater, the option should be exercised, and the payoff is recorded. If the continuation value is greater, the option should be held, and the simulation continues to the next time step.

  6. At expiration, the option is exercised if it is in-the-money, and the payoff is recorded.

  7. Discount the payoffs at each exercise time back to the present using the risk-free interest rate. The average of the discounted payoffs across all price paths provides an estimate of the American option price.

The LSM method used in step 4 is a critical component of the Monte Carlo approach for pricing American options. It allows us to estimate the continuation value at each time step without the need for a recursive tree-based method. The choice of basis functions and the number of basis functions used in the regression can impact the accuracy of the estimated continuation value and, consequently, the option price.

Once the Monte Carlo simulation is complete, we can analyze the results to gain insights into the behavior of the American option under various market conditions. This can include examining the distribution of payoffs, the probability of early exercise, and the sensitivity of the option price to changes in the underlying parameters (known as the "Greeks").

It's important to note that the accuracy of the Monte Carlo method for pricing American options depends on several factors, such as the number of price paths simulated, the size of the time steps, the choice of basis functions in the LSM method, and the presence of any variance reduction techniques. Increasing the number of price paths and reducing the size of the time steps can improve the accuracy of the results but also increase the computational burden.

In practice, implementing the Monte Carlo method for American option pricing requires careful consideration of these factors and may involve trade-offs between accuracy and computational efficiency. Nonetheless, the Monte Carlo approach remains a powerful and flexible tool for tackling the complex problem of pricing American options, particularly in cases where analytical solutions are not available or when dealing with multi-dimensional or path-dependent options.

Code

Monte Carlo Options Pricing in Rust

Jupyter Notebook with usage example

use rand::Rng;
use std::f64::consts::E;
use pyo3::prelude::*;

/// Calculates the price of an American call option using the Monte Carlo simulation method.
///
/// # Arguments
///
/// * `spot_price` - The current price of the underlying asset.
/// * `strike_price` - The strike price of the option.
/// * `risk_free_rate` - The risk-free interest rate.
/// * `volatility` - The volatility of the underlying asset.
/// * `time_to_maturity` - The time to maturity of the option (in years).
/// * `num_simulations` - The number of simulations to run.
/// * `num_steps` - The number of steps in each simulation.
///
/// # Returns
///
/// The price of the American call option.
///
/// # Example
///
/// ```rust
/// use monte_carlo_options_rs::monte_carlo_american_call;
///
/// let spot_price = 100.0;
/// let strike_price = 110.0;
/// let risk_free_rate = 0.05;
/// let volatility = 0.2;
/// let time_to_maturity = 1.0;
/// let num_simulations = 100_000;
/// let num_steps = 252;
///
/// let price = monte_carlo_american_call(
///     spot_price,
///     strike_price,
///     risk_free_rate,
///     volatility,
///     time_to_maturity,
///     num_simulations,
///     num_steps,
/// ).unwrap();
///
/// println!("American call option price: {:.2}", price);
/// ```
#[pyfunction]
fn monte_carlo_american_call(
    spot_price: f64,
    strike_price: f64,
    risk_free_rate: f64,
    volatility: f64,
    time_to_maturity: f64,
    num_simulations: usize,
    num_steps: usize,
) -> PyResult<f64> {
    let dt = time_to_maturity / num_steps as f64;
    let discount_factor = E.powf(-risk_free_rate * time_to_maturity);

    let mut rng = rand::thread_rng();
    let mut payoffs = Vec::with_capacity(num_simulations);

    for _ in 0..num_simulations {
        let mut price = spot_price;
        let mut max_payoff: f64 = 0.0;

        for _ in 0..num_steps {
            let rand_num = rng.gen_range(-1.0..1.0);
            price *= E.powf(
                (risk_free_rate - 0.5 * volatility.powi(2)) * dt
                    + volatility * rand_num * dt.sqrt(),
            );
            max_payoff = max_payoff.max((price - strike_price).max(0.0));
        }

        payoffs.push(max_payoff);
    }

    let sum_payoffs: f64 = payoffs.iter().sum();
    let avg_payoff = sum_payoffs / num_simulations as f64;

    Ok(avg_payoff * discount_factor)
}

/// Calculates the price of an American put option using the Monte Carlo simulation method.
///
/// # Arguments
///
/// * `spot_price` - The current price of the underlying asset.
/// * `strike_price` - The strike price of the option.
/// * `risk_free_rate` - The risk-free interest rate.
/// * `volatility` - The volatility of the underlying asset.
/// * `time_to_maturity` - The time to maturity of the option (in years).
/// * `num_simulations` - The number of simulations to run.
/// * `num_steps` - The number of steps in each simulation.
///
/// # Returns
///
/// The price of the American put option.
///
/// # Example
///
/// ```rust
/// use monte_carlo_options_rs::monte_carlo_american_put;
///
/// let spot_price = 100.0;
/// let strike_price = 90.0;
/// let risk_free_rate = 0.05;
/// let volatility = 0.2;
/// let time_to_maturity = 1.0;
/// let num_simulations = 100_000;
/// let num_steps = 252;
///
/// let price = monte_carlo_american_put(
///     spot_price,
///     strike_price,
///     risk_free_rate,
///     volatility,
///     time_to_maturity,
///     num_simulations,
///     num_steps,
/// ).unwrap();
///
/// println!("American put option price: {:.2}", price);
/// ```
#[pyfunction]
fn monte_carlo_american_put(
    spot_price: f64,
    strike_price: f64,
    risk_free_rate: f64,
    volatility: f64,
    time_to_maturity: f64,
    num_simulations: usize,
    num_steps: usize,
) -> PyResult<f64> {
    let dt = time_to_maturity / num_steps as f64;
    let discount_factor = E.powf(-risk_free_rate * time_to_maturity);

    let mut rng = rand::thread_rng();
    let mut payoffs = Vec::with_capacity(num_simulations);

    for _ in 0..num_simulations {
        let mut price = spot_price;
        let mut max_payoff: f64 = 0.0;

        for _ in 0..num_steps {
            let rand_num = rng.gen_range(-1.0..1.0);
            price *= E.powf(
                (risk_free_rate - 0.5 * volatility.powi(2)) * dt
                    + volatility * rand_num * dt.sqrt(),
            );
            max_payoff = max_payoff.max((strike_price - price).max(0.0));
        }

        payoffs.push(max_payoff);
    }

    let sum_payoffs: f64 = payoffs.iter().sum();
    let avg_payoff = sum_payoffs / num_simulations as f64;

    Ok(avg_payoff * discount_factor)
}

/// Calculates the delta of an American call option using the finite difference method.
///
/// Delta represents the rate of change of the option price with respect to the change in the underlying asset price.
///
/// # Arguments
///
/// * `spot_price` - The current price of the underlying asset.
/// * `strike_price` - The strike price of the option.
/// * `risk_free_rate` - The risk-free interest rate.
/// * `volatility` - The volatility of the underlying asset.
/// * `time_to_maturity` - The time to maturity of the option (in years).
/// * `num_simulations` - The number of simulations to run for the Monte Carlo method.
/// * `num_steps` - The number of steps in each simulation for the Monte Carlo method.
///
/// # Returns
///
/// The delta of the American call option.
///
/// # Example
///
/// ```rust
/// use monte_carlo_options_rs::{calculate_delta, monte_carlo_american_call};
///
/// let spot_price = 100.0;
/// let strike_price = 110.0;
/// let risk_free_rate = 0.05;
/// let volatility = 0.2;
/// let time_to_maturity = 1.0;
/// let num_simulations = 100_000;
/// let num_steps = 252;
///
/// let delta = calculate_delta(
///     spot_price,
///     strike_price,
///     risk_free_rate,
///     volatility,
///     time_to_maturity,
///     num_simulations,
///     num_steps,
/// ).unwrap();
///
/// println!("Delta of the American call option: {:.4}", delta);
/// ```
#[pyfunction]
fn calculate_delta(
    spot_price: f64,
    strike_price: f64,
    risk_free_rate: f64,
    volatility: f64,
    time_to_maturity: f64,
    num_simulations: usize,
    num_steps: usize,
) -> PyResult<f64> {
    let h = 0.01;
    let price_up = monte_carlo_american_call(
        spot_price * (1.0 + h),
        strike_price,
        risk_free_rate,
        volatility,
        time_to_maturity,
        num_simulations,
        num_steps,
    )?;
    let price_down = monte_carlo_american_call(
        spot_price * (1.0 - h),
        strike_price,
        risk_free_rate,
        volatility,
        time_to_maturity,
        num_simulations,
        num_steps,
    )?;
    Ok((price_up - price_down) / (2.0 * h * spot_price))
}

/// Calculates the gamma of an American call option using the finite difference method.
///
/// Gamma represents the rate of change of the option's delta with respect to the change in the underlying asset price.
///
/// # Arguments
///
/// * `spot_price` - The current price of the underlying asset.
/// * `strike_price` - The strike price of the option.
/// * `risk_free_rate` - The risk-free interest rate.
/// * `volatility` - The volatility of the underlying asset.
/// * `time_to_maturity` - The time to maturity of the option (in years).
/// * `num_simulations` - The number of simulations to run for the Monte Carlo method.
/// * `num_steps` - The number of steps in each simulation for the Monte Carlo method.
///
/// # Returns
///
/// The gamma of the American call option.
///
/// # Example
///
/// ```rust
/// use monte_carlo_options_rs::{calculate_gamma, monte_carlo_american_call};
///
/// let spot_price = 100.0;
/// let strike_price = 110.0;
/// let risk_free_rate = 0.05;
/// let volatility = 0.2;
/// let time_to_maturity = 1.0;
/// let num_simulations = 100_000;
/// let num_steps = 252;
///
/// let gamma = calculate_gamma(
///     spot_price,
///     strike_price,
///     risk_free_rate,
///     volatility,
///     time_to_maturity,
///     num_simulations,
///     num_steps,
/// ).unwrap();
///
/// println!("Gamma of the American call option: {:.4}", gamma);
/// ```
#[pyfunction]
fn calculate_gamma(
    spot_price: f64,
    strike_price: f64,
    risk_free_rate: f64,
    volatility: f64,
    time_to_maturity: f64,
    num_simulations: usize,
    num_steps: usize,
) -> PyResult<f64> {
    let h = 0.01;
    let price_up = monte_carlo_american_call(
        spot_price * (1.0 + h),
        strike_price,
        risk_free_rate,
        volatility,
        time_to_maturity,
        num_simulations,
        num_steps,
    )?;
    let price = monte_carlo_american_call(
        spot_price,
        strike_price,
        risk_free_rate,
        volatility,
        time_to_maturity,
        num_simulations,
        num_steps,
    )?;
    let price_down = monte_carlo_american_call(
        spot_price * (1.0 - h),
        strike_price,
        risk_free_rate,
        volatility,
        time_to_maturity,
        num_simulations,
        num_steps,
    )?;
    Ok((price_up - 2.0 * price + price_down) / (h * spot_price).powi(2))
}

/// Calculates the vega of an American call option using the finite difference method.
///
/// Vega represents the sensitivity of the option price to changes in the volatility of the underlying asset.
///
/// # Arguments
///
/// * `spot_price` - The current price of the underlying asset.
/// * `strike_price` - The strike price of the option.
/// * `risk_free_rate` - The risk-free interest rate.
/// * `volatility` - The volatility of the underlying asset.
/// * `time_to_maturity` - The time to maturity of the option (in years).
/// * `num_simulations` - The number of simulations to run for the Monte Carlo method.
/// * `num_steps` - The number of steps in each simulation for the Monte Carlo method.
///
/// # Returns
///
/// The vega of the American call option.
///
/// # Example
///
/// ```rust
/// use monte_carlo_options_rs::{calculate_vega, monte_carlo_american_call};
///
/// let spot_price = 100.0;
/// let strike_price = 110.0;
/// let risk_free_rate = 0.05;
/// let volatility = 0.2;
/// let time_to_maturity = 1.0;
/// let num_simulations = 100_000;
/// let num_steps = 252;
///
/// let vega = calculate_vega(
///     spot_price,
///     strike_price,
///     risk_free_rate,
///     volatility,
///     time_to_maturity,
///     num_simulations,
///     num_steps,
/// ).unwrap();
///
/// println!("Vega of the American call option: {:.4}", vega);
/// ```
#[pyfunction]
fn calculate_vega(
    spot_price: f64,
    strike_price: f64,
    risk_free_rate: f64,
    volatility: f64,
    time_to_maturity: f64,
    num_simulations: usize,
    num_steps: usize,
) -> PyResult<f64> {
    let h = 0.01;
    let price_up = monte_carlo_american_call(
        spot_price,
        strike_price,
        risk_free_rate,
        volatility * (1.0 + h),
        time_to_maturity,
        num_simulations,
        num_steps,
    )?;
    let price_down = monte_carlo_american_call(
        spot_price,
        strike_price,
        risk_free_rate,
        volatility * (1.0 - h),
        time_to_maturity,
        num_simulations,
        num_steps,
    )?;
    Ok((price_up - price_down) / (2.0 * h * volatility))
}

/// Calculates the theta of an American call option using the finite difference method.
///
/// Theta represents the rate of change of the option price with respect to the passage of time.
///
/// # Arguments
///
/// * `spot_price` - The current price of the underlying asset.
/// * `strike_price` - The strike price of the option.
/// * `risk_free_rate` - The risk-free interest rate.
/// * `volatility` - The volatility of the underlying asset.
/// * `time_to_maturity` - The time to maturity of the option (in years).
/// * `num_simulations` - The number of simulations to run for the Monte Carlo method.
/// * `num_steps` - The number of steps in each simulation for the Monte Carlo method.
///
/// # Returns
///
/// The theta of the American call option (per day).
///
/// # Example
///
/// ```rust
/// use monte_carlo_options_rs::{calculate_theta, monte_carlo_american_call};
///
/// let spot_price = 100.0;
/// let strike_price = 110.0;
/// let risk_free_rate = 0.05;
/// let volatility = 0.2;
/// let time_to_maturity = 1.0;
/// let num_simulations = 100_000;
/// let num_steps = 252;
///
/// let theta = calculate_theta(
///     spot_price,
///     strike_price,
///     risk_free_rate,
///     volatility,
///     time_to_maturity,
///     num_simulations,
///     num_steps,
/// ).unwrap();
///
/// println!("Theta of the American call option (per day): {:.4}", theta);
/// ```
#[pyfunction]
fn calculate_theta(
    spot_price: f64,
    strike_price: f64,
    risk_free_rate: f64,
    volatility: f64,
    time_to_maturity: f64,
    num_simulations: usize,
    num_steps: usize,
) -> PyResult<f64> {
    let h = 0.01;
    let price_up = monte_carlo_american_call(
        spot_price,
        strike_price,
        risk_free_rate,
        volatility,
        time_to_maturity * (1.0 + h),
        num_simulations,
        num_steps,
    )?;
    let price_down = monte_carlo_american_call(
        spot_price,
        strike_price,
        risk_free_rate,
        volatility,
        time_to_maturity * (1.0 - h),
        num_simulations,
        num_steps,
    )?;
    Ok(-(price_up - price_down) / (2.0 * h * time_to_maturity) / 365.0)
}

/// Calculates the rho of an American call option using the finite difference method.
///
/// Rho represents the sensitivity of the option price to changes in the risk-free interest rate.
///
/// # Arguments
///
/// * `spot_price` - The current price of the underlying asset.
/// * `strike_price` - The strike price of the option.
/// * `risk_free_rate` - The risk-free interest rate.
/// * `volatility` - The volatility of the underlying asset.
/// * `time_to_maturity` - The time to maturity of the option (in years).
/// * `num_simulations` - The number of simulations to run for the Monte Carlo method.
/// * `num_steps` - The number of steps in each simulation for the Monte Carlo method.
///
/// # Returns
///
/// The rho of the American call option.
///
/// # Example
///
/// ```rust
/// use monte_carlo_options_rs::{calculate_rho, monte_carlo_american_call};
///
/// let spot_price = 100.0;
/// let strike_price = 110.0;
/// let risk_free_rate = 0.05;
/// let volatility = 0.2;
/// let time_to_maturity = 1.0;
/// let num_simulations = 100_000;
/// let num_steps = 252;
///
/// let rho = calculate_rho(
///     spot_price,
///     strike_price,
///     risk_free_rate,
///     volatility,
///     time_to_maturity,
///     num_simulations,
///     num_steps,
/// ).unwrap();
///
/// println!("Rho of the American call option: {:.4}", rho);
/// ```
#[pyfunction]
fn calculate_rho(
    spot_price: f64,
    strike_price: f64,
    risk_free_rate: f64,
    volatility: f64,
    time_to_maturity: f64,
    num_simulations: usize,
    num_steps: usize,
) -> PyResult<f64> {
    let h = 0.01;
    let price_up = monte_carlo_american_call(
        spot_price,
        strike_price,
        risk_free_rate * (1.0 + h),
        volatility,
        time_to_maturity,
        num_simulations,
        num_steps,
    )?;
    let price_down = monte_carlo_american_call(
        spot_price,
        strike_price,
        risk_free_rate * (1.0 - h),
        volatility,
        time_to_maturity,
        num_simulations,
        num_steps,
    )?;
    Ok((price_up - price_down) / (2.0 * h))
}

/// Initializes the `libmonte_carlo_options_rs` Python module.
///
/// This function is called when the Python module is imported and adds the Rust functions
/// to the module so they can be accessed from Python.
///
/// # Arguments
///
/// * `_py` - The Python interpreter.
/// * `m` - The Python module to add the functions to.
///
/// # Returns
///
/// `Ok(())` if the module was initialized successfully, or an error if there was a problem.
///
/// # Example
///
/// ```python
/// import libmonte_carlo_options_rs
///
/// # Use the functions from the module
/// price = libmonte_carlo_options_rs.monte_carlo_american_call(...)
/// delta = libmonte_carlo_options_rs.calculate_delta(...)
/// gamma = libmonte_carlo_options_rs.calculate_gamma(...)
/// vega = libmonte_carlo_options_rs.calculate_vega(...)
/// theta = libmonte_carlo_options_rs.calculate_theta(...)
/// rho = libmonte_carlo_options_rs.calculate_rho(...)
/// ```
#[pymodule]
fn libmonte_carlo_options_rs(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(monte_carlo_american_call, m)?)?;
    m.add_function(wrap_pyfunction!(monte_carlo_american_put, m)?)?;
    m.add_function(wrap_pyfunction!(calculate_delta, m)?)?;
    m.add_function(wrap_pyfunction!(calculate_gamma, m)?)?;
    m.add_function(wrap_pyfunction!(calculate_vega, m)?)?;
    m.add_function(wrap_pyfunction!(calculate_theta, m)?)?;
    m.add_function(wrap_pyfunction!(calculate_rho, m)?)?;
    Ok(())
}

Pricing Options: Finite Differences with Crank-Nicolson

Discover how the Crank-Nicolson and finite differences methods revolutionized the world of option pricing. In this blog post, we dive deep into these powerful numerical techniques that have become essential tools for quantitative analysts and financial engineers.

We start by exploring the fundamentals of option pricing and the challenges posed by the Black-Scholes-Merton partial differential equation (PDE). Then, we introduce the finite differences method, a numerical approach that discretizes the PDE into a grid of points, enabling us to approximate option prices and calculate crucial risk measures known as the Greeks.

Next, we focus on the Crank-Nicolson method, a more advanced finite difference scheme that combines the best of both explicit and implicit methods. We explain how this method provides second-order accuracy in time and space, ensuring faster convergence and more accurate results.

Through step-by-step explanations and illustrative examples, we demonstrate how these methods are implemented in practice, including the treatment of boundary conditions and the incorporation of the early exercise feature for American options.

Whether you're a curious investor, a financial enthusiast, or an aspiring quantitative analyst, this blog post will provide you with valuable insights into the world of option pricing. Join us as we unravel the secrets behind these powerful numerical methods and discover how they have transformed the way we value and manage options in modern finance.

The Black-Scholes-Merton (BSM) partial differential equation

The Black-Scholes-Merton (BSM) partial differential equation is a mathematical model used for pricing options and other financial derivatives. It was developed by Fischer Black, Myron Scholes, and Robert Merton in the early 1970s and has become a cornerstone of modern financial theory.

Fundamentals of Options Pricing: Options are financial contracts that give the holder the right, but not the obligation, to buy (call option) or sell (put option) an underlying asset at a predetermined price (strike price) on or before a specific date (expiration date). The price of an option depends on several factors, including the current price of the underlying asset, the strike price, the time to expiration, the risk-free interest rate, and the volatility of the underlying asset.

The BSM equation is based on the following assumptions: 1. The market is efficient, and there are no arbitrage opportunities. 2. The underlying asset follows a geometric Brownian motion with constant drift and volatility. 3. The risk-free interest rate is constant and known. 4. There are no transaction costs or taxes. 5. The underlying asset pays no dividends.

Black-Scholes-Merton Partial Differential Equation: The BSM equation describes the price of a European option as a function of time and the underlying asset price. It is a second-order partial differential equation:

Vt+12σ2S22VS2+rSVS-rV=0

Where:

  • - V is the option price
  • - S is the underlying asset price
  • - t is time
  • - r is the risk-free interest rate
  • - σ is the volatility of the underlying asset

Challenges Posed by the BSM Equation:

  1. Volatility Estimation: The BSM equation assumes constant volatility, which is not always the case in real markets. Estimating volatility accurately is crucial for option pricing.

  2. Model Assumptions: The assumptions underlying the BSM model, such as no transaction costs and continuous trading, do not hold in practice. This can lead to discrepancies between theoretical and market prices.

  3. Numerical Methods: Solving the BSM equation often requires numerical methods, such as finite difference or Monte Carlo simulations, which can be computationally intensive.

  4. Market Anomalies: The BSM model does not account for market anomalies, such as the volatility smile or skew, which suggest that implied volatility varies with strike price and time to expiration.

  5. Model Extensions: The basic BSM model has been extended to account for dividends, early exercise (for American options), and other factors. These extensions add complexity to the model and its implementation.

Despite these challenges, the Black-Scholes-Merton model remains a foundational tool in options pricing and financial risk management. It provides a framework for understanding the factors that influence option prices and serves as a benchmark for more advanced models.

The Finite Difference Method

The finite difference method is a numerical technique used to solve partial differential equations (PDEs) by approximating derivatives with finite differences. It is particularly useful when analytical solutions are difficult or impossible to obtain. The method discretizes the continuous PDE problem into a discrete set of points in space and time, called a grid or mesh.

To illustrate the basics of the finite difference method, consider a simple one-dimensional heat equation:

ut=α2ux2

Where:

  • - u is the temperature
  • - t is time
  • - x is the spatial coordinate
  • - α is the thermal diffusivity

To apply the finite difference method, we discretize the space and time domains into a grid with uniform step sizes Δx and Δt, respectively. We denote the temperature at grid point i and time step n as u(i, n) = u(i Δx, n Δt).

The finite difference method approximates the derivatives using Taylor series expansions. For example, the first-order time derivative can be approximated using the forward difference:

utu(i,n+1)-u(i,n)Δt

Similarly, the second-order spatial derivative can be approximated using the central difference:

2ux2u(i+1,n)-2u(i,n)+u(i-1,n)Δx2

By substituting these approximations into the original PDE, we obtain a system of algebraic equations that can be solved numerically. The finite difference method provides an approximation of the solution at each grid point and time step.

There are various schemes for discretizing time, such as the explicit (forward Euler), implicit (backward Euler), and the Crank-Nicolson methods. Each scheme has its own stability, accuracy, and computational requirements.

The finite difference method is widely used in computational finance, particularly in the numerical solution of the Black-Scholes-Merton PDE for option pricing. It is also applied in other fields, such as heat transfer, fluid dynamics, and electromagnetism.

Let's walk through a simple example using the one-dimensional heat equation mentioned earlier. We'll solve the equation numerically using the explicit (forward Euler) finite difference method.

Consider a one-dimensional rod of length L = 1 m, with an initial temperature distribution u(x, 0) = sin(πx) and fixed boundary conditions u(0, t) = u(L, t) = 0. The thermal diffusivity is α = 0.1 m²/s. We'll solve for the temperature distribution over a time period of T = 0.5 s.

Step 1: Discretize the space and time domains Let's divide the rod into N = 10 equally spaced points, with Δx = L / (N - 1) = 0.1 m. We'll use a time step of Δt = 0.01 s, resulting in M = T / Δt = 50 time steps.

Step 2: Set up the initial condition At t = 0, the temperature at each grid point is: u(i, 0) = sin(πx_i), where x_i = i Δx, for i = 0, 1, ..., N-1.

For example:

u(0, 0) = sin(π × 0) = 0
u(1, 0) = sin(π × 0.1) ≈ 0.309
...
u(9, 0) = sin(π × 0.9) ≈ 0.309
u(10, 0) = sin(π × 1) = 0

Step 3: Apply the explicit finite difference scheme For each time step n = 0, 1, ..., M-1 and each interior grid point i = 1, 2, ..., N-2, update the temperature using the explicit scheme:

u(i, n+1) = u(i, n) + α Δt / (Δx)² × [u(i+1, n) - 2u(i, n) + u(i-1, n)]

For example, at the first time step (n = 0) and the first interior grid point (i = 1):

u(1, 1) = u(1, 0) + 0.1 × 0.01 / (0.1)² × [u(2, 0) - 2u(1, 0) + u(0, 0)]
        ≈ 0.309 + 0.1 × [0.588 - 2 × 0.309 + 0] ≈ 0.306

Repeat this process for all interior grid points and time steps, while keeping the boundary conditions fixed at 0.

Step 4: Analyze the results After completing all time steps, you will have approximated the temperature distribution u(x, t) at each grid point and time step. You can visualize the results by plotting the temperature profile at different times or creating an animation showing the evolution of the temperature over time.

Note that the explicit scheme used in this example has a stability condition: α Δt / (Δx)² ≤ 0.5. If this condition is not met, the numerical solution may become unstable and diverge from the actual solution. In such cases, an implicit scheme or a smaller time step may be necessary.

The Crank-Nicolson Method

Let's dive into the Crank-Nicolson method and see how it can be applied to the Black-Scholes-Merton partial differential equation for options pricing.

The Black-Scholes-Merton PDE for a European call option is:

Vt+12σ2S22VS2+rSVS-rV=0

Where:

  • - V is the option price
  • - S is the underlying asset price
  • - t is time
  • - r is the risk-free interest rate
  • - σ is the volatility of the underlying asset

The Crank-Nicolson method discretizes the PDE using a weighted average of the explicit and implicit schemes, with equal weights of 0.5. This approach results in second-order accuracy in both time and space.

Let's denote the option price at grid point i and time step n as V(i, n) = V(i ΔS, n Δt), where ΔS and Δt are the step sizes in the asset price and time dimensions, respectively.

The Crank-Nicolson scheme for the Black-Scholes-Merton PDE can be written as:

V(i,n+1)-V(i,n)Δt=12[αVi-1(n)-2Vi(n)+αVi+1(n)+αVi-1(n+1)-2Vi(n+1)+αVi+1(n+1)]

Where:

  • - α = 1/2 σ² S_i² Δt / (ΔS)²
  • - β = r S_i Δt / (2 ΔS)
  • - γ = r Δt / 2

This scheme leads to a tridiagonal system of linear equations that can be solved efficiently using the Thomas algorithm or other methods for tridiagonal matrices.

To illustrate the Crank-Nicolson method with an example, consider a European call option with the following parameters:

  • - S = 100 (current stock price)
  • - K = 100 (strike price)
  • - T = 1 (time to maturity in years)
  • - r = 0.05 (risk-free interest rate)
  • - σ = 0.2 (volatility)

We can discretize the asset price dimension into N = 200 points, with S_min = 0 and S_max = 200, and the time dimension into M = 100 steps.

At each time step, we construct the tridiagonal matrix and solve the resulting system of equations to obtain the option prices at the next time step. The boundary conditions are:

  • - V(0, n) = 0 (option value is zero when the stock price is zero)
  • - V(N, n) = S_max - K exp(-r(T - n Δt)) (option value at the maximum stock price)

After the final time step, the option price at the initial stock price S is the desired result.

The Crank-Nicolson method provides a stable and accurate solution to the Black-Scholes-Merton PDE. Its second-order accuracy in both time and space ensures faster convergence and more precise results compared to the explicit or implicit methods alone. However, it requires solving a system of equations at each time step, which can be computationally more expensive than the explicit method.

In practice, the Crank-Nicolson method is widely used in financial engineering for options pricing and other applications involving parabolic PDEs. It strikes a balance between accuracy and computational efficiency, making it a popular choice among practitioners.

Crank-Nicolson in Rust as a Python library

Complete Repo on Github

Jupyter Notebook for using the python library

extern crate nalgebra as na;
use na::{DMatrix};
use numpy::{PyArray2, PyArray};
use numpy::PyArrayMethods;
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
use pyo3::exceptions::PyValueError;

/// Constructs a tridiagonal matrix with specified diagonals.
///
/// # Arguments
///
/// * `n` - The size of the matrix.
/// * `a` - The value of the subdiagonal.
/// * `b` - The value of the main diagonal.
/// * `c` - The value of the superdiagonal.
///
/// # Returns
///
/// A tridiagonal matrix with the specified diagonals.
fn tridiag(n: usize, a: f64, b: f64, c: f64) -> DMatrix<f64> {
    let mut mat = DMatrix::<f64>::zeros(n, n);
    for i in 0..n {
        if i > 0 {
            mat[(i, i - 1)] = a;
        }
        mat[(i, i)] = b;
        if i < n - 1 {
            mat[(i, i + 1)] = c;
        }
    }
    mat
}

/// Solves the option pricing problem using the Crank-Nicolson method.
/// 
/// # Arguments
/// 
/// * `py` - The Python interpreter instance.
/// * `s0` - Initial stock price.
/// * `k` - Strike price.
/// * `r` - Risk-free rate.
/// * `sigma` - Volatility.
/// * `t` - Time to maturity.
/// * `n` - Number of time steps.
/// * `m` - Number of stock price steps.
/// 
/// # Returns
/// 
/// A 2D numpy array containing the option prices.
#[pyfunction]
fn crank_nicolson(
    py: Python,
    s0: f64,
    k: f64,
    r: f64,
    sigma: f64,
    t: f64,
    n: usize,
    m: usize,
) -> PyResult<Py<PyArray2<f64>>> {
    // Time step size
    let dt = t / n as f64;
    // Spatial step size
    let dx = sigma * (t / n as f64).sqrt();
    // Drift term
    let nu = r - 0.5 * sigma.powi(2);
    // Square of spatial step size
    let dx2 = dx * dx;

    // Initialize the grid for option prices
    let mut grid = DMatrix::<f64>::zeros(n + 1, m + 1);

    // Set terminal condition (payoff at maturity)
    for j in 0..=m {
        let stock_price = s0 * (-((m as isize / 2) as f64 - j as f64) * dx).exp();
        grid[(n, j)] = (k - stock_price).max(0.0);
    }

    // Coefficients for the tridiagonal matrices
    let alpha = 0.25 * dt * (sigma.powi(2) / dx2 - nu / dx);
    let beta = -0.5 * dt * (sigma.powi(2) / dx2 + r);
    let gamma = 0.25 * dt * (sigma.powi(2) / dx2 + nu / dx);

    // Construct tridiagonal matrices A and B
    let a = tridiag(m + 1, -alpha, 1.0 - beta, -gamma);
    let b = tridiag(m + 1, alpha, 1.0 + beta, gamma);

    // Perform LU decomposition of matrix A
    let lu = a.lu();

    // Backward time-stepping to solve the PDE
    for i in (0..n).rev() {
        let rhs = &b * grid.row(i + 1).transpose();
        let sol = lu.solve(&rhs).ok_or_else(|| PyValueError::new_err("Failed to solve LU"))?;
        grid.set_row(i, &sol.transpose());
    }

    // Convert DMatrix to Vec<Vec<f64>> for PyArray2
    let rows = grid.nrows();
    let cols = grid.ncols();
    let mut vec_2d = Vec::with_capacity(rows);
    for i in 0..rows {
        let mut row = Vec::with_capacity(cols);
        for j in 0..cols {
            row.push(grid[(i, j)]);
        }
        vec_2d.push(row);
    }

    // Create a numpy array from the Vec<Vec<f64>>
    let array = PyArray::from_vec2_bound(py, &vec_2d).map_err(|_| PyValueError::new_err("Failed to create numpy array"))?;
    Ok(array.unbind())
}

/// Calculates the Greeks (delta, gamma, theta, vega, and rho) for the option.
/// 
/// # Arguments
/// 
/// * `py` - The Python interpreter instance.
/// * `s0` - Initial stock price.
/// * `k` - Strike price.
/// * `r` - Risk-free rate.
/// * `sigma` - Volatility.
/// * `t` - Time to maturity.
/// * `n` - Number of time steps.
/// * `m` - Number of stock price steps.
/// 
/// # Returns
/// 
/// A tuple containing the values of delta, gamma, theta, vega, and rho.
#[pyfunction]
fn calculate_greeks(
    py: Python,
    s0: f64,
    k: f64,
    r: f64,
    sigma: f64,
    t: f64,
    n: usize,
    m: usize,
) -> PyResult<(f64, f64, f64, f64, f64)> {
    let grid = crank_nicolson(py, s0, k, r, sigma, t, n, m)?;
    let grid = grid.bind(py).readonly();
    let ds = s0 * (sigma * (t / n as f64).sqrt()).exp();
    let dt = t / n as f64;

    let price_idx = m / 2;
    let price = grid.as_slice().unwrap()[price_idx];
    let price_up = grid.as_slice().unwrap()[price_idx + 1];
    let price_down = grid.as_slice().unwrap()[price_idx - 1];

    // Calculate delta, gamma, and theta
    let delta = (price_up - price_down) / (2.0 * ds);
    let gamma = (price_up - 2.0 * price + price_down) / (ds * ds);
    let theta = (grid.as_slice().unwrap()[price_idx + 1] - price) / dt;

    // Calculate vega
    let eps = 0.01;
    let sigma_vega = sigma + eps;
    let grid_vega = crank_nicolson(py, s0, k, r, sigma_vega, t, n, m)?;
    let grid_vega = grid_vega.bind(py).readonly();
    let price_vega = grid_vega.as_slice().unwrap()[price_idx];
    let vega = (price_vega - price) / eps;

    // Calculate rho
    let r_rho = r + eps;
    let grid_rho = crank_nicolson(py, s0, k, r_rho, sigma, t, n, m)?;
    let grid_rho = grid_rho.bind(py).readonly();
    let price_rho = grid_rho.as_slice().unwrap()[price_idx];
    let rho = (price_rho - price) / eps;

    Ok((delta, gamma, theta, vega, rho))
}

/// Extracts the option price from the grid at the initial time step for the given parameters.
/// 
/// # Arguments
/// 
/// * `py` - The Python interpreter instance.
/// * `grid` - The option price grid.
/// * `_s0` - Initial stock price (not used).
/// * `_k` - Strike price (not used).
/// * `_r` - Risk-free rate (not used).
/// * `_sigma` - Volatility (not used).
/// * `_t` - Time to maturity (not used).
/// * `_n` - Number of time steps (not used).
/// * `m` - Number of stock price steps.
/// 
/// # Returns
/// 
/// The option price at the initial time step.
#[pyfunction]
fn extract_option_price(
    py: Python,
    grid: Py<PyArray2<f64>>,
    m: usize,
) -> PyResult<f64> {
    let grid = grid.bind(py).readonly();
    // Assuming s0 is near the middle of the grid
    let price_idx = m / 2;
    Ok(grid.as_slice().unwrap()[price_idx])
}

/// Module definition for the finite_differences_options_rs Python module.
#[pymodule]
fn libfinite_differences_options_rs(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(crank_nicolson, m)?)?;
    m.add_function(wrap_pyfunction!(calculate_greeks, m)?)?;
    m.add_function(wrap_pyfunction!(extract_option_price, m)?)?;
    Ok(())
}

Numerical Methods of Pricing Options

Stock options are financial derivatives that give the holder the right, but not the obligation, to buy (call options) or sell (put options) an underlying stock at a predetermined price (strike price) on or before a specified date (expiration date). Options are valuable tools for investors and traders to hedge risk, generate income, or speculate on price movements. I may write a post how to use options to generate regular income with only a modicum of risk.

The value of an option is influenced by several factors, including the current stock price, strike price, time to expiration, risk-free interest rate, and the stock's volatility. These factors are quantified using "greeks," which measure the sensitivity of the option's price to changes in these variables. The primary greeks are:

  1. Delta (Δ): The rate of change of the option price with respect to the change in the underlying stock price.
  2. Gamma (Γ): The rate of change of delta with respect to the change in the underlying stock price.
  3. Theta (Θ): The rate of change of the option price with respect to the passage of time.
  4. Vega (ν): The rate of change of the option price with respect to the change in the implied volatility of the underlying stock.
  5. Rho (ρ): The rate of change of the option price with respect to the change in the risk-free interest rate.

To price options and calculate greeks, closed-form models like the Black-Scholes and Bjerksund-Stensland are commonly used. The Black-Scholes model is widely used for European-style options, which can only be exercised at expiration. The model assumes that the underlying stock price follows a geometric Brownian motion with constant volatility and risk-free interest rate. The Black-Scholes formula for a call option is:

Formula
C=SN(d1)-Ke-rTN(d2)
where:
d1=ln(SK)+(r+σ22)TσT
d2=d1-σT

The Bjerksund-Stensland model is an extension of the Black-Scholes model that allows for early exercise, making it suitable for American-style options. The closed-form solution is more complex and involves additional calculations.

The closed-form formulas for the Black-Scholes greeks are:

Greek Formula
Delta (Δ) Δ=N(d1)
Gamma (Γ) Γ=N'(d1)SσT
Theta (Θ) Θ=-SN'(d1)σ2T-rKe-rTN(d2)
Vega (ν) ν=STN'(d1)
Rho (ρ) ρ=KTe-rTN(d2)

Understanding options and their greeks is crucial for effective risk management and trading strategies. Closed-form models provide a quick and efficient way to price options and calculate greeks, although they rely on certain assumptions that may not always hold in real-world market conditions.

I go over Black-Scholes and Bjerksund-Stensland in more detail in my post on pricing options. In this post, I will focus on numerical methods used to price options, including binomial trees and the Leisen-Reimer method.

Numerical methods and closed-form methods are two different approaches to solving mathematical problems, including the pricing of options and calculation of greeks. Here's how they differ:

Closed-form methods:

Closed-form methods provide an exact solution to a problem using a mathematical formula or equation. These methods give a precise answer that can be calculated directly from the inputs without the need for iterative calculations or approximations. The Black-Scholes model is an example of a closed-form solution for pricing European-style options.

Advantages:

  • Exact solution
  • Fast computation
  • Easy to implement and interpret

Disadvantages:

  • Rely on assumptions that may not hold in real-world conditions
  • Limited flexibility to accommodate complex scenarios or non-standard options

Numerical methods:

Numerical methods provide approximate solutions to problems that cannot be solved analytically or have complex closed-form solutions. These methods involve iterative calculations and approximations to arrive at a solution within a specified tolerance level. Examples of numerical methods for option pricing include the Binomial Option Pricing Model (BOPM), Monte Carlo simulations, and Finite Difference Methods%20difference%20equations.) (FDM).

Advantages:

  • Can handle complex scenarios and non-standard options
  • Flexible and adaptable to various assumptions and market conditions
  • Provide insights into option price behavior over time and across various input parameters

Disadvantages:

  • Approximate solution
  • Computationally intensive and time-consuming
  • Requires more programming expertise to implement and interpret

In practice, closed-form methods are preferred when available due to their simplicity and speed. However, numerical methods are essential for dealing with more complex options and market conditions that violate the assumptions of closed-form models.

Numerical methods can also be used to validate closed-form solutions and to provide additional insights into option price behavior. In some cases, a combination of closed-form and numerical methods may be used to balance the trade-offs between accuracy, speed, and flexibility.

We will be focusing on the Binomial Option Pricing Model (BOPM) in this post, which is a popular numerical method for pricing options. The BOPM is based on the concept of a binomial tree, where the option price is calculated at each node of the tree by considering the possible price movements of the underlying stock.

The binomial tree consists of nodes representing the possible stock prices at each time step and branches representing the up and down movements of the stock price. The option price at each node is calculated using the risk-neutral pricing formula, which considers the expected value of the option price at the next time step.

The BOPM can be used to price both European and American-style options and can handle complex scenarios such as dividend payments, early exercise, and multiple time steps. The flexibility and adaptability of the BOPM make it a valuable tool for pricing options in real-world market conditions.

The Leisen-Reimer method is an enhancement of the BOPM that improves the accuracy of option pricing by adjusting the probabilities of up and down movements in the binomial tree. The Leisen-Reimer method uses a modified version of the risk-neutral pricing formula that accounts for skewness and kurtosis in the stock price distribution.

The Leisen-Reimer binomial tree is an improvement over the simple binomial tree method for pricing options. It uses a more accurate approximation for the up and down factors in the tree, leading to faster convergence and more precise results. Here's a simple pseudo-code comparison between the two methods:

Simple Binomial Tree:

function SimpleBinomialTree(S, K, T, r, sigma, N):
    dt = T / N
    u = exp(sigma * sqrt(dt))
    d = 1 / u
    p = (exp(r * dt) - d) / (u - d)

    Initialize asset_prices[N+1] to 0
    asset_prices[0] = S * d^N

    for i from 1 to N:
        asset_prices[i] = u * asset_prices[i-1]

    Initialize option_values[N+1] to 0

    for i from 0 to N:
        option_values[i] = max(0, asset_prices[i] - K)

    for i from N-1 down to 0:
        for j from 0 to i:
            option_values[j] = exp(-r * dt) * (p * option_values[j+1] + (1-p) * option_values[j])

    return option_values[0]

Leisen-Reimer Binomial Tree:

function LeisenReimerBinomialTree(S, K, T, r, sigma, N):
    dt = T / N
    d1 = (log(S / K) + (r + 0.5 * sigma^2) * T) / (sigma * sqrt(T))
    d2 = d1 - sigma * sqrt(T)

    p = 0.5 + copysign(1, d2) * 0.5 * sqrt(1 - exp(-((d2 / (N + 1/3 + 0.1/(N+1)))^2) * (N + 1/6)))
    u = exp(sigma * sqrt(dt / p))
    d = exp(-sigma * sqrt(dt / (1 - p)))

    Initialize asset_prices[N+1] to 0
    asset_prices[0] = S * d^N

    for i from 1 to N:
        asset_prices[i] = u * asset_prices[i-1]

    Initialize option_values[N+1] to 0

    for i from 0 to N:
        option_values[i] = max(0, asset_prices[i] - K)

    for i from N-1 down to 0:
        for j from 0 to i:
            option_values[j] = exp(-r * dt) * (p * option_values[j+1] + (1-p) * option_values[j])

    return option_values[0]

The main differences between the two methods are:

  1. The Leisen-Reimer binomial tree method incorporates the d1 and d2 values from the Black-Scholes model to determine the up and down factors (u and d) in the binomial tree. In the Black-Scholes model, d1 and d2 are used to calculate the probability of the option expiring in-the-money under the assumption of a log-normal distribution of stock prices. By using these values in the binomial tree, the Leisen-Reimer method captures the essence of the Black-Scholes model's assumptions while retaining the flexibility and intuitive appeal of the binomial tree framework. This approach leads to a more accurate approximation of the continuous-time process assumed by the Black-Scholes model, resulting in faster convergence and more precise option pricing compared to the simple binomial tree method.

  2. The probability p is calculated differently in the Leisen-Reimer method, using a more complex formula that includes a correction term (1/3 + 0.1/(N+1)) to improve convergence.

These modifications lead to faster convergence and more accurate results compared to the simple binomial tree method, especially for a smaller number of time steps (N). The Leisen-Reimer method is particularly useful for options with high volatility or near-the-money strike prices, where the standard BOPM may produce inaccurate results.

When comparing the computational complexity of the binomial tree methods (both simple and Leisen-Reimer) to the Black-Scholes model, there is a significant difference in their time and space complexities.

Time Complexity: - Black-Scholes: O(1) The Black-Scholes formula involves a fixed number of arithmetic operations and standard normal distribution function evaluations, resulting in a constant time complexity. Regardless of the input parameters, the computation time remains the same.

  • Binomial Trees (Simple and Leisen-Reimer): O(N^2) Both the simple binomial tree and the Leisen-Reimer binomial tree have a time complexity of O(N^2), where N is the number of time steps in the tree. The computation time grows quadratically with the number of time steps, making them significantly slower than the Black-Scholes model, especially for large values of N.

Space Complexity: - Black-Scholes: O(1) The Black-Scholes formula only requires a constant amount of memory to store the input parameters and the calculated option price, resulting in a space complexity of O(1).

  • Binomial Trees (Simple and Leisen-Reimer): O(N) Both the simple binomial tree and the Leisen-Reimer binomial tree have a space complexity of O(N), as they require storing the asset price tree and the option value array, each of size N+1. The memory requirements grow linearly with the number of time steps.

The Black-Scholes model has a significantly lower computational complexity compared to the binomial tree methods. The Black-Scholes model has a constant time complexity of O(1) and a constant space complexity of O(1), while the binomial tree methods have a quadratic time complexity of O(N^2) and a linear space complexity of O(N).

However, it's important to note that the binomial tree methods offer more flexibility in handling complex options and relaxing assumptions, which may justify their use in certain situations despite their higher computational complexity. The choice between the Black-Scholes model and binomial tree methods depends on the specific requirements of the problem and the trade-offs between accuracy, flexibility, and computational efficiency.

// binomial_lr_with_greeks.rs

use crate::binomial_lr_option::BinomialLROption;

/// Represents a binomial LR (Leisen-Reimer) option with Greeks calculation.
///
/// This struct extends the `BinomialLROption` to include the calculation of option Greeks,
/// such as delta, gamma, theta, vega, and rho.
pub struct BinomialLRWithGreeks {
    /// The underlying binomial LR option.
    pub lr_option: BinomialLROption,
}

impl BinomialLRWithGreeks {
    /// Creates a new `BinomialLRWithGreeks` instance with the given `BinomialLROption`.
    ///
    /// # Arguments
    ///
    /// * `lr_option` - The binomial LR option to be used for Greeks calculation.
    pub fn new(lr_option: BinomialLROption) -> Self {
        BinomialLRWithGreeks { lr_option }
    }

    /// Generates a new stock price tree based on the binomial LR option parameters.
    ///
    /// This method calculates the stock prices at each node of the binomial tree using
    /// the up and down factors from the binomial LR option.
    fn new_stock_price_tree(&mut self) {
        let u_over_d = self.lr_option.tree.u / self.lr_option.tree.d;
        let d_over_u = self.lr_option.tree.d / self.lr_option.tree.u;

        self.lr_option.tree.option.sts = vec![vec![
            self.lr_option.tree.option.s0 * u_over_d,
            self.lr_option.tree.option.s0,
            self.lr_option.tree.option.s0 * d_over_u,
        ]];

        for _ in 0..self.lr_option.tree.option.n {
            let prev_branches = &self.lr_option.tree.option.sts[self.lr_option.tree.option.sts.len() - 1];
            let mut st = prev_branches
                .iter()
                .map(|&x| x * self.lr_option.tree.u)
                .collect::<Vec<_>>();
            st.push(prev_branches[prev_branches.len() - 1] * self.lr_option.tree.d);
            self.lr_option.tree.option.sts.push(st);
        }
    }

    /// Calculates the option price and Greeks (delta, gamma, theta, vega, rho).
    ///
    /// This method first sets up the binomial LR option parameters and generates the stock price tree.
    /// It then calculates the option payoffs using the `begin_tree_traversal` method from the binomial LR option.
    /// Finally, it computes the option price and various Greeks based on the calculated payoffs and stock prices.
    ///
    /// # Returns
    ///
    /// A tuple containing the following values:
    /// - `option_value`: The calculated option price.
    /// - `delta`: The option's delta (rate of change of option price with respect to the underlying asset price).
    /// - `gamma`: The option's gamma (rate of change of delta with respect to the underlying asset price).
    /// - `theta`: The option's theta (rate of change of option price with respect to time).
    /// - `vega`: The option's vega (sensitivity of option price to changes in volatility).
    /// - `rho`: The option's rho (sensitivity of option price to changes in the risk-free interest rate).
    pub fn price(&mut self) -> (f64, f64, f64, f64, f64, f64) {
        self.lr_option.setup_parameters();
        self.new_stock_price_tree();

        let payoffs = self.lr_option.tree.begin_tree_traversal();
        let option_value = payoffs[payoffs.len() / 2];
        let payoff_up = payoffs[0];
        let payoff_down = payoffs[payoffs.len() - 1];

        let s_up = self.lr_option.tree.option.sts[0][0];
        let s_down = self.lr_option.tree.option.sts[0][2];

        let ds_up = s_up - self.lr_option.tree.option.s0;
        let ds_down = self.lr_option.tree.option.s0 - s_down;
        let ds = s_up - s_down;
        let dv = payoff_up - payoff_down;

        // Calculate delta as the change in option value divided by the change in stock price
        let delta = dv / ds;

        // Calculate gamma as the change in delta divided by the change in stock price
        let gamma = ((payoff_up - option_value) / ds_up - (option_value - payoff_down) / ds_down)
            / ((self.lr_option.tree.option.s0 + s_up) / 2.0 - (self.lr_option.tree.option.s0 + s_down) / 2.0);

        let dt = 0.0001; // Small perturbation in time
        let original_t = self.lr_option.tree.option.t;
        self.lr_option.tree.option.t -= dt;
        self.lr_option.setup_parameters();
        let payoffs_theta = self.lr_option.tree.begin_tree_traversal();
        let option_value_theta = payoffs_theta[payoffs_theta.len() / 2];

        // Calculate theta as the negative of the change in option value divided by the change in time
        let theta = -(option_value_theta - option_value) / dt;
        self.lr_option.tree.option.t = original_t;

        let dv = 0.01;
        self.lr_option.tree.option.sigma += dv;
        self.lr_option.setup_parameters();
        let payoffs_vega = self.lr_option.tree.begin_tree_traversal();
        let option_value_vega = payoffs_vega[payoffs_vega.len() / 2];

        // Calculate vega as the change in option value divided by the change in volatility
        let vega = (option_value_vega - option_value) / dv;
        self.lr_option.tree.option.sigma -= dv;

        let dr = 0.01;
        self.lr_option.tree.option.r += dr;
        self.lr_option.setup_parameters();
        let payoffs_rho = self.lr_option.tree.begin_tree_traversal();
        let option_value_rho = payoffs_rho[payoffs_rho.len() / 2];

        // Calculate rho as the change in option value divided by the change in interest rate
        let rho = (option_value_rho - option_value) / dr;
        self.lr_option.tree.option.r -= dr;

        (option_value, delta, gamma, theta, vega, rho)
    }
}

Numerical methods like the Binomial Option Pricing Model and the Leisen-Reimer method are essential tools for pricing options and calculating greeks in real-world market conditions. These methods provide flexibility, accuracy, and insights into option price behavior that closed-form models may not capture. By understanding the strengths and limitations of numerical methods, traders and investors can make informed decisions and manage risk effectively in the options market.

The complete source code for this project can be found on Github. Binomial Option Pricing Model with Greeks This code is written in Rust, but is based off of the Python code in the book "Mastering Python for Finance" Second Edition by James Ma Weiming. The book is a great resource for learning how to price options using numerical methods among many finance topic problems all solvable and explorable in Python. The Rust code is a translation of the Python code with the addition of calculating all of the Greeks.

The Rust code compiles to a library that can then be imported in Python. Making this a Python module allows for easier integration with other Python libraries and tools. The Python code can be used to visualize the results and provide a user interface for the Rust code.

Modeling Stock Options with the Greeks

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.

Modeling a Battery System

In a previous occupation-life, I worked for a large utility company. I worked on an assortment of projects. The projects ranged from analyzing data from smart meters for the generation of leads on electricity theft to work force optimization for the utility's wind and solar assets. Toward the end of my tenure, I was on a project that involved modeling battery systems. At the heart of the project was the objective of minimizing the delta between what was promised or entitled from the battery systems and what was actually delivered. I will refrain from going into the details of the algorithms that were developed to achieve this objective. Instead, I will focus on using standard equations and python to model a battery system.

The model we will develop is a simple model that captures the dynamics of a lithium-ion battery system. The model is a first-order model that captures the dynamics of the battery system. The model is given by the following equations:

  1. State of Charge (SOC) equation:
    dSOCdt=-IQ
  2. Temperature-dependent internal resistance equation: R=R0*(1+α*(T-Tref))
  3. Temperature-dependent open-circuit voltage equation: Voc=Voc0-β*(T-Tref)
  4. Voltage equation: dVdt=Voc-V-I*RQ*R
  5. Heat generation equation:
    Qgen=I2*R
  6. Temperature equation: dTdt=Qgen-(T-Tamb)RthCth

These equations describe the battery model used in the simulation, including the state of charge, temperature-dependent internal resistance and open-circuit voltage, voltage dynamics, heat generation, and temperature dynamics.

The battery model used in this simulation is based on the work of several researchers who have contributed to the field of lithium-ion battery modeling. The specific equations and concepts used in this model are derived from various sources and have been adapted to create a simplified representation of a lithium-ion battery.

Some of the key concepts and equations used in this model can be attributed to the following works:

  1. The state of charge (SOC) equation is based on the Coulomb counting method, which is a fundamental concept in battery modeling. This method has been widely used and discussed in various battery modeling papers and books.
  2. The temperature-dependent internal resistance equation is inspired by the Arrhenius equation, which describes the relationship between temperature and reaction rates. The Arrhenius equation has been applied to battery modeling to capture the effect of temperature on internal resistance. This concept has been discussed in papers such as "Thermal modeling of lithium-ion batteries" by Pesaran et al. (2001).
  3. The temperature-dependent open-circuit voltage equation is based on the concept of the open-circuit voltage (OCV) of a battery varying with temperature. This relationship has been studied and modeled in various papers, such as "Temperature-dependent battery models for high-power lithium-ion batteries" by Huria et al. (2012).
  4. The voltage equation and the heat generation equation are derived from basic electrical and thermal principles, which have been applied to battery modeling in numerous studies. These equations are commonly used in various battery models to describe the voltage dynamics and heat generation within the battery.
  5. The temperature equation is based on the heat transfer principles and the concept of thermal resistance and capacitance. This equation describes the temperature dynamics of the battery considering the heat generation and the heat exchange with the environment. Similar temperature equations have been used in various battery thermal models.

What exactly am I trying to achieve with this model? I am trying to capture the dynamics of a battery system. The model will allow us to simulate the behavior of a battery system under different operating conditions and analyze its performance. The model will provide insights into the state of charge, voltage dynamics, temperature dynamics, and heat generation of the battery system. By simulating the battery system, we can study its behavior and optimize its performance for various applications. Ultimately, I would like to place an actual solar + battery system in a remote location and use a Raspberry Pi or something similar to act as a gateway (using a Sixfab 4G/LTE shield) for gathering weather data and possibly also acting as a Meshtastic gateway. The battery system would be used to power the gateway and the sensors. The model will allow us to optimize the battery system for this application and ensure reliable operation in remote locations.

import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt

# Battery parameters
Q = 100000  # Battery capacity in mAh (100Ah = 100000mAh)
R_0 = 0.05  # Internal resistance at reference temperature in ohms
V_oc_0 = 12 # Open-circuit voltage at reference temperature in volts
alpha = 0.01 # Temperature coefficient for internal resistance (1/°C)
beta = 0.005 # Temperature coefficient for open-circuit voltage (V/°C)
T_ref = 25  # Reference temperature in °C
C_th = 2000 # Thermal capacity of the battery (J/°C)
R_th = 5    # Thermal resistance between battery and environment (°C/W)

# Voltage conversion parameters
converter_efficiency = 0.90  # 90% efficiency for the DC-DC converter

# Raspberry Pi parameters
I_pi_5V = 0.5  # Current drawn by the Raspberry Pi in A at 5V
P_pi = I_pi_5V * 5  # Power consumption of the Raspberry Pi at 5V

# Adjusted power draw from the battery considering voltage conversion
P_pi_battery = P_pi / converter_efficiency  # Power consumption from the battery at 12V

# Sixfab Cellular Modem HAT parameters
P_modem_min = 2  # Minimum power consumption of the modem in watts
P_modem_max = 6  # Maximum power consumption of the modem in watts

# Time and simulation parameters
t_hours = 72
t_start = 0
t_end = t_hours * 3600  # 64 hours in seconds
num_steps = int(t_hours * 60)  # Let's say we simulate at a 1-minute time step
t_points = np.linspace(t_start, t_end, num_steps)

# Initial conditions
SOC_0 = 1.0  # Initial state of charge (0-1)
V_0 = V_oc_0 # Initial voltage
T_0 = 25     # Initial temperature in °C

# Input load current profile, ambient temperature, and modem power consumption (example)
I_load = np.ones(num_steps) * 5  # Constant load current of 5000mA (5A)
T_amb = np.linspace(25, 35, num_steps)  # Ambient temperature varying from 25°C to 35°C
P_modem = np.linspace(P_modem_min, P_modem_max, num_steps)  # Modem power consumption varying between min and max

# Define solar power output: 
# For simplicity, let's assume 12 hours of solar power followed by 12 hours of no power, repeating
def solar_power(t, peak_power=200, sunrise=6*3600, sunset=18*3600):
    period = 24 * 3600  # Period of the solar power cycle (24 hours)
    day_time = t % period
    # During night time there's no solar power
    if day_time < sunrise or day_time > sunset:
        return 0
    # During day time, solar power varies sinusoidally, peaking at noon
    else:
        return peak_power * np.maximum(0, np.cos((day_time - (sunrise+sunset)/2) * np.pi / (sunset-sunrise)))

# Solar power array
P_solar = np.array([solar_power(t) for t in t_points])

# Define the battery model equations
def battery_model(y, t):
    SOC, V, T = y
    I_load_t = np.interp(t, t_points, I_load)
    T_amb_t = np.interp(t, t_points, T_amb)
    P_modem_t = np.interp(t, t_points, P_modem)
    P_solar_t = np.interp(t, t_points, P_solar)

    # Total power needed by the devices
    P_total = P_modem_t + P_pi_battery

    # Net power from the battery (negative when charging)
    P_net = P_total - P_solar_t

    I = P_net / V  # Current drawn from the battery (positive when discharging, negative when charging)
    dSOC_dt = -I / Q
    # Limit the SoC to a maximum of 1 (100% charge)
    if SOC > 1 and dSOC_dt > 0:
        dSOC_dt = 0

    R = R_0 * (1 + alpha * (T - T_ref))
    V_oc = V_oc_0 - beta * (T - T_ref)
    dV_dt = (V_oc - V - I * R) / (Q * R)
    Q_gen = I**2 * R
    dT_dt = (Q_gen - (T - T_amb_t) / R_th) / C_th
    return [dSOC_dt, dV_dt, dT_dt]

# Solve the ODEs
y0 = [SOC_0, V_0, T_0]
sol = odeint(battery_model, y0, t_points)

# Clamp the SOC values to not exceed 1
SOC = np.clip(sol[:, 0], 0, 1)
V = sol[:, 1]
T = sol[:, 2]

# Plot the results
plt.figure(figsize=(14, 10))

plt.subplot(2, 2, 1)
plt.plot(t_points / 3600, SOC)
plt.title('State of Charge')
plt.xlabel('Time (hours)')
plt.ylabel('State of Charge (fraction)')
plt.grid(True)

plt.subplot(2, 2, 2)
plt.plot(t_points / 3600, V)
plt.title('Voltage')
plt.xlabel('Time (hours)')
plt.ylabel('Voltage (V)')
plt.grid(True)

plt.subplot(2, 2, 3)
plt.plot(t_points / 3600, T)
plt.title('Temperature')
plt.xlabel('Time (hours)')
plt.ylabel('Temperature (°C)')
plt.grid(True)

plt.subplot(2, 2, 4)
plt.plot(t_points / 3600, P_solar)
plt.title('Solar Power')
plt.xlabel('Time (hours)')
plt.ylabel('Solar Power (W)')
plt.grid(True)

plt.tight_layout()
plt.show()

Deep Dive into the Model

odeint is a function in Python used to solve ordinary differential equations (ODEs). It's part of the scipy.integrate module, which provides several integration techniques. odeint specifically is popular because of its simplicity and effectiveness in handling a wide array of ODE problems.

Here’s a breakdown of how odeint works:

  1. Problem Specification:
  2. You define the ODE you want to solve in the form of a function. This function must compute the derivatives at a given point in time and state. For a system described by dy=f(y,t), where y is the state vector and t is time, you need to define the function f(y,t).

  3. Initial Conditions:

  4. You specify the initial conditions of the system, y0, which are the values of the state variables at the start time t0.

  5. Time Points:

  6. You provide a sequence of time points for which you want the solution of the ODE. The function will return the state of the system at each of these points.

  7. Calling odeint:

  8. You call odeint with the function, initial conditions, and the time points. Optionally, you can also pass additional arguments and options to control aspects like the integration method and error tolerances.

  9. Integration:

  10. odeint uses the LSODA (Livermore Solver for Ordinary Differential Equations with Automatic method switching for stiff and non-stiff problems) algorithm from the FORTRAN library. It automatically selects between stiff and non-stiff methods. If the problem is stiff, it uses backward differentiation formulas (BDF) from the Gear method. If it's non-stiff, it uses Adams' method.
  11. The solver evaluates the function at various points using these methods, adjusting step size as needed based on error estimates to maintain accuracy while minimizing computational effort.

  12. Output:

  13. odeint returns an array of the state vectors at the requested time points. Each row in the output array corresponds to a time point, and each column corresponds to a component of the state vector.

odeint is powerful for solving complex differential equations in scientific and engineering applications, making it a valuable tool for simulating dynamic systems.

Findings

The simulation results show the state of charge, voltage, temperature, and solar power over time. The state of charge decreases as the battery discharges, and it increases when the battery is charged. The voltage decreases as the battery discharges and increases when the battery is charged. The temperature of the battery increases due to heat generation from the current flow and decreases due to heat exchange with the environment. The solar power output varies over time, reflecting the day-night cycle.

The simulation provides insights into the behavior of the battery system under different operating conditions. By analyzing the simulation results, we can optimize the battery system for specific applications and improve its performance. The model can be further refined and extended to capture more complex battery dynamics and operating conditions.

In wanting to answer the question of how long the battery system can power the Raspberry Pi and the cellular modem, we can analyze the simulation results to determine the battery life under different load conditions. By comparing the power consumption of the devices with the battery capacity and solar power output, we can estimate the battery life and optimize the system for longer operation. We can also answer the question of whether the battery and solar panel system can provide enough power to keep the Raspberry Pi and cellular modem running continuously for a given period through multiple days of operation.

The model can be used to study the performance of the battery system under various scenarios and optimize its design for specific applications. By simulating the battery system, we can analyze its behavior, identify potential issues, and improve its performance. The model provides a valuable tool for designing and testing battery systems for remote applications.

The model can be extended and refined to capture more complex battery dynamics, environmental conditions, and load profiles. By incorporating additional factors such as aging effects, temperature gradients, and charge-discharge cycles, the model can provide a more accurate representation of the battery system. The model can also be used to study the impact of different battery chemistries, cell configurations, and operating conditions on the performance of the battery system.

In conclusion, the battery model presented in this article provides a simple yet effective tool for simulating the behavior of a lithium-ion battery system. The model captures the dynamics of the battery system and allows us to analyze its performance under different operating conditions. By simulating the battery system, we can study its behavior, optimize its design, and ensure reliable operation for remote applications. The model can be further refined and extended to capture more complex battery dynamics and operating conditions. By using the model, we can design and test battery systems for various applications and improve their performance. The model provides a valuable tool for studying the behavior of battery systems and optimizing their design for specific applications.

Rethinking Investment Time Horizons

Imagine investing $1,000 in a stock and watching it grow to $100,000. It may sound like a pipe dream, but according to two books, "100 to 1 in the Stock Market" by Thomas William Phelps and "100 Baggers: Stocks That Return 100-to-1 and How To Find Them" by Christopher W. Mayer, this type of extraordinary return is not only possible but has occurred more frequently than one might expect.

Phelps' classic work, first published in 1972, presents a compelling case for the existence of "centibaggers" - stocks that return 100 times the initial investment. His book laid the foundation for the study of these incredible investments and provided insights into the characteristics that set them apart from the rest of the market.

Fast forward to 2015, and Christopher W. Mayer's "100 Baggers" expands upon Phelps' work, diving deeper into the concept of stocks that return 100-to-1. Mayer's book offers modern case studies, updated strategies, and a fresh perspective on identifying these elusive investment opportunities.

Both authors emphasize the importance of factors such as strong leadership, sustainable competitive advantages, and significant growth potential in identifying potential centibaggers. They also stress the need for investors to conduct thorough research, maintain a long-term mindset, and have the patience to weather short-term volatility.

While the pursuit of 100-to-1 returns is not without risk, Phelps and Mayer argue that investors who understand the key characteristics and strategies for identifying these stocks can greatly improve their chances of success. Their books serve as valuable guides for those seeking to uncover the market's hidden gems.

In this write-up, we'll explore the key ideas and strategies presented in both "100 to 1 in the Stock Market" and "100 Baggers," compare and contrast the authors' approaches, and discuss how investors can apply these lessons to their own investment strategies. By understanding the wisdom shared in these two influential works, investors can gain valuable insights into the pursuit of extraordinary returns in the stock market.

The Concept of 100 Baggers

A "100 bagger" is a stock that increases in value by 100 times the initial investment. For example, if you invested $1,000 in a stock and its value rose to $100,000, that stock would be considered a 100 bagger. This term was popularized by Thomas William Phelps in his 1972 book "100 to 1 in the Stock Market" and later expanded upon by Christopher W. Mayer in his 2015 book "100 Baggers."

Historical examples of 100 baggers Throughout history, there have been numerous examples of stocks that have achieved 100 bagger status. Some notable examples include:

  1. Berkshire Hathaway: Under the leadership of Warren Buffett, Berkshire Hathaway has grown from around $19 per share in 1965 to over $600,000 per share in 2024, representing a return of more than 2,000,000%.
  2. Monster Beverage: Monster Beverage (formerly Hansen Natural) saw its stock price increase from around $0.08 per share in 1995 to over $80 per share in 2015, a 100,000% return.
  3. Amazon: Amazon's stock price has grown from $1.50 per share during its IPO in 1997 to over $3,000 per share in 2021, a return of more than 200,000%.
  4. Apple: Apple's stock has risen from a split-adjusted IPO price of $0.10 in 1980 to over $165 per share in 2024, a return of more than 140,000%.

Both Phelps and Mayer highlight these and other examples to illustrate the potential for extraordinary returns in the stock market. While 100 baggers are rare, they are not impossible to find, and investors who are willing to put in the effort and exercise patience can greatly increase their chances of identifying these lucrative opportunities. The two of them also emphasize the importance of maintaining a long-term perspective and avoiding the temptation to trade in and out of positions based on short-term market fluctuations. In reality, most people are not patient enough to hold onto a stock for the long term, and they end up selling too soon. This is why it is important to have a long-term mindset and the patience to weather short-term volatility.

The power of compounding returns The concept of 100 baggers highlights the incredible power of compounding returns over time. When a stock consistently delivers high returns year after year, the compounding effect can lead to astronomical growth in value.

To illustrate, consider an investment that grows at an annual rate of 20%. After 25 years, the initial investment would be worth 95 times the starting value. If that same investment grew at a 26% annual rate, it would take only 20 years to achieve a 100 bagger return.

The power of compounding underscores the importance of identifying stocks with strong, sustainable growth potential and holding them for the long term. By allowing investments to compound over time, investors can potentially turn relatively small initial investments into substantial sums.

However, it's crucial to recognize that achieving 100 bagger returns is not easy and requires a combination of skill, research, and patience. In the following sections, we'll explore the key characteristics of 100 baggers and strategies for identifying these rare and lucrative investment opportunities.

Key Characteristics of 100 Baggers

Both Thomas William Phelps and Christopher W. Mayer have identified several key characteristics that are common among stocks that achieve 100 bagger returns. By understanding these attributes, investors can better position themselves to identify potential 100 baggers in the market.

A. Strong, visionary leadership One of the most critical factors in a company's long-term success is the presence of strong, visionary leadership. 100 bagger companies are often led by exceptional managers who have a clear understanding of their industry, a compelling vision for the future, and the ability to execute their strategies effectively.

These leaders are able to navigate their companies through challenges, adapt to changing market conditions, and capitalize on new opportunities. They are also skilled at communicating their vision to employees, investors, and other stakeholders, creating a strong sense of purpose and alignment throughout the organization.

B. Sustainable competitive advantages Another key characteristic of 100 baggers is the presence of sustainable competitive advantages, or "moats." These are the unique qualities that allow a company to maintain its edge over competitors and protect its market share over time.

Some examples of competitive advantages include:

  1. Network effects: The more users a product or service has, the more valuable it becomes (e.g., social media platforms).
  2. Economies of scale: Larger companies can produce goods or services more efficiently and at lower costs than smaller competitors.
  3. Brand loyalty: Strong brand recognition and customer loyalty can create a barrier to entry for competitors.
  4. Intellectual property: Patents, trademarks, and other proprietary technologies can give a company a significant advantage.

Companies with strong, sustainable competitive advantages are better positioned to maintain their growth and profitability over the long term, making them more likely to become 100 baggers.

C. Robust growth potential To achieve 100 bagger returns, a company must have significant growth potential. This can come from a variety of sources, such as: 1. Expanding into new markets or geographies 2. Introducing new products or services 3. Increasing market share in existing markets 4. Benefiting from industry tailwinds or secular growth trends

Investors should look for companies with a large addressable market, a proven ability to innovate, and a track record of consistent growth. Companies that can grow their earnings and cash flow at high rates over an extended period are more likely to become 100 baggers.

D. Attractive valuation Finally, to maximize the potential for 100 bagger returns, investors should seek out companies that are trading at attractive valuations relative to their growth potential. This means looking for stocks that are undervalued by the market or have yet to be fully appreciated by other investors.

One way to identify potentially undervalued stocks is to look for companies with low price-to-earnings (P/E) ratios relative to their growth rates. Another approach is to look for companies with strong fundamentals and growth prospects that are trading at a discount to their intrinsic value.

By combining the search for strong, visionary leadership, sustainable competitive advantages, robust growth potential, and attractive valuations, investors can increase their chances of uncovering potential 100 baggers in the market. However, it's important to remember that identifying these stocks requires thorough research, due diligence, and a long-term investment horizon.

Strategies for Finding Potential 100 Baggers

While identifying potential 100 baggers is no easy task, there are several strategies investors can employ to increase their chances of success. By combining thorough research, a focus on smaller companies, an understanding of long-term trends, and a patient, long-term mindset, investors can position themselves to uncover the market's hidden gems.

A. Conducting thorough research and due diligence One of the most critical strategies for finding potential 100 baggers is to conduct extensive research and due diligence. This involves going beyond surface-level financial metrics and digging deep into a company's business model, competitive landscape, management team, and growth prospects.

Some key areas to focus on when researching potential 100 baggers include:

  1. Financial statements: Look for companies with strong and consistent revenue growth, high margins, and robust cash flow generation.
  2. Management team: Assess the quality and track record of the company's leadership, paying particular attention to their vision, strategy, and ability to execute.
  3. Competitive advantages: Identify the unique qualities that set the company apart from its competitors and give it a lasting edge in the market.
  4. Industry dynamics: Understand the larger trends and forces shaping the company's industry, and look for companies positioned to benefit from these tailwinds.

By conducting thorough research and due diligence, investors can gain a deeper understanding of a company's true potential and make more informed investment decisions.

B. Focusing on smaller, lesser-known companies Another key strategy for finding potential 100 baggers is to focus on smaller, lesser-known companies. These companies are often overlooked by larger investors and analysts, creating opportunities for value-oriented investors to get in on the ground floor of a potential winner.

Smaller companies may have more room for growth than their larger counterparts, as they can expand into new markets, introduce new products, or gain market share more easily. They may also be more agile and adaptable to changing market conditions, allowing them to capitalize on new opportunities more quickly.

However, investing in smaller companies also comes with increased risk, as these firms may have less access to capital, fewer resources, and a shorter track record of success. As such, investors must be particularly diligent in their research and analysis when considering smaller, lesser-known companies.

C. Identifying long-term trends and industry tailwinds To find potential 100 baggers, investors should also focus on identifying long-term trends and industry tailwinds that can drive sustained growth over time. These trends can come from a variety of sources, such as:

  1. Demographic shifts: Changes in population size, age structure, or consumer preferences can create new opportunities for companies in certain industries.
  2. Technological advancements: The emergence of new technologies can disrupt existing industries and create new markets for innovative companies to exploit.
  3. Regulatory changes: Changes in government policies or regulations can create new opportunities or challenges for companies in affected industries.

By identifying and understanding these long-term trends, investors can position themselves to benefit from the companies best positioned to capitalize on these tailwinds.

D. Patience and long-term mindset Finally, one of the most essential strategies for finding potential 100 baggers is to maintain a patient, long-term mindset. Building a 100 bagger takes time, often decades, and investors must be willing to hold onto their investments through short-term volatility and market fluctuations.

This requires a deep conviction in the underlying business and its long-term prospects, as well as the discipline to resist the temptation to sell too early. Investors should approach potential 100 baggers as long-term business owners rather than short-term traders and be prepared to weather the ups and downs of the market over time.

By combining thorough research, a focus on smaller companies, an understanding of long-term trends, and a patient, long-term mindset, investors can increase their chances of uncovering the market's most promising opportunities and achieving the outsized returns associated with 100 bagger investments.

Case Studies from the Book

In "100 Baggers," Christopher W. Mayer presents several case studies of companies that have achieved 100-to-1 returns for their investors. These real-world examples provide valuable insights into the characteristics and strategies that have contributed to these remarkable success stories.

A. Overview of a few notable 100 bagger examples from the books

  1. Monster Beverage: Originally known as Hansen Natural, this company focused on selling natural sodas and juices. However, their introduction of the Monster Energy drink in 2002 catapulted the company to new heights. From 1995 to 2015, Monster Beverage's stock price increased by a staggering 100,000%, turning a $10,000 investment into $10 million.
  2. Altria Group: Formerly known as Philip Morris, Altria Group is a tobacco company that has delivered consistent returns for investors over the long term. Despite facing challenges such as litigation and declining smoking rates, Altria has adapted and diversified its business, resulting in a return of more than 100,000% from 1968 to 2015.
  3. Walmart: Founded by Sam Walton in 1962, Walmart has grown from a single discount store in Arkansas to the world's largest retailer. By focusing on low prices, efficient operations, and strategic expansion, Walmart has delivered returns of more than 100,000% since its IPO in 1970.
  4. Pfizer: Phelps noted that an investment of $1,000 in Pfizer in 1942 would have grown to $102,000 by 1962, representing a 100-fold increase.
  5. Chrysler: An investment of $1,000 in Chrysler in 1932 would have grown to $271,000 by 1952, a return of more than 200 times the initial investment.
  6. Coca-Cola: A $1,000 investment in Coca-Cola in 1919 would have grown to $126,000 by 1939, a return of more than 100 times.
  7. Sears, Roebuck and Co.: An investment of $1,000 in Sears in 1922 would have grown to $175,000 by 1942, representing a return of 175 times the initial investment.
  8. Merck & Co.: a $1,000 investment in Merck & Co. in 1930 would have grown to $160,000 by 1950, a return of 160 times the initial investment.

It is not lost on me that Sears, Roebuck and Co. is now bankrupt and Chrysler got swallowed by Fiat which in turn became Stellantis. This is a reminder that past performance is not indicative of future results. It is also a reminder that the market is not always efficient. It is also a reminder that the market is not always rational. It is also a reminder that the market is not always right.

B. Lessons learned from these success stories

  1. Focus on long-term growth: Each of these companies had a clear vision for long-term growth and remained committed to their strategies over time.
  2. Adapt to changing market conditions: Whether it was Monster Beverage's pivot to energy drinks or Altria's diversification into new product categories, these companies demonstrated an ability to adapt to changing market conditions and consumer preferences. As an aside, I was an Altria shareholder for years. It was not until their botched investment into JUUL Labs, Inc. and subsequent opinion that vaping just might not be able to generate the returns of the bygone days of Big Tobacco that I sold my entire position.
  3. Maintain a competitive edge: Walmart's relentless focus on low prices and efficient operations allowed it to maintain a competitive advantage over its rivals and continue growing over time.
  4. >Reinvest in the business: These companies consistently reinvested their profits into the business, funding new growth initiatives, expanding into new markets, and improving their operations.

C. How readers can apply these lessons in their own investing

  1. Identify companies with clear growth strategies: Look for companies that have a well-defined vision for long-term growth and a track record of executing on their plans.
  2. Assess adaptability: Consider how well a company is positioned to adapt to changing market conditions and consumer preferences over time.
  3. Evaluate competitive advantages: Seek out companies with strong and sustainable competitive advantages that can help them maintain their edge in the market.
  4. Analyze capital allocation: Pay attention to how a company allocates its capital, looking for firms that reinvest in the business and pursue high-return opportunities.
  5. Maintain a long-term perspective: As these case studies demonstrate, building a 100 bagger takes time. Investors must be patient and maintain a long-term outlook, even in the face of short-term volatility or market uncertainty.

By studying the success stories presented in "100 Baggers," readers can gain valuable insights into the characteristics and strategies that have contributed to these remarkable investment outcomes. By applying these lessons to their own investment approach, investors can improve their chances of identifying and profiting from the next generation of 100 bagger opportunities.

Criticisms and Risks

While the pursuit of 100 bagger investments can be an exciting and potentially lucrative endeavor, it is important to acknowledge the challenges and risks associated with this approach. By understanding the limitations and potential counterarguments to the strategies presented in "100 Baggers," investors can make more informed decisions and better manage their risk.

A. Acknowledge the challenges and risks of seeking 100 baggers

  1. Rarity: 100 baggers are, by definition, rare and exceptional investments. The vast majority of stocks will not achieve this level of returns, and identifying these opportunities requires significant skill, research, and luck.
  2. Volatility: Companies with the potential for 100 bagger returns are often smaller, less established firms with higher levels of volatility and risk. Investors must be prepared for significant ups and downs along the way.
  3. Time horizon: Building a 100 bagger takes time, often decades. Investors must have the patience and discipline to hold onto their investments through market cycles and short-term fluctuations.
  4. Survivorship bias: It is important to note that the case studies presented in "100 Baggers" represent the success stories, and there are countless other companies that have failed or underperformed over time. Investors must be aware of survivorship bias when evaluating historical examples of 100 baggers.

B. Potential counterarguments or limitations of the books' approaches


  1. Market efficiency: Some critics argue that the market is largely efficient and that consistently identifying undervalued stocks is difficult, if not impossible. They contend that the success stories presented in the book are more a result of luck than skill.
  2. Hindsight bias: It is easier to identify the characteristics of successful investments after the fact than it is to predict them in advance. Critics may argue that the book's approach is more useful for explaining past successes than for identifying future opportunities.
  3. Changing market dynamics: The strategies that have worked in the past may not be as effective in the future, as market conditions, industry dynamics, and investor behavior evolve over time.

C. Importance of diversification and risk management

  1. Diversification: Given the high level of risk associated with pursuing 100 baggers, investors must diversify their portfolios across multiple stocks, sectors, and asset classes. By spreading their bets, investors can mitigate the impact of any single investment that fails to meet expectations. Mayer actually suggests that investors should have a concentrated portfolio of stocks. He argues that by concentrating efforts on fewer stocks, his thinking is you should have fewer disappointments.
  2. Risk tolerance: Investors must honestly assess their own risk tolerance and investment objectives, recognizing that the pursuit of 100 baggers may not be suitable for everyone.

While the strategies presented in "100 Baggers" offer a compelling framework for identifying high-potential investment opportunities, investors must remain aware of the challenges and risks associated with this approach. By acknowledging the limitations, maintaining a diversified portfolio, and implementing sound risk management practices, investors can increase their chances of success while mitigating the potential downside of pursuing 100 bagger investments.

Personal Takeaways and Recommendations

After reading "100 Baggers" by Christopher W. Mayer and "100 to 1 in the Stock Market" by Thomas William Phelps, I feel I have gained valuable insights into the characteristics and strategies associated with some of the most successful investments in history. These books have not only provided a compelling framework for identifying potential 100 baggers but have also reinforced the importance of a long-term, patient approach to investing.

A. Learnings from the book


  1. The power of compounding: The case studies presented in these books demonstrate the incredible power of compounding returns over time. By identifying companies with strong growth potential and holding onto them for the long term, investors can achieve outsized returns that far exceed the market average.
  2. The importance of quality: Successful 100 bagger investments often share common characteristics, such as strong management teams, sustainable competitive advantages, and robust growth prospects. By focusing on high-quality companies with these attributes, investors can increase their chances of success.
  3. The value of patience: Building a 100 bagger takes time, often decades. These books have reinforced the importance of maintaining a long-term perspective and having the patience to hold onto investments through short-term volatility and market fluctuations.
  4. The benefits of independent thinking: Many of the most successful 100 bagger investments were initially overlooked or misunderstood by the broader market. These books have encouraged me to think independently, conduct my own research, and be willing to go against the crowd when necessary.
B. How you plan to incorporate these ideas into your investment strategy
  1. Focus on quality: I plan to place a greater emphasis on identifying high-quality companies with strong management teams, sustainable competitive advantages, and robust growth prospects.
  2. Conduct thorough research: I will dedicate more time and effort to conducting thorough research and due diligence on potential investments, looking beyond surface-level financial metrics to gain a deeper understanding of a company's business model, competitive landscape, and long-term potential.
  3. Maintain a long-term perspective: I will strive to maintain a long-term perspective with my investments, resisting the temptation to trade in and out of positions based on short-term market movements or emotions.
  4. Diversify and manage risk: While pursuing potential 100 baggers, I will continue to diversify my portfolio across multiple stocks, sectors, and asset classes, and implement sound risk management practices to protect against downside risk.

C. Why would I recommend these books to others

  1. Valuable insights: "100 Baggers" and "100 to 1 in the Stock Market" offer valuable insights into the characteristics and strategies associated with some of the most successful investments in history. By studying these examples, readers can gain a deeper understanding of what it takes to identify and profit from high-potential investment opportunities.
  2. Engaging and accessible: Both books are well-written and engaging, presenting complex investment concepts in an accessible and easy-to-understand manner. They strike a good balance between theory and practical application, making them suitable for both novice and experienced investors.
  3. Long-term perspective: These books promote a long-term, patient approach to investing that is often lacking in today's fast-paced, short-term oriented market environment. By encouraging readers to think like business owners and focus on the long-term potential of their investments, these books can help investors avoid common pitfalls and achieve better outcomes.
  4. Inspiration and motivation: The case studies and success stories presented in these books can serve as a source of inspiration and motivation for investors, demonstrating what is possible with a disciplined, long-term approach to investing.

While "100 Baggers" and "100 to 1 in the Stock Market" are not without their limitations and potential criticisms, I believe they offer valuable insights and strategies that can benefit investors of all levels. By incorporating the key lessons from these books into a well-diversified, risk-managed investment approach, investors can improve their chances of identifying and profiting from the next generation of 100 bagger opportunities.

Conclusion

Throughout this write-up, we have explored the concept of 100 baggers and the key lessons presented in Christopher W. Mayer's "100 Baggers" and Thomas William Phelps' "100 to 1 in the Stock Market." These books offer valuable insights into the characteristics and strategies associated with some of the most successful investments in history, providing a roadmap for investors seeking to identify and profit from high-potential opportunities.

A. Recapping the main points about 100 baggers and key lessons

  1. 100 baggers are rare and exceptional investments that generate returns of 100 times or more over the long term.
  2. These investments often share common characteristics, such as strong management teams, sustainable competitive advantages, robust growth prospects, and attractive valuations.
  3. To identify potential 100 baggers, investors must conduct thorough research, focus on smaller and lesser-known companies, identify long-term trends and industry tailwinds, and maintain a patient, long-term mindset.
  4. >The case studies presented in these books demonstrate the power of compounding returns and the importance of thinking like a business owner rather than a short-term trader.

B. The potential rewards and risks of this investment approach

  1. Potential rewards: Investing in 100 baggers can generate life-changing returns, far exceeding the market average and providing financial security for investors and their families.
  2. Potential risks: The pursuit of 100 baggers is not without risk, as these investments are often associated with higher levels of volatility, uncertainty, and potential for loss. Investors must be prepared for the possibility of underperformance or even complete loss of capital.
  3. Importance of diversification and risk management: To mitigate these risks, investors must maintain a well-diversified portfolio, implement sound risk management practices, and carefully consider the size of their positions in potential 100 baggers.
  4. Be prepared for the long haul: Building a 100 bagger takes time, often decades. Investors must have the patience and discipline to hold onto their investments through market cycles and short-term fluctuations. And along the way, there can be drops. NVIDIA, for example, dropped at least 50% three times in the last 25 years.

C. Do your own research and make informed decisions

  1. While "100 Baggers" and "100 to 1 in the Stock Market" offer valuable insights and strategies, they should not be viewed as a substitute for independent research and analysis.
  2. Investors must take responsibility for their own investment decisions, conducting thorough due diligence, evaluating multiple perspectives, and carefully considering their own financial goals and risk tolerance.
  3. The concepts and strategies presented in these books should be viewed as a starting point for further exploration and adaptation, rather than a one-size-fits-all approach to investing.
  4. By combining the lessons from these books with their own research and insights, investors can develop a personalized investment approach that aligns with their unique circumstances and objectives.

"100 Baggers" and "100 to 1 in the Stock Market" offer a compelling framework for identifying and profiting from high-potential investment opportunities. While the pursuit of 100 baggers is not without risk, investors who approach this strategy with a well-diversified, risk-managed, and long-term mindset stand to benefit from the incredible power of compounding returns over time. By studying the lessons presented in these books, conducting their own research, and making informed decisions, investors can position themselves for success in the ever-changing world of investing.

Final, final thought: in January of 2007, I founded an investment club. Fifth & Company Holdings. There were initially five members. By the time I motioned to wind down the businesses of the group in 2020, there were over ten members. Our first investment in early 2007 was ten shares of Deere & Company. We bought ten shares at $48.50 per share. We reinvested all of our dividends from 2007 until we sold in 2020. That was over four years ago. If we hadn't wound down the group, those shares would be worth well over $5,000. If there is one thing took away from Phelps and Mayer, it is if it is at all possible, hold onto your stocks for the long term.

LoRaWAN Weather Station

Recently, I purchased a SenseCAP 8-in-1 LoRaWAN Weather Station. This weather station is designed for long-term outdoor use and is capable of monitoring eight environmental parameters. The SenseCAP 8-in-1 LoRaWAN Weather Station is a versatile and efficient environmental monitoring solution developed by Seeed Studio. The weather station is designed to provide reliable information for applications such as smart agriculture, environmental research, and weather forecasting.

The SenseCAP Weather Station leverages the LoRaWAN protocol for data transmission. LoRaWAN is a low-power, wide-area network (LPWAN) technology that enables long-range communication with minimal power consumption. By utilizing LoRaWAN, the weather station can operate autonomously for extended periods, reducing maintenance requirements and ensuring continuous data collection. The LoRaWAN protocol also provides secure and reliable communication, making it an ideal choice for outdoor applications that require robust connectivity.

I wrote about LoRaWAN + Helium in a previous post. LoRaWAN is an ideal choice for IoT applications that require long-range communication and low power consumption. The SenseCAP Weather Station is designed to take advantage of these benefits, making it an excellent choice for outdoor environmental monitoring. I happen to continue to "mine" the cryptocurrency Helium, which provides me with a ready-made LoRaWAN network to connect the weather station (and other sensors) to. Connecting the SenseCAP 8-in-1 LoRaWAN Weather Station to the Helium network offers numerous benefits and expands the possibilities for environmental monitoring.

Helium is a decentralized, blockchain-based network that enables the deployment of IoT devices and sensors on a global scale. By integrating the SenseCAP Weather Station with the Helium network, users can take advantage of its vast coverage and robust infrastructure. The Helium network's unique incentive model encourages the growth of the network by rewarding participants who provide coverage and maintain the network's integrity. This incentive-driven approach ensures the network's reliability and scalability, making it an ideal choice for deploying weather stations in various locations. Moreover, the Helium network's encryption and security features protect the data transmitted by the SenseCAP Weather Station, ensuring the integrity and confidentiality of the collected information. Not that I am overly concerned about the data collected by the weather station, but it is nice to know that the data is secure.

The eight sensors included in the SenseCAP Weather Station are:

  1. Temperature sensor
  2. Humidity sensor
  3. Barometric pressure sensor
  4. Light intensity sensor
  5. UV sensor
  6. Wind speed sensor
  7. Wind direction sensor
  8. Rain gauge

These sensors work together to provide a detailed understanding of the environment, allowing users to monitor and analyze atmospheric conditions in real-time. The data collected by the weather station can be used to identify trends, detect anomalies, and make informed decisions based on the observed patterns.

SenseCAP provides a web console and mobile app for configuring and monitoring the weather station. The web console offers data visualization tools and analytics features that enable users to explore the collected data and gain insights into the environmental conditions. The mobile app allows for the configuration of the weather station. You need this app to not only configure the weather station but also to get the connection parameters to connect the weather station to the Helium network.

Because of how I wanted to use the data from the weather station, I opted to not use the SenseCAP web console, instead, I used the Helium Console to "attach" the weather station to the Helium network. This allows me to use the Helium API to pull the data from the weather station and use it in my own applications. You can see the current state of the weather in my backyard by visiting this link.

Some technical bits before I continue with the story. The SenseCAP 8-in-1's data is transmitted via LoRaWAN over the Helium Network, the device is registered in the Helium Console (using the values obtained from the mobile app). From there, the data packets are routed to Amazon Web Services' IoT Core service where a Lambda function is triggered to process the data and store it in a PostgreSQL TimescaleDB database. There is another Lambda function that is written in Python, and uses Flask to provide a RESTful API to access the data in the database. The bulk of the data manipulation happens in JavaScript on the aforementioned local weather page. The data is updated every 5 minutes. Many of the metrics on that page are also derived from the weather station data. For example, the "Dew Point" is calculated from the temperature and humidity data. The "Apparent Temperature" is calculated from the temperature, humidity, and wind speed data. The "Heat Index" is calculated from the temperature and humidity data. The "Wind Chill" is calculated from the temperature and wind speed data. And so on. If you do not see "Heat Index," it is most likely because it is not hot enough to calculate it.

The SenseCAP 8-in-1 LoRaWAN Weather Station is a technically advanced device that incorporates a range of high-precision sensors to provide accurate and reliable environmental data. The weather station is equipped with a Sensirion SHT30 temperature and humidity sensor, which offers a temperature accuracy of ±0.2°C and a humidity accuracy of ±2% RH. This sensor ensures that the collected temperature and humidity data is precise and consistent, enabling users to make informed decisions based on the measurements. The barometric pressure sensor, a Bosch BMP280, provides an accuracy of ±1 hPa, allowing for accurate monitoring of atmospheric pressure changes. The light intensity sensor, a Vishay VEML7700, has a spectral range of 400-900nm and an accuracy of ±5%, making it suitable for measuring ambient light conditions. The UV sensor, a Vishay VEML6075, detects both UVA and UVB radiation with an accuracy of ±10%, providing valuable information for assessing UV exposure levels.

The wind speed and direction sensors are key components of the SenseCAP Weather Station. The wind speed sensor, an Optoelectronics Technology Company (OETC) FST200-201, is a 3-cup anemometer with a measurement range of 0-50 m/s and an accuracy of ±3%. The wind direction sensor, also from OETC (FX2001), utilizes a wind vane design and provides a measurement range of 0-360° with an accuracy of ±5°. These sensors enable the weather station to capture detailed wind data, which is essential for understanding local weather patterns and predicting potential changes. The rain gauge, a tipping bucket design, has a resolution of 0.2mm per tip and an accuracy of ±4%, allowing for precise measurement of precipitation levels. Lastly, the CO2 sensor, a Sensirion SCD30, measures atmospheric carbon dioxide concentrations with an accuracy of ±(30ppm + 3%) and a measurement range of 400-10,000ppm, providing insights into air quality and environmental conditions.

The device's LoRaWAN connectivity is facilitated by a Semtech SX1262 chipset, which provides long-range, low-power communication. The SenseCAP Weather Station supports both the 915MHz and 868MHz frequency bands, making it compatible with LoRaWAN networks worldwide. The device's enclosure is made of durable, weather-resistant ABS plastic, with an IP65 rating that ensures protection against dust and water ingress. The compact design, measuring 190mm x 120mm x 145mm and weighing approximately 1kg, makes the weather station easy to deploy and install in various locations. Overall, the SenseCAP 8-in-1 LoRaWAN Weather Station's impressive array of technical specifications and features make it a reliable and efficient tool for environmental monitoring and data collection.

Integrating the SenseCAP 8-in-1 LoRaWAN Weather Station with AWS IoT and AWS Lambda can significantly enhance data processing, storage, and analysis capabilities. AWS IoT is a robust platform that enables secure communication between IoT devices and the AWS Cloud. By connecting the SenseCAP Weather Station to AWS IoT, users can easily collect and store the sensor data in a centralized location, making it accessible for further processing and analysis. AWS Lambda, a serverless compute service, allows users to run code without the need to manage underlying infrastructure. With Lambda, users can create custom functions that process and analyze the weather station data in real-time, triggering actions based on specific conditions or thresholds. For example, a Lambda function can be set up to send alerts when temperature readings exceed a certain level or when precipitation reaches a specific threshold. Additionally, Lambda functions can be used to perform data transformations, such as unit conversions or data aggregation, before storing the processed data in a database or forwarding it to other AWS services for further analysis or visualization. By leveraging the power of AWS IoT and Lambda, users can create efficient, automated workflows that optimize the value of the data collected by the SenseCAP Weather Station, ultimately facilitating informed decision-making and advanced environmental monitoring.

Efficient Market Hypothesis

Delving into Burton G. Malkiel's "A Random Walk Down Wall Street" (12th edition) via its audiobook rendition offered me a new perspective in the realm of investment literature. While I had previously engaged with seminal works like Benjamin Graham's "The Intelligent Investor," Malkiel's book was a fresh discovery. Initially, his tone seemed somewhat critical, almost curmudgeonly, as he meticulously dissected various investment theories and strategies. However, as the narrative unfolded, I grasped his underlying message: the stock market's inherent unpredictability and the futility of trying to outsmart it through timing or stock picking. Malkiel, instead, champions a more prudent "buy and hold" strategy, centering on the value of low-cost index funds that mirror the market's average movements, offering a more reliable path to steady long-term returns. This approach, blending caution with insight, emerges as a pivotal piece of advice for both novice and seasoned investors.

Malkiel's book starts by establishing the foundational elements of investing. He ventures into an exploration of diverse financial instruments, such as stocks, bonds, and real estate. He also provides a comprehensive historical review of the stock market, marking significant milestones and events that have shaped its course. A recurring theme in his narrative is the unpredictable nature of the stock market, which he likens to a "random walk." Here, he posits that future market movements are not reliably predictable based on past trends, challenging the notion that historical patterns can guide future investments.

At the heart of Malkiel's thesis is the Efficient Market Hypothesis (EMH), a theory he ardently advocates. EMH suggests that asset prices in the stock market fully absorb and reflect all available information, making it exceedingly difficult, if not impossible, for investors to consistently achieve returns that outstrip the overall market average. This hypothesis negates the effectiveness of both technical analysis, which relies on past market trends, and fundamental analysis, based on company performance evaluations, in surpassing the market average consistently.

Malkiel extends his analysis to critique a range of investment approaches and current trends, including the intricacies of technical analysis, the dynamics of mutual funds, and the complexities of the new-issue market. He is notably critical of actively managed funds, underscoring their typically higher fees and their often unfulfilled promise of consistently outperforming the market. In contrast, he advocates for a "buy and hold" strategy, emphasizing the virtues of investing in low-cost index funds. These funds, by tracking market averages, offer a more likely pathway to steady and reliable returns over extended periods.

The book also dives into the sphere of behavioral finance, acknowledging the often irrational and psychologically influenced nature of investor behavior. Despite the prevalence of these behavioral irregularities, Malkiel stands by the core tenets of EMH. He suggests investment strategies that acknowledge these human biases yet remain anchored in the principles of the random walk theory.

In later editions of the book, Malkiel ensures its ongoing relevance by incorporating discussions on recent developments in the financial landscape. He examines phenomena like the emergence of exchange-traded funds (ETFs), the ramifications of the dot-com bubble, the profound impact of the 2008 financial crisis, and the advent of new investment technologies. Through these updates, Malkiel assesses how these contemporary issues align with or diverge from his foundational arguments, offering readers insights that resonate with the current financial climate.

"A Random Walk Down Wall Street" stands out as a cornerstone text in the domain of personal finance and investment literature. Its enduring appeal lies in Malkiel's skillful demystification of complex financial concepts and his provision of actionable, practical advice. His advocacy for a disciplined, long-term investment philosophy, with a focus on diversification and minimizing costs, has been a guiding light for numerous investors navigating the often turbulent waters of financial decision-making.

The genesis of the Efficient Market Hypothesis (EMH) can be traced back to the early work of Louis Bachelier in 1900, but it was Eugene Fama who later brought it to prominence, earning a Nobel Prize for his contributions. Fama's 1965 Ph.D. thesis and subsequent 1970 paper, "Efficient Capital Markets: A Review of Theory and Empirical Work," laid a robust foundation for EMH. This theory asserts that financial markets are "informationally efficient," meaning securities' prices in these markets instantaneously and accurately reflect all available information.

EMH categorizes market efficiency into three distinct forms: weak, semi-strong, and strong. Each form carries its own set of implications regarding the speed and accuracy with which information is incorporated into asset prices:

  1. Weak-Form Efficiency: Asserts that all past trading information is already incorporated into stock prices. Therefore, technical analysis based on historical price and volume cannot yield superior returns.

  2. Semi-Strong Form Efficiency: Suggests that all publicly available information is reflected in stock prices, not just past trading data. This means that neither fundamental nor technical analysis can consistently outperform the market.

  3. Strong-Form Efficiency: The most stringent version, stating that all information, public and private, is fully reflected in stock prices. According to this form, not even insider information could give an investor an advantage.

The weak-form efficiency suggests that the market has integrated all historical price and volume data into current stock prices. This assertion fundamentally challenges the effectiveness of technical analysis, a method that relies heavily on past market data to predict future price movements. If weak-form efficiency holds true, then patterns or trends derived from historical data should not provide an edge to investors, as these patterns are already reflected in current prices.

Semi-strong form efficiency broadens this perspective by stating that all publicly available information, including financial reports, news, economic indicators, and more, is already factored into stock prices. This level of market efficiency implies that even well-informed fundamental analysis, which involves a deep dive into a company's financials and market position, cannot consistently lead to outperforming the market. In a semi-strong efficient market, new information is rapidly assimilated, meaning that by the time an investor acts on this information, the market has already adjusted, negating any potential advantage.

Strong-form efficiency takes this concept to its most extreme, positing that all information, both public and private (including insider information), is already incorporated into stock prices. If the market is strong-form efficient, no group of investors, not even insiders with access to non-public information, can consistently achieve returns that beat the market average. This form of EMH suggests that market prices are always fair and reflect the true value of an asset, leaving no room for consistent above-average gains through information-based trading.

These different forms of market efficiency have significant implications for investors and financial analysts:

  1. Investment Strategy: The acceptance of EMH, particularly in its semi-strong or strong forms, often leads investors to favor passive investment strategies, such as investing in index funds. These strategies are based on the belief that actively trying to outperform the market is futile and that a better approach is to simply mirror the market's performance.

  2. Role of Financial Analysts: In a market that adheres to EMH, particularly the semi-strong and strong forms, the traditional role of financial analysts in identifying undervalued stocks or predicting market trends becomes questionable. Instead, their role might shift towards identifying long-term investment trends, assessing risk management, and offering advice on portfolio diversification.

  3. Behavioral Finance: EMH has also spurred interest in behavioral finance, which seeks to understand how psychological factors influence financial markets. This field acknowledges that while EMH provides a useful framework, real-world markets are often influenced by irrational behavior, cognitive biases, and emotional decision-making, leading to market anomalies and inefficiencies.

  4. Market Anomalies: Despite the strong theoretical foundation of EMH, empirical research has identified several market anomalies that challenge the hypothesis. These include phenomena like the small-firm effect, the January effect, and momentum investing, which suggest that there are times and situations where market inefficiencies can be exploited for above-average returns.

  5. Regulatory Implications: EMH also has implications for financial market regulation. If markets are efficient and all information is reflected in prices, the need for regulation to ensure fair and transparent markets becomes more pronounced. Regulators focus on ensuring that all market participants have equal access to information and that insider trading and market manipulation are curtailed.

While the Efficient Market Hypothesis offers a compelling framework for understanding market dynamics and guiding investment strategies, it is not without its critics and challenges. The ongoing debate between supporters of EMH and proponents of alternative theories, like behavioral finance, continues to enrich our understanding of financial markets and investment strategy. This ongoing debate between the Efficient Market Hypothesis (EMH) and alternative theories, particularly behavioral finance, has significantly expanded our comprehension of the complexities inherent in financial markets and investment strategies.

Behavioral finance, in particular, presents a contrast to the traditional EMH view by emphasizing the impact of psychological factors on investor behavior and market outcomes. Proponents of behavioral finance argue that investors are not always rational actors, as EMH assumes, but are instead often influenced by cognitive biases and emotional reactions. This can lead to irrational decision-making and market anomalies that EMH cannot fully explain. One key area of focus in behavioral finance is the study of cognitive biases, such as overconfidence, anchoring, and herd behavior. These biases can lead investors to make decisions that deviate from what would be expected in a fully rational and efficient market. For example, herd behavior can cause investors to irrationally follow market trends, leading to asset bubbles or crashes that are not justified by underlying fundamentals.

Another challenge to EMH comes from empirical evidence of market anomalies that are difficult to reconcile with the hypothesis. Examples include the momentum effect, where stocks that have performed well in the past continue to perform well in the short term, and the value effect, where stocks with lower price-to-earnings ratios tend to outperform. These anomalies suggest that there might be strategies that can consistently yield above-average returns, contrary to what EMH would predict. The debate also extends to the field of corporate finance and market microstructure. Studies in these areas have shown instances where market efficiency is compromised due to factors such as information asymmetry, transaction costs, and market liquidity. These elements can create opportunities for certain investors to achieve above-average returns, challenging the notion that markets are always perfectly efficient.

Furthermore, the global financial crisis of 2007-2008 brought new scrutiny to EMH. The crisis highlighted situations where market prices did not seem to reflect underlying economic fundamentals, leading to significant financial turmoil. This has led some to question whether markets can sometimes be driven more by speculation and irrational behavior than by rational, informed decision-making. In response to these challenges, some proponents of EMH have adapted their views, acknowledging that while markets are generally efficient, there can be periods of inefficiency due to various factors, including investor behavior, market structure, and external economic forces. This more nuanced perspective accepts that while EMH provides a useful baseline for understanding market dynamics, it is not an absolute rule that applies uniformly across all situations and times.

The dialogue between EMH and its critiques, particularly from the field of behavioral finance, has led to a more comprehensive and realistic understanding of financial markets. It recognizes that while markets are often efficient in processing information, there are exceptions and nuances influenced by human behavior, market structure, and external conditions. This enriched perspective is crucial for investors, financial analysts, and policymakers in navigating the complexities of the financial world and making informed decisions.

I have long been skeptical of technical analysis, in particular, the chartist. Despite having two degrees in computer science, I have also been critical of using machine learning and pattern matching to predict stock prices. But, could there be something to technical analysis by way the fact that there are people who believe in it and use it; would not that belief and use have an impact on the market?

Yes, it's possible for there to be identifiable patterns embedded in financial data, and this is a central contention between proponents of technical analysis and those who adhere to the Random Walk Theory or the Efficient Market Hypothesis (EMH). Here's a closer look at this debate:

  1. Technical Analysis Perspective: Proponents of technical analysis believe that there are patterns in stock price movements that, if correctly identified, can be used to predict future price movements. These patterns are thought to arise due to various factors like investor psychology, market sentiment, and supply and demand dynamics. Technical analysts use historical price data and volume data to identify trends and patterns that they believe can be profitably exploited.

  2. Random Walk and EMH Perspective: On the other hand, the Random Walk Theory and EMH suggest that markets are efficient, meaning all available information is already reflected in stock prices. According to these theories, any patterns that appear in historical data are merely coincidences and do not provide a reliable basis for predicting future price movements. They argue that price changes are largely random, driven by the unpredictable arrival of new information.

  3. Evidence of Market Anomalies: However, empirical research has identified various market anomalies that seem to contradict the EMH. For example, the momentum effect (where stocks that have performed well in the past continue to perform well in the short term) and the mean reversion effect (where extreme movements in stock prices tend to be followed by a reversal to the mean) are two well-documented phenomena. These anomalies suggest that there might be patterns in market data that can be exploited.

  4. Complexity of Financial Markets: Financial markets are complex systems influenced by a myriad of factors, including economic indicators, company performance, political events, and trader psychology. This complexity could theoretically lead to the emergence of patterns that might not be immediately apparent or easily predictable.

  5. Limits of Human Perception: Even if patterns exist, the human tendency to see patterns where none exist (pareidolia) and to remember successful predictions while forgetting unsuccessful ones (confirmation bias) can lead to overestimating the effectiveness of pattern recognition in market analysis.

  6. Advances in Technology and Analysis: With advancements in computing power and data analysis techniques, especially with machine learning and artificial intelligence, the ability to analyze vast amounts of market data and identify potential patterns has improved. However, the debate continues as to whether these patterns provide a consistently reliable basis for predicting future market movements.

While it's possible that there are patterns in financial data, the effectiveness of using these patterns for consistent and profitable trading is a matter of ongoing debate in the financial community. The validity and utility of these patterns depend on one's perspective on market efficiency and the predictability of stock price movements.

The belief in technical analysis by a significant number of market participants can, in itself, contribute to its effectiveness to some extent. This phenomenon is often referred to as a self-fulfilling prophecy in financial markets. Here's how it works:

  1. Self-Fulfilling Prophecies: If a large number of traders believe in a specific technical analysis pattern and act on it, their collective actions can influence the market in a way that makes the prediction come true. For example, if many traders believe that a certain stock will rise after it crosses a particular price point (a resistance level), their buying action at that point can drive the price up, thus confirming the original prediction.

  2. Market Psychology and Behavior: Technical analysis, to a large degree, is based on studying investor behavior and market psychology. Patterns and indicators in technical analysis often reflect the mass psychology of investors. When many traders react similarly to certain price patterns or indicators, it can create trends or reversals in the market.

  3. Short-Term Predictability: While the Random Walk Theory and EMH argue against the predictability of stock prices in the long run, they leave room for short-term predictability, which is where technical analysis is often focused. In the short term, trader behavior, driven by beliefs and reactions to patterns, can impact stock prices.

  4. Limits of Market Efficiency: While EMH posits that markets are efficient, real-world markets may not always be perfectly efficient. Inefficient markets can allow for some predictability based on price patterns and trends, making technical analysis more viable.

  5. Role of Institutional Traders: The presence of large institutional traders, who often use technical analysis as part of their trading strategy, can also lend weight to the effectiveness of technical analysis. Their significant trading volumes can influence market movements in line with the predictions of technical analysis.

  6. Complex Adaptive Systems: Markets are complex adaptive systems where the actions of participants can change the rules of the system. In such an environment, the widespread belief in a particular method or system, like technical analysis, can alter market dynamics to align with those beliefs, at least temporarily.

However, it's important to note that while the belief in technical analysis can influence market movements, this influence may not always lead to predictable or consistent outcomes. Market conditions, economic factors, and unexpected news can all disrupt technical patterns. Moreover, relying solely on technical analysis without considering fundamental factors and broader market conditions can lead to inaccurate predictions and potential investment risks.

A Little Rust, a Little Python and some OpenAI: Custom Company Stock Reports

I've been playing around with Rust and Python lately. I've also been playing around with OpenAI's API. I thought it would be fun to combine all three and create a custom company stock report generator. I'm not a financial advisor, so don't take any of this as financial advice. I'm just having fun with some code.

Generative models are all the rage these days. OpenAI's API is a great way to play around with them. I've been using it to generate text. I've also been using it to generate images. I thought it would be fun to use it to generate stock reports. GAI (Generative Artificial Intelligence) is a great way to generate text, but it works even better at taking a pile of data and commentary on a subject and producing a report on that topic. For now, I won't be sharing the code for this project, but I will share the results. The code is an unholy mess that might be the result of me no longer writing software professionally for nearly five years now. I will share snippets of code but not the whole thing.

Check out the reports!

The architecture is something like this:

  • An AWS Lambda function written in Python that orchestrates the heavy lifting. This function is triggered by an AWS SQS queue.
  • An AWS SQS queue that is populated by an AWS Lambda function written in Rust.
  • This Lambda function is exposed as an URL that is mapped to a custom slash command in Slack.

The Python Lambda function does the following:

  • A company stock symbol is passed to it via the SQS queue.
  • It then makes call to Polygon.io's APIs to get the company's name, and a list of recent news articles about the company.
  • Each news article is pulled down and the page contents are extracted using BeautifulSoup4. The text is then passed to OpenAI's API to generate a summary of the article.
  • The Python Lambda function also uses the python module yfinance to pull down the company's stock price history.
  • The Python Lambda function then uses the python module matplotlib to generate a graph of the company's stock price history.
  • Technical analysis is performed on the company's stock price history using the python module ta.
  • The technical analysis is then passed to OpenAI's API to generate a summary of the technical analysis.

The Rust Lambda function does the following:

  • It receives a company stock symbol via an HTTP POST request.
  • The symbol is submitted to an AWS API Gateway endpoint which inserts the symbol into an AWS SQS queue.

The Python Lambda function posts the reports progress to a channel in Slack. The Python Lambda function also posts the report to a channel in Slack when it is complete. The Python Lambda function also posts the report to a web page. The entire site is hosted on AWS S3.

One of the things that I ran into was wanting to get competitors or other companies in the same industry or sector that the subject of a report was part of. Trying to find a data source, that outputted what I wanted was quite difficult. I wanted, for example, a list of all the companies in the same sector as US Steel. I ended up turning to OpenAI's API to generate a list of companies in the same sector. I used the following prompt:

"return a json structure representing competitor companies to US Steel; include ticker symbol, company name and industry;  json should be in the format [{'company': 'Company Name Goes Here', 'symbol': 'SYMBOL', 'industry': 'Actual Industry Name Goes Here'}]; only output json do not wrap it in markdown; use double quotes for quoting keys and values"

Even a year ago, verbosely describing what you wanted to an API let alone an AI API would have been a pipe dream. I was able to get the following output from OpenAI's API:

[
    {
        "company": "ArcelorMittal",
        "symbol": "MT",
        "industry": "Steel"
    },
    {
        "company": "Cleveland-Cliffs Inc.",
        "symbol": "CLF",
        "industry": "Steel"
    },
    {
        "company": "Commercial Metals Company",
        "symbol": "CMC",
        "industry": "Steel"
    },
    {
        "company": "Nucor Corporation",
        "symbol": "NUE",
        "industry": "Steel"
    },
    {
        "company": "Reliance Steel & Aluminum Co.",
        "symbol": "RS",
        "industry": "Steel"
    },
    {
        "company": "Steel Dynamics, Inc.",
        "symbol": "STLD",
        "industry": "Steel"
    },
    {
        "company": "Ternium S.A.",
        "symbol": "TX",
        "industry": "Steel"
    },
]

The report application (the Python Lambda function) is backed by a DynamoDB table. The table has the following schema:

{
    "symbol":       symbol,
    "date_":        end_date.strftime("%Y-%m-%d %H:%M:%S"),
    "fundamentals": stock_fundamentals.to_json(orient='records'),
    "financials":   ticker.financials.to_json(orient='records'),
    "report":       complete_text,
    "data":         last_day_summary.to_json(orient='records'),
    "cost":         Decimal(str(cost)),
    "news":         news_summary,
    "url":          report_url,
    "run_id":       run_id,
}

The symbol field is the company's stock symbol. The date_ field is the date the report was generated. The fundamentals field is a JSON representation of the company's fundamentals. The financials field is a JSON representation of the company's financials. The report field is the report itself. The data field is a JSON representation of the company's stock price history. The cost field is the cost of generating the report; derived from published OpenAI model costs. The news field is a summary of the news articles about the company. The url field is the URL of the report. The run_id field is an ID generated by sqids that is used to identify the report. It is particularly useful when debugging and viewing progress in Slack.

Here is the gist of the code used by the Rust Lambda function:

use lambda_http::{service_fn, RequestExt, IntoResponse, Request, Body};
use std::str;
use percent_encoding::{percent_decode};
use regex::Regex;
use reqwest;
use serde_json::json;
use rust_decimal::Decimal;

#[tokio::main]
async fn main() -> Result<(), lambda_http::Error> {
    tracing_subscriber::fmt()
    .with_max_level(tracing::Level::INFO)
    // disable printing the name of the module in every log line.
    .with_target(false)
    // disabling time is handy because CloudWatch will add the ingestion time.
    .without_time()
    .init();

    lambda_http::run(service_fn(report)).await?;
    Ok(())
}

fn convert_binary_body_to_text(request: &Request) -> Result<String, &'static str> {
    match request.body() {
        Body::Binary(binary_data) => {
            // Attempt to convert the binary data to a UTF-8 encoded string
            str::from_utf8(binary_data)
                .map(|s| s.to_string())
                .map_err(|_| "Failed to convert binary data to UTF-8 string")
        }
        _ => Err("Request body is not binary"),
    }
}

async fn report(
    request: Request
) -> Result<impl IntoResponse, std::convert::Infallible> {
    let _context = request.lambda_context_ref();

    match convert_binary_body_to_text(&request) {
        Ok(text) => {
            // Successfully converted binary data to text

            let client = reqwest::Client::new();
            let re = Regex::new(r"[&]").unwrap();
            let re2 = Regex::new(r"^text=").unwrap();
            let re3 = Regex::new(r"[=]").unwrap();
            let re4 = Regex::new(r"^response_url=").unwrap();

            let decoded = percent_decode(text.as_bytes())
                            .decode_utf8_lossy() // This method will replace invalid UTF-8 sequences with � (REPLACEMENT CHARACTER)
                            .to_string();  

            let parts: Vec<&str> = re.split(&decoded).collect();

            let mut response_url = String::new();
            let mut name = String::new();
            let mut symbol = String::new();
            let mut resp;

            for part in &parts {
                if re2.is_match(&part) {

                    let p2: Vec<&str> = re3.split(&part).collect();

                    symbol = str::replace(&p2[1], "$", "").to_uppercase();

                    let mut url = format!("https://submit-company-to-sqs?symbol={}", symbol);

                    let _ = client.get(&url)
                        .send()
                        .await
                        .unwrap()
                        .json::<serde_json::Value>()
                        .await
                        .unwrap();

                    url = format!("https://api.polygon.io/v3/reference/tickers/{}?apiKey=APIKEYGOESHERE", symbol);

                    resp = client.get(&url)
                        .send()
                        .await
                        .unwrap()
                        .json::<serde_json::Value>()
                        .await
                        .unwrap();

                    name = extract_info(&resp, "name");

                }
                else if re4.is_match(&part) {
                    let p2: Vec<&str> = re3.split(&part).collect();

                    response_url = format!("{}", p2[1].to_string());

                }
            }

            let _ = client.post(response_url)
                .json(&json!({
                    "response_type": "in_channel",
                    "text": format!("Request for a report for *{}* (<https://finance.yahoo.com/quote/{}|{}>) submitted.", name, symbol, symbol)
                }))
                .send()
                .await;

            Ok(format!(""))
        }
        Err(error) => {
            // Handle the error (e.g., log it, return an error response, etc.)
            Ok(format!("Error: {}", error))
        }
    }

}

fn extract_info(resp: &serde_json::Value, value: &str) -> String {
    if let Some(results) = resp["results"].as_object() {
        if let Some(name_value) = results.get(value) {
            str::replace(name_value.to_string().as_str(), "\"", "")
        } else {
            "Error1".to_string()
        }
    } else {
        "Error2".to_string()
    }
}