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:


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.


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


  • 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 (FDM).


  • 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


  • 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.


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 * 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
                .map(|&x| x * self.lr_option.tree.u)
            st.push(prev_branches[prev_branches.len() - 1] * self.lr_option.tree.d);

    /// 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) {

        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;
        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;
        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;
        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:



  • 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:


And in the gamma formula:


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:



  • Δ 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.
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'."))


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:



  • Γ 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.
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));


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:



  • Θ 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.

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:



  • ν 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.
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);


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:



  • ρ 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.
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'."))


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 {

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


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:




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.


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:
  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:
  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
        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.subplot(2, 2, 2)
plt.plot(t_points / 3600, V)
plt.xlabel('Time (hours)')
plt.ylabel('Voltage (V)')

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

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)')


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.


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.


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'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;

async fn main() -> Result<(), lambda_http::Error> {
    // disable printing the name of the module in every log line.
    // disabling time is handy because CloudWatch will add the ingestion time.


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
                .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)

            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)

                    url = format!("{}?apiKey=APIKEYGOESHERE", symbol);

                    resp = client.get(&url)

                    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 _ =
                    "response_type": "in_channel",
                    "text": format!("Request for a report for *{}* (<{}|{}>) submitted.", name, symbol, symbol)

        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 {
    } else {

Monty: a Minimalist Interpreter for the Z80

In today's world, where high-powered servers and multi-core processors are the norm, it's easy to overlook the importance of lightweight, efficient computing solutions. However, these solutions are vital in various domains such as embedded systems, IoT devices, and older hardware where resources are limited. Lightweight interpreters like Monty can make a significant difference in such environments.

Resource efficiency is a paramount consideration in constrained hardware environments, where every byte of memory and each CPU cycle is a precious commodity. Lightweight interpreters are meticulously designed to optimize the utilization of these limited resources, ensuring that the system runs efficiently. Speed is another critical factor; the minimalistic design of lightweight interpreters often allows them to execute code more rapidly than their heavier counterparts. This is especially vital in applications where time is of the essence, such as real-time systems or embedded devices.

Portability is another advantage of lightweight interpreters. Their compact size and streamlined architecture make it easier to port them across a variety of hardware platforms and operating systems. This versatility makes them a go-to solution for a broad range of applications, from IoT devices to legacy systems. In addition to their functional benefits, lightweight interpreters also contribute to sustainability. By optimizing performance on older hardware, these interpreters can effectively extend the lifespan of such systems, thereby reducing electronic waste and contributing to more sustainable computing practices.

Finally, the cost-effectiveness of lightweight interpreters cannot be overstated. The reduced hardware requirements translate to lower upfront and operational costs, making these solutions particularly attractive for startups and small businesses operating on tighter budgets. In sum, lightweight interpreters offer a multitude of advantages, from resource efficiency and speed to portability, sustainability, and cost-effectiveness, making them an ideal choice for a wide array of computing environments.

Architecture and Design

Monty is designed as a minimalist character-based interpreter specifically targeting the Z80 microprocessor. Despite its minimalism, it aims for fast performance, readability, and ease of use. The interpreter is compact making it highly suitable for resource-constrained environments. One of the key architectural choices is to avoid using obscure symbols; instead, it opts for well-known conventions to make the code more understandable.

Syntax and Operations

Unlike many other character-based interpreters that rely on complex or esoteric symbols, Monty uses straightforward and familiar conventions for its operations. For example, the operation for "less than or equal to" is represented by "<=", aligning with standard programming languages. This design choice enhances readability and lowers the learning curve, making it more accessible to people who have experience with conventional programming languages.

Performance Considerations

Monty is engineered for speed, a critical attribute given its deployment on the Z80 microprocessor, which is often used in embedded systems and retro computing platforms. Its size and efficient operation handling contribute to its fast execution speed. The interpreter is optimized to perform tasks with minimal overhead, thus maximizing the utilization of the Z80's computational resources.

Extensibility and Usability

While Monty is minimalist by design, it does not compromise on extensibility and usability. The interpreter can be extended to include additional features or operations as needed. Its design principles prioritize ease of use and readability, making it an excellent choice for those looking to work on Z80-based projects without the steep learning curve often associated with low-level programming or esoteric languages.

  1. Designed for Z80 Microprocessor: Monty is optimized for this specific type of microprocessor, making it highly efficient for a range of embedded solutions.

  2. Small Footprint: Monty is ideal for constrained environments where resource usage must be minimized.

  3. Readability: Despite its minimalistic approach, Monty does not compromise on code readability. It adopts well-known conventions and symbols, making the code easier to understand and maintain.

  4. Feature-Rich: Monty supports various data types, input/output operations, and even advanced features like different data width modes, making it a versatile tool despite its small size.

In this blog post, we'll take a comprehensive tour of Monty Language, delving into its unique features, syntax, and functionalities. The topics we'll cover include:

  1. Syntax and Readability: How Monty offers a readable syntax without compromising on its lightweight nature.

  2. Reverse Polish Notation (RPN): A look into Monty's use of RPN for expressions and its advantages.

  3. Data Handling: Exploring how Monty deals with different data types like arrays and characters.

  4. Data Width Modes: Understanding Monty's flexibility in handling data width, covering both byte and word modes.

  5. Input/Output Operations: A complete guide on how Monty handles I/O operations effectively.

  6. Advanced Features: Discussing some of the more advanced features and commands that Monty supports, including terminal and stream operations.

By the end of this post, you'll have an in-depth understanding of Monty Language, its capabilities, and why it stands out as a minimalist yet powerful interpreter.

Discussion on Constrained Environments (e.g., Embedded Systems, IoT Devices)

Constrained environments in computing refer to platforms where resources such as processing power, memory, and storage are limited. These environments are common in several key sectors:

Embedded systems are specialized computing setups designed to execute specific functions or tasks. They are pervasive in various industries and applications, ranging from automotive control systems and industrial machines to medical monitoring devices. These systems often have to operate under tight resource constraints, similar to Internet of Things (IoT) devices. IoT encompasses a wide array of gadgets such as smart home appliances, wearable health devices, and industrial sensors. These devices are typically limited in terms of computational resources and are designed to operate on low power, making efficient use of resources a crucial aspect of their design.

In the realm of edge computing, data processing is localized, taking place closer to the source of data—be it a sensor, user device, or other endpoints. By shifting the computational load closer to the data origin, edge computing aims to reduce latency and improve speed. However, like embedded and IoT systems, edge devices often operate under resource constraints, necessitating efficient use of memory and processing power. This is also true for legacy systems, which are older computing platforms that continue to be operational. These systems frequently have substantial resource limitations when compared to contemporary hardware, making efficiency a key concern for ongoing usability and maintenance.

Together, these diverse computing environments—embedded systems, IoT devices, edge computing platforms, and legacy systems—all share the common challenge of maximizing performance under resource constraints, making them prime candidates for lightweight, efficient software solutions.

The Value of Efficiency and Simplicity in Such Settings

In constrained environments, efficiency and simplicity aren't just desirable qualities; they're essential. Here's why:

  1. Resource Optimization: With limited memory and CPU cycles, a lightweight interpreter can make the difference between a system running smoothly and one that's sluggish or non-functional.

  2. Battery Life: Many constrained environments are also battery-powered. Efficient code execution can significantly extend battery life.

  3. Reliability: Simple systems have fewer points of failure, making them more reliable, especially in critical applications like healthcare monitoring or industrial automation.

  4. Quick Deployment: Simple, efficient systems can be deployed more quickly and are easier to maintain, providing a faster time-to-market for businesses.

  5. Cost Savings: Efficiency often translates to cost savings, as you can do more with less, reducing both hardware and operational costs.

C. How Monty Fits into This Landscape

Monty Language is tailored to thrive in constrained environments for several reasons:

  1. Minimal Footprint: With a size of just 5K, Monty is incredibly lightweight, making it ideal for systems with limited memory.

  2. Optimized for Z80 Microprocessor: The Z80 is commonly used in embedded systems and IoT devices. Monty's optimization for this microprocessor means it can deliver high performance in these settings.

  3. Simple Syntax: Monty's syntax is easy to understand, which simplifies development and maintenance. This is crucial in constrained environments where every line of code matters.

  4. Feature Completeness: Despite its minimalist nature, Monty offers a broad array of functionalities, from handling various data types to advanced I/O operations, making it a versatile choice for various applications.

The Technical Specifications: Designed for Z80, 5K Footprint

The technical specs of Monty are a testament to its focus on minimalism and efficiency:

  1. Z80 Microprocessor: Monty is specially optimized for the Z80 microprocessors

  2. Memory Footprint: One of the most striking features of Monty is its extremely small footprint—just 5K. This makes it incredibly lightweight and ideal for systems where memory is at a premium.

Comparison with Other Character-Based Interpreters

When compared to other character-based interpreters, Monty offers several distinct advantages:

  1. Resource Usage: Monty's 5K footprint is often significantly smaller than that of other interpreters, making it more suitable for constrained environments.

  2. Performance: Due to its lightweight nature and optimization for the Z80 processor, Monty often outperforms other interpreters in speed and efficiency.

  3. Feature Set: Despite its size, Monty does not skimp on features, offering functionalities like various data types, I/O operations, and even advanced features like different data width modes.

  4. Community and Support: While Monty may not have as large a user base as some other interpreters, it has a dedicated community and robust documentation, making it easier for newcomers to get started.

Importance of Familiar Syntax and Conventions

Syntax and conventions play a crucial role in the usability and adoption of any programming language or interpreter. Monty stands out in this regard for several reasons:

  1. Ease of Learning: Monty's use of well-known symbols and conventions makes it easy to learn, especially for those already familiar with languages like C.

  2. Readability: The use of familiar syntax significantly improves code readability, which is vital for long-term maintainability and collaboration.

  3. Interoperability: The use of widely accepted conventions makes it easier to integrate Monty into projects that also use other languages or interpreters, thereby enhancing its versatility.

  4. Developer Productivity: Familiar syntax allows developers to become productive quickly, reducing the time and cost associated with the development cycle.

Overview of Monty's Syntax

Monty's syntax is designed to be minimalist, efficient, and highly readable. It employs character-based commands and operators to perform a wide range of actions, from basic arithmetic operations to complex I/O tasks.

  1. Character-Based Commands: Monty uses a simple set of character-based commands for operations. For example, the + operator is used for addition, and the . operator is used for printing a number.

  2. Stack-Based Operations: Monty heavily relies on stack-based operations, particularly evident in its use of Reverse Polish Notation (RPN) for arithmetic calculations.

  3. Special Commands: Monty includes special commands that start with a / symbol for specific tasks, such as /aln for finding the length of an array.

  4. Data Types: Monty allows for a variety of data types including numbers, arrays, and strings, and provides specific syntax and operators for each.

The Rationale Behind Using Well-Known Conventions

The choice of well-known conventions in Monty's design serves multiple purposes:

Ease of adoption is a significant advantage of Monty, especially for developers who are already well-versed in conventional programming symbols and operators. The familiarity of the syntax allows them to quickly integrate Monty into their workflow without the steep learning curve often associated with new or esoteric languages. This ease of adoption dovetails with the improved readability of the code. By utilizing well-known symbols and operators, Monty enhances the code's legibility, thereby facilitating easier collaboration and maintenance among development teams. Moreover, the use of familiar syntax serves to minimize errors, reducing the likelihood of mistakes that can arise from unfamiliar or complex symbols. This contributes to the overall robustness of the code, making Monty not just easy to adopt, but also reliable in a production environment.

Examples to Showcase the Ease of Use

Let's look at a couple of examples to demonstrate how easy it is to write code in Monty.

  1. Simple Addition in RPN:
  2. 10 20 + .

    Here, 10 and 20 are operands, + is the operator, and . prints the result. Despite being in RPN, the code is quite straightforward to understand.

  3. Finding Array Length: [1 2 3] A= A /aln . In this example, an array [1 2 3] is stored in variable A, and its length is found using /aln and printed with ..

Introduction to RPN and Its Historical Context

Reverse Polish Notation (RPN), a concatenative way of writing expressions, has a storied history of adoption, especially in early computer systems and calculators. One of the most notable examples is the Hewlett-Packard HP-35, which was one of the first scientific calculators to utilize RPN. The reason for its early adoption lies in its computational efficiency; RPN eliminates the need for parentheses to indicate operations order, thereby simplifying the parsing and computation process. This computational efficiency was a significant advantage in the era of limited computational resources, making RPN a preferred choice for systems that needed to perform calculations quickly and efficiently.

The foundations of RPN are deeply rooted in formal logic and mathematical reasoning, a legacy of its inventor, Polish mathematician Jan Łukasiewicz. This strong theoretical basis lends the notation its precision and reliability, qualities that have only helped to sustain its popularity over the years. Beyond calculators and early computer systems, RPN's computational benefits have led to its incorporation into various programming languages and modern calculators. It continues to be a popular choice in fields that require high computational efficiency and precise mathematical reasoning, further solidifying its relevance in the computing world.

Advantages of Using RPN in Computational Settings

One of the most salient advantages of RPN is its efficiency in computation, particularly beneficial in constrained environments like embedded systems or older hardware. The absence of parentheses to indicate the order of operations simplifies the parsing and calculation process, allowing for quicker computations. This straightforward approach to handling mathematical expressions leads to faster and more efficient code execution, making RPN a compelling choice for systems that require high-speed calculations.

Another notable benefit of RPN is its potential for reducing computational errors. The notation's unambiguous approach to representing the order of operations leaves little room for mistakes, thus minimizing the chances of errors during calculation. This clarity is especially crucial in fields that demand high levels of precision, such as scientific computing or engineering applications, where even a minor error can have significant consequences.

The stack-based nature of RPN not only adds to its computational efficiency but also simplifies its implementation in software. Because operations are performed as operands are popped off a stack, the computational overhead is reduced, making it easier to implement in various programming languages or specialized software. Furthermore, the notation's ability to perform real-time, left-to-right calculations makes it particularly useful in streaming or time-sensitive applications, where immediate data processing is required. All these factors collectively make RPN a robust and versatile tool for a wide range of computational needs.

Real-World Examples Demonstrating RPN in Monty

Here are a few examples to showcase how Monty utilizes RPN for various operations:

  1. Simple Arithmetic: 5 7 + . Adds 5 and 7 to output 12. The + operator comes after the operands.

  2. Complex Calculations: 10 2 5 * + . Multiplies 2 and 5, then adds 10 to output 20.

  3. Stack Manipulations: 1 2 3 + * . Adds 2 and 3, then multiplies the result with 1 to output 5.

The Stack-Based Nature of RPN and Its Computational Advantages

The inherent stack-based nature of Reverse Polish Notation (RPN) significantly simplifies the parsing process in computational tasks. In traditional notations, complex parsing algorithms are often required to unambiguously determine the order of operations. However, in RPN, each operand is pushed onto a stack, and operators pop operands off this stack for computation. This eliminates the need for intricate parsing algorithms, thereby reducing the number of CPU cycles required for calculations. The streamlined parsing process ultimately contributes to more efficient code execution.

Memory efficiency is another benefit of RPN's stack-based approach. Unlike other notations that may require the use of temporary variables to hold intermediate results, RPN's method of pushing and popping operands and results on and off the stack minimizes the need for such variables. This leads to a reduction in memory overhead, making RPN especially valuable in constrained environments where memory resources are at a premium.

The stack-based architecture of RPN also offers advantages in terms of execution speed and debugging. Operations can be executed as soon as the relevant operands are available on the stack, facilitating faster calculations and making RPN well-suited for real-time systems. Additionally, the stack can be easily inspected at any stage of computation, which simplifies the debugging process. Being able to directly examine the stack makes it easier to identify issues or bottlenecks in the computation, adding another layer of convenience and efficiency to using RPN.

Introduction to Data Types Supported by Monty

Monty Language supports a limited but versatile set of data types to fit its minimalist design. These data types include:

  1. Numbers: Integers are the basic numeric type supported in Monty.

  2. Arrays: Monty allows for the creation and manipulation of arrays, supporting both single and multi-dimensional arrays.

  3. Characters: Monty supports ASCII characters, which can be used in various ways including I/O operations.

  4. Strings: While not a distinct data type, strings in Monty can be represented as arrays of characters.

B. How to Manipulate Arrays in Monty

Arrays are a crucial data type in Monty, and the language provides several commands for array manipulation:

  1. Initialization: [1 2 3] A= Initializes an array with the elements 1, 2, and 3 and stores it in variable A.

  2. Length: A /aln . Finds the length of array A and prints it.

  3. Accessing Elements: A 1 [] . Accesses the second element of array A and prints it.

C. Character Handling in Monty

Monty also allows for the manipulation of ASCII characters:

  1. Character Initialization: _A B= Initializes a character 'A' and stores it in variable B.

  2. Character Printing: B .c Prints the character stored in variable B.

  3. Character Input: ,c C= Takes a character input and stores it in variable C.

D. Examples for Each Data Type

Here are some simple examples to showcase operations with each data type:

  1. Numbers: 5 2 + . Adds 5 and 2 and prints the result (7).

  2. Characters: _H .c Prints the character 'H'.

Introduction to Monty's Flexibility in Data Width

One of the standout features of Monty Language is its flexibility in handling data width. Recognizing that different applications and environments have varying requirements for data size, Monty provides options to operate in two distinct modes: byte mode and word mode.

  1. Byte Mode: In this mode, all numeric values are treated as 8-bit integers, which is useful for highly constrained environments.

  2. Word Mode: In contrast, word mode treats all numeric values as 16-bit integers, providing more range and precision for calculations.

Discussion on Byte Mode and Word Mode

Let's delve deeper into the two modes:

  1. Byte Mode (/byt):

    • Ideal for systems with severe memory limitations.
    • Suitable for applications where the data range is small and 8 bits are sufficient.
    • Can be activated using the /byt command.
  2. Word Mode (/wrd):

    • Useful for applications requiring higher numeric ranges or greater precision.
    • Consumes more memory but offers greater flexibility in data manipulation.
    • Activated using the /wrd command.

How to Switch Between Modes and When to Use Each

Switching between byte and word modes in Monty is straightforward:

  1. To Switch to Byte Mode: /byt

  2. To Switch to Word Mode: /wrd

When to Use Each Mode:

  1. Byte Mode:

    • When memory is extremely limited.
    • For simple I/O operations or basic arithmetic where high precision is not needed.
  2. Word Mode:

    • When the application involves complex calculations requiring a larger numeric range.
    • In systems where memory is not as constrained.

Overview of I/O Operations in Monty

Input/Output (I/O) operations are fundamental to any programming language or interpreter, and Monty is no exception. Despite its minimalist design, Monty offers a surprisingly robust set of I/O operations:

  1. Printing: Monty allows for the output of various data types including numbers, characters, and arrays.

  2. Reading: Monty provides commands to read both numbers and characters from standard input.

  3. Advanced I/O: Monty even supports more advanced I/O functionalities, such as handling streams, although these may require deeper familiarity with the language.

Detailed Look into Commands for Printing and Reading Various Data Types

Monty's I/O commands are designed to be as straightforward as possible, here's a look at some of them:

  1. Printing Numbers (.):

    • The . command prints the top number from the stack.
  2. Printing Characters (.c):

    • The .c command prints the top character from the stack.
  3. Printing Arrays (.a):

    • The .a command prints the entire array from the stack.
  4. Reading Numbers (,):

    • The , command reads a number from standard input and pushes it onto the stack.
  5. Reading Characters (,c):

    • The ,c command reads a character from standard input and pushes it onto the stack.

C. Practical Examples Showcasing I/O Operations

Here are some examples to showcase Monty's I/O capabilities:

  1. Printing a Number: 42 . This will print the number 42.

  2. Printing a Character: _A .c This will print the character 'A'.

  3. Printing an Array: [1 2 3] .a This will print the array [1 2 3].

  4. Reading a Number and Doubling It:

    , 2 * .

    This will read a number from the input, double it, and then print it.

  5. Reading and Printing a Character:

  6. ,c .c

    This will read a character from the input and then print it.

Monty's I/O operations, although simple, are incredibly versatile and can be effectively used in a wide range of applications. Whether you're printing arrays or reading characters, Monty provides the tools to do so in a straightforward manner, aligning with its minimalist philosophy while offering robust functionality.


Monty is a character-based interpreter optimized for resource-constrained environments like embedded systems and IoT devices. It offers a rich set of features, including advanced terminal operations and stream-related functionalities. One of its key strengths lies in its minimalist design, which focuses on fast performance, readability, and ease of use. Monty uses well-known symbols for operations, making it easier for developers to adopt. Its design philosophy aims to offer a robust set of features without compromising on size and efficiency. The interpreter is also extensible, allowing for the addition of new features as required.

Monty's design makes it especially effective for niche markets that require resource optimization, such as embedded systems, IoT devices, and even legacy systems with limited computational resources. Its advanced terminal operations enable robust human-machine interactions, while its streaming functionalities offer a powerful toolset for real-time data processing. Monty's syntax, inspired by well-known programming conventions, minimizes the learning curve, thereby encouraging quicker adoption. This blend of features and efficiencies makes Monty an ideal solution for specialized applications where resource usage, real-time processing, and ease of use are critical factors.

Monty brings together the best of both worlds: the capability of a feature-rich language and the efficiency of a lightweight interpreter. Its focus on performance, extensibility, and readability makes it a compelling option for projects in resource-constrained environments. The interpreter's versatility in handling both terminal operations and stream-related tasks makes it suitable for a wide array of applications, from simple utilities to complex data pipelines. When considering a programming solution for projects that require fast execution, low memory overhead, and ease of use, Monty stands out as a robust and efficient choice. Its design is particularly aligned with the needs of specialized markets, making it a tool worth considering for your next retro project in embedded systems, IoT, or similar fields.

Additional Resources:

Arduino Z80 + Forth

The Forth programming language was developed in the late 1960s by Charles H. Moore as a stack-oriented language that would allow efficient data manipulation and rapid program development. One of its most distinctive features is the dual-stack architecture, where a parameter stack is used for data passing and a return stack manages control flow. This unique design made Forth an excellent fit for the microprocessor architectures that emerged in the 1970s, most notably the Z80.

The Z80 microprocessor, introduced in 1976 by Zilog, has an architecture that pairs well with Forth, particularly because of its efficient use of memory and registers. A typical Forth environment on the Z80 is initialized through a kernel, written in Z80 assembly language, which serves as the foundational layer. Upon this base, high-level Forth "words" or function calls are constructed, broadening the language's capabilities. Users can further extend these capabilities by defining their own "words" through a system called "colon definitions." The resulting definitions are stored in Forth's dictionary, a data structure that allows for quick look-up and execution of these custom words.

For hardware interfacing, the Z80 microprocessor's built-in support for memory-mapped I/O is an advantage that complements Forth's intrinsic ability for direct hardware manipulation. Forth’s language primitives enable direct interaction with specific memory locations, facilitating control over connected hardware components. This hardware-level control is indispensable for applications like real-time control systems or embedded applications. In this context, the Z80's specific features, such as its set of index registers and bit manipulation instructions, are highly beneficial.

On top of the core Forth environment, specialized versions have been developed exclusively for the Z80. One such environment is Firth, a Z80-centric Forth variant by John Hardy, which is optimized for retrocomputing applications. For our project, we'll be deploying Firth in conjunction with Retroshield Z80 — a Z80 ⇄ Arduino Mega bridge that allows the execution of Z80 instructions while emulating certain hardware components in Arduino code.

A unique feature of Forth is its dual functionality as both an interpreter and a compiler provides a valuable toolset for various application scenarios. In interpreter mode, users can execute code interactively, which is ideal for real-time debugging and incremental code testing. On the other hand, the compiler mode employs a single-pass approach, generating optimized executable code with minimal overhead. This design is particularly crucial in resource-constrained environments that require quick code iterations and minimal execution time.

While Forth may not execute as quickly as pure assembly language, its benefits often outweigh this shortcoming. For instance, the language offers structured control flow constructs like loops and conditionals, which are not inherently present in assembly. It also has a unified parameter passing mechanism via its dual-stack architecture, making it more manageable and readable than equivalent assembly code. These features make Forth an efficient option in scenarios where resources are limited but performance and functionality cannot be compromised.

The Z80's architecture, with its index registers and bit manipulation instructions, enables an additional level of optimization when used with Forth. Such low-level hardware functionalities can be directly accessed and manipulated through Forth's high-level words, offering a blend of ease-of-use and performance. These technical synergies between the Z80's architecture and Forth's language design make it a compelling choice for embedded systems and other hardware-centric applications. This tight coupling between hardware and software functionalities enables developers to construct highly efficient, tailored solutions for complex computational problems.

Firth on the RetroShield Z80

Debug:      1
SPI-RAM:    0 Bytes
SRAM Size:  6144 Bytes
SRAM_START: 0x2000
SRAM_END:   0x37FF

Firth - a Z80 Forth by John Hardy

---- Sent utf8 encoded message: ": iterate begin over over > while dup . 1+ repeat drop drop ;\n" ----
: iterate begin over over > while dup . 1+ repeat drop drop ;

---- Sent utf8 encoded message: "25 1 iterate\n" ----
25 1 iterate

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 

RetroShield Z80

The RetroShield Z80 functions as a hardware emulator for the Z80 microprocessor by interfacing with an Arduino Mega. The Arduino Mega handles the simulation of memory and I/O ports, utilizing its multiple GPIO pins to emulate the Z80's address, data, and control buses. Clock speed synchronization between the Z80 and Arduino Mega is essential to ensure precise timing for software instruction execution.

Emulated memory configurations are provided via the Arduino Mega, mapping directly into the Z80's accessible address space. Due to hardware limitations on the Arduino, the size of the emulated memory is constrained but generally sufficient for most retrocomputing tasks. Specialized software environments such as Firth, a Forth implementation tailored for the Z80, can be executed. Direct memory access and hardware interaction are among the functionalities that RetroShield Z80 offers.

The emulation logic is generally implemented in software that runs on the Arduino Mega, and the Z80-specific code can be developed independently. Once the Z80 code is compiled into a binary or hex format, it can be loaded into the emulated memory. This approach expedites development cycles by facilitating the simultaneous handling of both emulation and Z80-specific software without the need to switch between different hardware setups for coding and debugging.

Retroshield ⇄ Arduino Mega Data Flow

While Firth can run on it without any major issues, the system is not entirely plug-and-play when it comes to serial communications. The heart of the problem lies in the difference in hardware expectations between the Retroshield's existing software and Firth's architecture and implementation. Current software solutions tailored for the Retroshield often operate under the assumption that an Intel 8251 USART will handle serial communications. However, Firth is engineered to work with the Motorola 6850 ACIA.

In order to use Firth with the Retroshield, our first task is to replace the Intel 8251 USART emulation code with Motorola 6850 ACIA emulation code. The Intel 8251 has a simpler register structure, featuring just two primary registers, STATE and MODE. These are essential for controlling various functionalities of the device, including data flow and operational mode settings. On the other hand, the Motorola 6850 ACIA comes with a more complex set of four registers: DATA RX for data reception, DATA TX for data transmission, CONTROL for configuring the device, and STATUS for monitoring various operational conditions.

Setting the Stage with Definitions and Initialization

Our code starts by setting up environment—defining memory-mapped addresses and control registers essential for the UART's operation. Memory-mapped addresses enable the CPU to interact directly with peripheral devices, like our UART in this case.

#define ADDR_6850_DATA        0x81
#define ADDR_6850_CONTROL     0x80
#define CONTROL_RTS_STATE     (reg6850_CONTROL & 0b01000000)
#define CONTROL_TX_INT_ENABLE (reg6850_CONTROL & 0b00100000)
#define CONTROL_RX_INT_ENABLE (reg6850_CONTROL & 0b10000000)

These constants create an abstraction layer that allows you to interact with the UART as if it were part of the CPU's own memory.

Internal Registers and Initialization

Following the addresses, internal registers (reg6850_DATA_RX, reg6850_DATA_TX, etc.) are initialized. A specialized function, mc6850_init(), is employed to set the UART's initial state. The method of doing this is straightforward but crucial—each bit in the control and status registers controls a particular feature of the UART.

void mc6850_init() {
  // Initialize the 6850 UART settings here
  reg6850_DATA_RX    = 0x00;
  reg6850_DATA_TX    = 0x00;
  reg6850_CONTROL    = 0b01010100;  // RTS HIGH, TX INT Disabled, RX INT Disabled, 8n1, Divider by 1
  reg6850_STATUS     = 0b00000010;  // CTS LOW, DCD LOW, TX EMPTY 1, RX FULL 0


Pin Assignments and Directions

Before delving into the core logic, the code sets up the pin assignments for the microcontroller. These pins are responsible for various functionalities like clock operations, memory and I/O requests, and interrupts.

#define uP_RESET_N  38
#define uP_MREQ_N   41
// ... and many more

Setting up the pins is critical, as these are the actual electrical interfaces that will interact with the outside world.

The Heart of the Code: cpu_tick()

At the core of the program is a function aptly named cpu_tick(). This is where the magic happens—the function is called every clock cycle and is responsible for orchestrating the entire emulation.

void cpu_tick() {

  if (!CONTROL_RTS_STATE && Serial.available())
    reg6850_STATUS = reg6850_STATUS | 0b00000001;  // set receieve data register to 1

      digitalWrite(uP_INT_N, LOW);
      digitalWrite(uP_INT_N, HIGH);

    reg6850_STATUS = reg6850_STATUS & 0b11111110;

    digitalWrite(uP_INT_N, HIGH);



  // IO Access?

  if (!STATE_IORQ_N) // -N ("Dash N") Active Low
    // IO Read?
    if (!STATE_RD_N && prevIORQ) // Z80 is going to read from device
      // Reading from Serial and outputing to 6850


      // 6850 access
      if (ADDR_L == ADDR_6850_DATA) {
        // need to give it DATA_RX value

        prevDATA = reg6850_DATA_RX =;

      else if (ADDR_L == ADDR_6850_CONTROL) 
        // when 0x80, we need to return the status value
        // It means "you can send stuff to me" -- depends upon the bits in STATUS
        prevDATA = reg6850_STATUS;

      DATA_OUT = prevDATA;
    else if (!STATE_RD_N && !prevIORQ)
      DATA_OUT = prevDATA;
    else if (!STATE_WR_N && prevIORQ) // Z80 wants to write to a device (IO bus)
      DATA_DIR = DIR_IN;
      ************** Read from Z80, write to Serial ************** 
      // 6850 access
      if (ADDR_L == ADDR_6850_DATA)
        // there is output available from Z80
        prevDATA = reg6850_DATA_TX = DATA_IN;
        reg6850_STATUS = reg6850_STATUS & 0b11111101;  // clear transmit data entity field
        reg6850_STATUS = reg6850_STATUS | 0b00000010;  // set transmit empty back to 1
      else if (ADDR_L == ADDR_6850_CONTROL)
        // reg6850_CONTROL gets set here and then used in the READ phase when ADDR_L is ADDR_6850_CONTROL

        prevDATA = reg6850_CONTROL = DATA_IN;


      DATA_IN = prevDATA;
      DATA_OUT = 0;


The function cpu_tick() oversees read and write operations for both memory and I/O, manages interrupts, and updates internal registers based on the state of the control lines. This function is a miniaturized event loop that gets invoked every clock cycle, updating the system state.

The first part of cpu_tick() sets up our STATUS register and interrupts for whether there is data pending to be read or not. If there is data pending, the STATUS register is set to 1, and the interrupt pin is set to LOW. If there is no data pending, the STATUS register is set to 0, and the interrupt pin is set to HIGH.

STATUS Register bitmasks

7   6   5   4   3   2   1   0
  • Bit 7: IRQ (also mirrored at Bit 0) - Interrupt Request (set when an interrupt condition exists, reset when the interrupt is acknowledged)
  • Bit 6: TDRE - Transmitter Data Register Empty (set when the transmit data register is empty)
  • Bit 5: TC - Transmit Control (set when the last character in the transmit data register has been sent)
  • Bit 4: RDRF - Receiver Data Register Full (set when a character has been received and is ready to be read from the receive data register)
  • Bit 3: FE - Frame Error (set when the received character does not have a valid stop bit)
  • Bit 2: OVRN - Overrun (set if a character is received before the previous one is read)
  • Bit 1: PE - Parity Error (set when the received character has incorrect parity)
  • Bit 0: Mirrors the IRQ bit

The ACIA's status register uses an 8-bit configuration to manage various aspects of its behavior, ranging from interrupt requests to data carrier detection. Starting from the left-most bit, the IRQ (Interrupt Request) is set whenever the ACIA wants to interrupt the CPU. This can happen for several reasons, such as when the received data register is full, the transmitter data register is empty, or the !DCD bit is set. Next, the PE (Parity Error) is set if the received parity bit doesn't match the locally generated parity for incoming data. The OVRN (Receiver Overrun) bit is set when new data overwrites old data that hasn't been read by the CPU, indicating data loss. The FE (Framing Error) flag comes into play when the received data is not correctly framed by start and stop bits.

TDRE (Transmitter Data Register Empty) indicates that the data register for transmission is empty and ready for new data; it resets when the register is full or if !CTS is high, signaling that the peripheral device isn't ready. Finally, the RDRF (Receiver Data Register Full) is set when the corresponding data register is full, indicating that data has been received, and it gets reset once this data has been read. Each of these bits serves a unique and critical function in managing communication and data integrity for the ACIA.

The next part of cpu_tick() handles I/O operations. The code checks if the Z80 is trying to read or write to the ACIA. If the Z80 is trying to read from the ACIA, the code checks if the Z80 is trying to read from the DATA register or the CONTROL register. If the Z80 is trying to read from the DATA register, the code sets the DATA register to the value of the ACIA's DATA_RX register. If the Z80 is trying to read from the CONTROL register, the code sets the DATA register to the value of the ACIA's STATUS register. If the Z80 is trying to write to the ACIA, the code checks if the Z80 is trying to write to the DATA register or the CONTROL register. If the Z80 is trying to write to the DATA register, the code sets the ACIA's DATA_TX register to the value of the DATA register. If the Z80 is trying to write to the CONTROL register, the code sets the ACIA's CONTROL register to the value of the DATA register.

That's it. I do owe many thanks to Retroshield's creator, Erturk Kocalar, for his help and asistance in completing the 6850 ACIA emulation code. Without an extended pair programming session with him, I would still be spinning my wheels on fully understanding the interplay between Arduino and Z80.

Z80 CP/M: History and Legacy and Emulation

After reading an article that Adafruit put out on running CP/M on an emulator running on an Arduino, I thought I could expand up the article and add to the story. Enjoy.

In the early years of microcomputers, their processing power was incredibly limited compared to what we are accustomed to today. These devices, which emerged in the 1970s, were designed to be affordable and accessible for individual users, businesses, and small organizations, marking a stark departure from the large and expensive mainframes and minicomputers of the time. However, this accessibility came at a cost: the processing capabilities of these early microcomputers were constrained by the technology of the era, as well as by economic and practical considerations.

One of the initial limitations of early microcomputers was the processor itself. Early models, such as the Altair 8800 and Apple I, relied on 8-bit microprocessors like the Intel 8080 and MOS 6502. These 8-bit processors could typically handle only very simple calculations and operations in comparison to more advanced processors. Clock speeds were also significantly lower; they generally ranged from under 1 MHz to a few MHz. This lack of processing speed constrained the tasks that these computers could perform; complex calculations, large datasets, and intricate simulations were largely beyond their reach.

Memory was another significant limiting factor. Early microcomputers were equipped with a very small amount of RAM, often measured in kilobytes rather than the gigabytes or terabytes commonplace today. The limited RAM constrained the size and complexity of the programs that could be run, as well as the amount of data that could be processed at one time. It was not uncommon for users to constantly manage their memory use meticulously, choosing which programs and data could be loaded into their precious few kilobytes of RAM.

Storage capacity in early microcomputers was also quite constrained. Hard drives were expensive and uncommon in the earliest microcomputers, which often used cassette tapes or floppy disks for data storage. These mediums offered extremely limited storage capacity, often on the order of a few tens or hundreds of kilobytes. This required users to be extremely judicious with how they used and stored data and software, as the total available storage space was minuscule compared to today's standards.

In addition to hardware limitations, the software available for early microcomputers was often rudimentary due to the limited processing power. Graphical interfaces were virtually non-existent in the earliest microcomputers, with users typically interacting with the system through text-based command-line interfaces. Software applications were often basic and focused on simple tasks, such as word processing or basic spreadsheet calculations. Sophisticated applications like advanced graphics editing, video processing, or 3D modeling were well beyond the capabilities of these early systems.

Against this burgeoning backdrop of the microcomputer revolution, a man by the name of Gary Kildall developed the Control Program for Microcomputers (CP/M) system. CP/M was a pre-MS-DOS operating system. Kildall, while working at Intel, developed a high-level language named PL/M (Programming Language for Microcomputers). He needed a way to test and debug programs written in PL/M on the newly developed Intel 8080 microprocessor. This led to the creation of CP/M. Recognizing the imminent proliferation of different hardware systems, Kildall, with his experience at Intel and knowledge of microprocessors, saw a need for a standardized software platform. Many microcomputers were operating on incompatible systems, and Kildall's solution was CP/M, an operating system designed to work across diverse hardware setups.

At the heart of CP/M's design was its modularity, characterized predominantly by the BIOS (Basic Input/Output System). The BIOS acted as an intermediary layer that handled the direct communication with the hardware, such as disk drives, keyboards, and displays. By isolating system-specific hardware instructions within the BIOS, CP/M maintained a core set of generic commands. This modular architecture meant that to make CP/M compatible with a new machine, only the BIOS needed to be tailored to the specific hardware, preserving the integrity of the rest of the operating system. This modularity enabled rapid porting of CP/M across a wide array of early microcomputers without rewriting the entire OS.

Another notable technical feature of CP/M was its file system. CP/M used a disk-oriented file system, which was one of the first to use a hierarchical directory structure. This structure allowed users to organize and manage files efficiently on floppy disks. The operating system employed a simple 8.3 filename convention (up to 8 characters for the filename and 3 for the extension) which, though limited by today's standards, was effective for the time. Files were accessed through File Control Blocks (FCBs), a data structure that provided a consistent interface for file operations, further simplifying application development.

CP/M's command-line interface (CLI) was a hallmark feature, providing users with a means to interact with the system and run applications. The CLI, while rudimentary by today's standards, allowed users to navigate the directory structure, execute programs, and manage files. Coupled with a set of basic utilities bundled with the OS, this interface provided an accessible environment for both end-users and developers. For developers, CP/M provided a BDOS (Basic Disk Operating System) interface, allowing applications to be written without deep knowledge of the underlying hardware, thus fostering a rich ecosystem of software tailored for the CP/M platform.

However, CP/M's technical success didn't guarantee lasting market dominance. As it gained traction, Kildall's company, Digital Research, became a major player in the microcomputer software industry. But a missed business opportunity with IBM led to IBM choosing Microsoft's MS-DOS, which bore similarities to CP/M, for its Personal Computer. The story of early personal computing is interesting, and is depicted nicely in Pirates of Silicon Valley (available on DVD). The IBM + MS-DOS choice tilted the scales in the software market, positioning MS-DOS and its successors as major players, while CP/M gradually faded. Nonetheless, CP/M's role in early personal computing is significant, representing a key step towards standardized operating systems.

I wasn't around for the early days of personal computing when CP/M was a big deal. By the time I started exploring computers in the mid-1980s, the Apple IIe was the choice in education where I was first really exposed to personal computers. The Apple IIe was straightforward and easy to use. When I turned it on, I was met with the AppleSoft BASIC interface. In 1992, as I would soon be a teenager, my family purchased its first personal computer from Gateway 2000. Even though I missed the CP/M phase, the Apple IIe provided a solid introduction to the world of computing for me with the Gateway 2000 being foundational in my ever growing interest in computers.

Let's get back to CP/M.

The primary architecture CP/M was designed for was the Intel 8080 and its compatible successor, the Zilog Z80. However, CP/M was adapted to run on several different architectures over time. Here's a brief overview of some architectures and their technical specs:

  1. Intel 8080:

    • 8-bit microprocessor
    • Clock speeds typically up to 2 MHz
    • 4.5k transistors
    • 16-bit address bus, enabling it to access 65,536 memory locations
  2. Zilog Z80:

    • 8-bit microprocessor
    • Clock speeds of 2.5 MHz to 10 MHz
    • Around 8.5k transistors
    • 16-bit address bus, 8-bit data bus
    • It had enhanced instruction set compared to 8080 and was binary compatible with it.
  3. Intel 8085:

    • 8-bit microprocessor
    • Clock speeds of up to 5 MHz
    • An improved and more power-efficient version of the 8080
    • Included new instructions over the 8080
  4. Zilog Z8000 and Intel 8086/8088:

    • These were 16-bit processors.
    • CP/M-86 was developed for these processors as an extension to the original 8-bit CP/M.
    • The 8086 had a 16-bit data bus, and the 8088, used in the original IBM PC, had an 8-bit data bus.
  5. Motorola 68000:

    • While not a primary platform for CP/M, there were ports and adaptations made for the 16/32-bit Motorola 68000 series.
    • Used in early Apple Macintosh computers, Atari ST, Commodore Amiga, and others.
  6. Interdata 7/32:

    • This is a lesser-known 32-bit minicomputer for which CP/M was adapted.

We have already looked at the Z80 (in the context of the TI-84+ graphing calculator) as well as the Motorola 68000 (in the context of the TI-89 graphing calculator). Instead of focusing on a specific architecture, the RC2014, to run CP/M on bare metal, we will be looking at running a CP/M emulator on Adafruit's Grand Central M4 Express. I would love to get of the RC2014 kits and run CP/M on bare metal, but for now, we won't be doing that.

We're concentrating on setting up RunCPM on the Grand Central, so we'll only touch on the Z80 briefly. For additional information on the Z80, visit The person behind also write an in-depth look at Z80 hardware and assembly language in Hackspace Magazine issues 7 & 8. If you're interested in a comprehensive study of the Z80, consider the books: Build your own Z80 computer - design guidelines and application notes by Steve Ciarcia (you can also grab the PDF here or here) and Programming the Z80 by Rodnay Zaks (you can also grab PDFs here, or here or here or here). Both books have out of print for decades and are rather expensive on Amazon.


CP/M incorporated wildcard characters in its file naming conventions, a legacy we continue to see in modern systems. Specifically, '?' was used to match any single character, and '*' could match part of or an entire file name or file type.

In terms of commands, many accepted these wildcards, and such a command was labeled as using an ambiguous file reference, abbreviated as "afn". In contrast, commands that required file references to be specific, without the use of wildcards, were termed as using an unambiguous file reference or "ufn". These shorthand terms, "afn" and "ufn", are frequently found in official CP/M documentation and will be adopted for our discussion here.

Builtin Commands:

  • DIR afn (or simply DIR): Employed to display the names of files that match the specified wildcard pattern.

  • ERA afn: This command is used to delete one or multiple files.

  • REN ufn1=ufn2: As the name suggests, this command allows users to rename a specific file.

  • TYPE ufn: Useful for viewing the contents of an ASCII file..

Standard Programs:

CP/M was equipped with a suite of standard programs, often referred to as Transient Commands. These weren't embedded within the core of CP/M but were accessible to the user as needed. They'd be loaded, run, and then purged from the memory. Several of these commands were fundamental for operations within the CP/M environment. A concise overview of some notable Transient Commands is provided below, though a more exhaustive exploration can be found in the CP/M manual.

  • STAT: This program offers insights into the current disk's status, specifics about individual files, and device assignment details.

  • ASM: A tool for program assembly. It takes a source code input and assembles it to produce an executable.

  • LOAD: Designed for Intel HEX formatted code files, this command loads the code and subsequently converts it into an executable format.

  • DDT: This is CP/M's built-in debugger, essential for diagnosing and resolving program errors.

  • ED: CP/M's text editor, enabling users to create and modify text files within the operating system.

  • SUBMIT: A utility to accept a file containing a list of commands, essentially enabling batch processing.

  • DUMP: A handy tool for those looking to view a file's contents represented in hexadecimal format.

For those eager to dive deeper into the vast ocean of CP/M's capabilities and legacy, the Tim Olmstead Memorial Digital Research CP/M Library is an invaluable resource, housing a trove of information and code associated with CP/M.

RunCPM is essentially a Z80 emulator that comes packaged with different CP/M versions tailored to function on the emulated Z80. It's a comprehensive toolkit for those interested in delving into Z80 assembly language programming, with the added perk of accessing the Grand Central's IO capabilities. As a bonus, Microsoft Basic is incorporated within the package, and for enthusiasts looking to explore further, various other languages can be sourced online. One such language is Modula-2, which holds significance as Niklaus Wirth's successor to the famed Pascal language.

When it comes to building RunCPM, the approach isn't one-size-fits-all. The build method you opt for is contingent on the target platform. In our case, we're aiming for compatibility with the Grand Central, so the Arduino method is the route we'll take. Begin by launching the RunCPM.ino file within the Arduino IDE (or Visual Code). However, prior to this step, ensure that the IDE is configured to build for the Grand Central. The following are stripped down instructions for RunCPM from its Github repo.

RunCPM - Z80 CP/M emulator

RunCPM is an application which can execute vintage CP/M 8 bits programs on many modern platforms, like Windows, Mac OS X, Linux, FreeBSD, Arduino DUE and variants, like Adafruit Grand Central Station, and the Teensy or ESP32. It can be built both on 32 and 64 bits host environments and should be easily portable to other platforms.

RunCPM is fully written in C and in a modular way, so porting to other platforms should be only a matter of writing an abstraction layer file for it. No modification to the main code modules should be necessary.

If you miss using powerful programs like Wordstar, dBaseII, mBasic and others, then RunCPM is for you. It is very stable and fun to use.

RunCPM emulates CP/M from Digital Research as close as possible, the only difference being that it uses regular folders on the host instead of disk images.

Grand Central M4 (GSM4)

  • The ATSAMD51 is large with an Arduino Mega shape and pinout.
  • The front half has the same shape and pinout as Adafruit's Metro's, so it is compatible with all Adafruit shields.
  • It's got analog pins, and SPI/UART/I2C hardware support in the same spot as the Metro 328 and M0.
  • It's powered with an ATSAMD51P20, which includes:
    • Cortex M4 core running at 120 MHz
    • Floating point support with Cortex M4 DSP instructions
    • 1MB flash, 256 KB RAM
    • 32-bit, 3.3V logic and power
    • 70 GPIO pins in total
    • Dual 1 MSPS DAC (A0 and A1)
    • Dual 1 MSPS ADC (15 analog pins)
    • 8 x hardware SERCOM (can be I2C, SPI or UART)
    • 22 x PWM outputs
    • Stereo I2S input/output with MCK pin
    • 12-bit Parallel capture controller (for camera/video in)
    • Built-in crypto engines with AES (256 bit), true RNG, Pubkey controller
  • Power the Grand Central with 6-12V polarity protected DC or the micro USB connector to any 5V USB source.
  • The 2.1mm DC jack has an on/off switch next to it so you can turn off your setup easily.
  • The board will automagically switch between USB and DC.
  • Grand Central has 62 GPIO pins, 16 of which are analog in, and two of which are true analog out.
  • There's a hardware SPI port, hardware I2C port, and hardware UART.
  • 5 more SERCOMs are available for extra I2C/SPI/UARTs.
  • Logic level is 3.3V.

The GC M4 comes with native USB support, eliminating the need for a separate hardware USB to Serial converter. When configured to emulate a serial device, this USB interface enables any computer to send and receive data to the GC M4. Moreover, this interface can be used to launch and update code via the bootloader. The board’s USB support extends to functioning as a Human Interface Device (HID), allowing it to act like a keyboard or mouse, which can be a significant feature for various interactive projects.

On the hardware front, the GC M4 features four indicator LEDs and one NeoPixel located on the front edge of the PCB, designed for easy debugging and status indication. The set includes one green power LED, two RX/TX LEDs that indicate data transmission over USB, and a red LED connected to a user-controllable pin. Adjacent to the reset button, there is an RGB NeoPixel. This NeoPixel can be programmed to serve any purpose, such as displaying a status color code, which adds a visually informative aspect to your projects.

Furthermore, the GC M4 includes an 8 MB QSPI (Quad SPI) Flash storage chip on board. This storage can be likened to a miniature hard drive embedded within the microcontroller. In a CircuitPython environment, this 8 MB flash memory serves as the storage space for all your scripts, libraries, and files, effectively acting as the "disk" where your Python code lives. When the GC M4 is used in an Arduino context, this storage allows for read/write operations, much like a small data logger or an SD card. A dedicated helper program is provided to facilitate accessing these files over USB, making it easy to transfer data between the GC M4 and a computer. This built-in storage is a significant feature, as it simplifies the process of logging data and managing code, and it opens up new possibilities for more advanced and storage-intensive projects.

The GC M4 board boasts a built-in Micro SD Card slot, providing a convenient and flexible option for removable storage of any size. This storage is connected to an SPI (Serial Peripheral Interface) SERCOM, providing high-speed data communication. Notably, SDIO (Secure Digital Input Output), a faster interface that is commonly used for SD cards, is not supported on this board. Nevertheless, the availability of a dedicated Micro SD Card slot is a standout feature, as it allows users to easily expand the storage capacity of their projects without any complex setup. This integrated Micro SD Card slot is a substantial advantage when comparing the GC M4 to other boards, such as the Arduino Due. Unlike the GC M4, the Arduino Due does not come with built-in SD card support. For projects that require additional storage or data logging capabilities on the Due, users must purchase and connect an external Micro SD adapter or a shield, which can add to the overall cost and complexity of the setup. The built-in SD Card slot on the GC M4 eliminates the need for such additional components, simplifying project designs and reducing both the cost and the physical footprint of the final build.

This convenient feature underscores the GC M4's design philosophy of providing an integrated, user-friendly experience. By including an SD Card slot directly on the board, the GC M4 encourages broader experimentation with data-intensive applications, such as data logging, file storage, and multimedia processing, which can be essential for a wide range of creative and practical projects.

Comes pre-loaded with the UF2 bootloader, which looks like a USB storage key. Simply drag firmware on to program, no special tools or drivers needed! It can be used to load up CircuitPython or Arduino IDE (it is bossa v1.8 compatible)

With all of these features, it probably seems like cheating for getting CP/M working. And we will be barely exercising these features. If only Gary Kildall could see how computers and technology have evolved.

Grand Central Specific Adaptations for RunCMP

Arduino digital and analog read/write support was added by Krzysztof Kliś via extra non-standard BDOS calls (see the bottom of cpm.h file for details).

LED blink codes: GSM4 user LED will blink fast when RunCPM is waiting for a serial connection and will send two repeating short blinks when RunCPM has exited (CPU halted). Other than that the user LED will indicate disk activity.

RunCPM needs A LOT of RAM and Flash memory by Arduino standards, so you will need to run on Arduinos like the DUE (not the Duemilanove) and similar controllers, like Adafruit's Grand Central. It is theoretically possible to run it on an Arduino which has enough Flash (at least 96K) by adding external RAM to it via some shield, but this is untested, probably slow and would require an entirely different port of RunCPM code. That could be for another day, but if you want to get CP/M running quickly, grab a Grand Central or Due.

You will also need a micro sd ("tf") card.

When using Arduino boards, the serial speed as well as other parameters, may be set by editing the RunCPM.ino sketch. The default serial speed is 9600 for compatibility with vintage terminals.

You will need to clone the RunCPM repository:

git clone -v

In RunCPM.ino, you will want to specify the Grand Center header file be included:

#include "hardware/arduino/gc.h"

instead of

#include "hardware/arduino/due_sd_tf.h"

Getting Started

Preparing the RunCPM folder :

To set up the RunCPM environment, create a folder that contains both the RunCPM executable and the CCP (Console Command Processor) binaries for the system. Two types of CCP binaries are provided: one for 64K memory and another for 60K memory. On your micro SD card, you will want to create a directory called A which will need a directory called 0 in it. Place in 0 the contents of

The 64K version of the CCPs maximizes the amount of memory available to CP/M applications. However, its memory addressing ranges are not reflective of what a real CP/M computer would have, making it less authentic in terms of emulating a physical CP/M machine.

On the other hand, the 60K version of the CCPs aims to provide a more realistic memory addressing space. It maintains the CCP entry point at the same loading address that it would occupy on a physical CP/M computer, adding to the authenticity of the emulation.

While the 64K and 60K versions are standard, it is possible to use other memory sizes, but this would necessitate rebuilding the CCP binaries. The source code needed to do this is available on disk A.ZIP. The CCP binaries are named to correspond with the amount of memory they are designed to operate with. For example, DRI's CCP designed for a 60K memory environment would be named CCP-DR.60K. RunCPM searches for the appropriate file based on the amount of memory selected when it is built.

It is important to note that starting with version 3.4 of RunCPM, regardless of the amount of memory allocated to the CP/M system, RunCPM will allocate 64K of RAM on the host machine. This ensures that the BIOS always starts at the same position in memory. This design decision facilitates the porting of an even wider range of CCP codes to RunCPM. Starting from version 3.4, it is essential to use new copies of the master disk A.ZIP, as well as the ZCPR2 CCP and ZCPR3 CCP (all of which are provided in the distribution).

Building dependencies

All boards now use the SdFat 2.x library, from here: All Arduino libraries can be found here:

SdFat library change

If you get a 'File' has no member named 'dirEntry' error, then a modification is needed on the SdFat Library SdFatConfig.h file (line 78 as of version 2.0.2) changing:




As file type 1 is required for most of the RunCPM ports.

To find your libraries folder, open the Preferences in Arduino IDE and look at the Sketchbook location field.

Changes to Adapt to the Grand Central

Given that the official repository has already integrated the modifications to support the Grand Central, the following changes are primarily to serve educational purposes or as guidance for those intending to adapt the setup for other M4 boards.

All of the following should already be set in RunCPM.ino, but I'll write them out so you can see what changes have been made.

abstraction_arduino.h For the Grand Central, the alteration pertains to the setting of HostOs:

On line 8, the line:


Should be transformed to:


RunCPM.ino Aligning with the alteration in abstraction_arduino.h, we also need to integrate Grand Central support in this file. Specifically, configurations relating to the SD card, LED interfaces, and the board's designation need adjustment. Insert a branch to the board configuration #if structure at approximately line 28:

  #define USE_SDIO 0
  SdFat SD;
  #define LED 13
  #define LEDinv 0

Due to certain ambiguous factors (perhaps the unique SPI bus configuration for the SD card), initializing the SD card and file system requires a different approach. Thus, following the insertion of the previous snippet, at line 108:

  if (SD.cardBegin(SDINIT, SD_SCK_MHZ(50))) {
    if (!SD.fsBegin()) {
      _puts("\nFile System initialization failed.\n");
  if (SD.begin(SDINIT)) {

This snippet replaces the original:

if (SD.begin(SDINIT)) {

Following these modifications, it's straightforward to get RunCPM functional. For communication, the USB connection acts as the terminal interface. However, take note that not all terminal emulators provide flawless compatibility. Since CP/M anticipates a VT100-style terminal, some features might not behave as expected.

Installing Adafruit SAMD M4 Boards

If you haven't already, you will need to add Adafruit board definitions to Arduino IDE. To do this, copy the URL below and paste into the text field in the dialog box; navigate to File --> Preferences

We will only need to add one URL to the IDE in this example, but you can add multiple URLS by separating them with commas. Copy and paste the link below into the Additional Boards Manager URLs option in the Arduino IDE preferences.

Preparing the CP/M virtual drives :

VERY IMPORTANT NOTE - Starting with RunCPM version 3.7, the use of user areas has become mandatory. The support for disk folders without user areas was dropped between versions 3.5 and 3.6. If you are running a version up to 3.5, it is advisable to consider upgrading to version 3.7 or higher. However, before making this move, it is important to update your disk folder structure to accommodate the newly required support for user areas.

RunCPM emulates the disk drives and user areas of the CP/M operating system by means of subfolders located under the RunCPM executable’s directory. To prepare a folder or SD card for running RunCPM, follow these procedures:

Create subfolders in the location where the RunCPM executable is located. Name these subfolders "A", "B", "C", and so forth, corresponding to each disk drive you intend to use. Each one of these folders represents a separate disk drive in the emulated CP/M environment. Within the "A" folder, create a subfolder named "0". This represents user area 0 of disk A:. Extract the contents of the A.ZIP package into this "0" subfolder. When you switch to another user area within CP/M, RunCPM will automatically create the respective subfolders, named "1", "2", "3", etc., as they are selected. For user areas 10 through 15, subfolders are created with names "A" through "F".

It is crucial to keep all folder and file names in uppercase to avoid potential issues with case-sensitive filesystems. CP/M originally supported only 16 disk drives, labeled A: through P:. Therefore, creating folder names representing drives beyond P: will not function in the emulation, and the same limitation applies to user areas beyond 15 (F).

Available CCPs :

RunCPM can run on its internal CCP or using binary CCPs from real CP/M computers. A few CCPs are provided:

  • CCP-DR - Is the original CCP from Digital Research.
  • CCP-CCPZ - Is the Z80 CCP from RLC and others.
  • CCP-ZCP2 - Is the original ZCPR2 CCP modification.
  • CCP-ZCP3 - Is the original ZCPR3 CCP modification.
  • CCP-Z80 - Is the Z80CCP CCP modification, also from RLC and others.

The A.ZIP package includes the source code for the Console Command Processors (CCPs), allowing for native rebuilding if necessary. To facilitate this, SUBMIT (.SUB) files are provided, which are also useful for rebuilding some of the RunCPM utilities.

While the package comes with a set of CCPs, users can adapt additional CCPs to work with RunCPM. If successful in this adaptation, users are encouraged to share their work so it can be potentially added to the package for others to use. By default, RunCPM utilizes an internal CCP. However, if you prefer to use a different CCP, two specific steps must be taken, which are outlined below:

1 - Change the selected CCP in globals.h (in the RunCPM folder). Find the lines that show:

/ Definition of which CCP to use (must define only one) /

#define CCP_INTERNAL // If this is defined, an internal CCP will emulated

//#define CCP_DR

//#define CCP_CCPZ

//#define CCP_ZCPR2

//#define CCP_ZCPR3

//#define CCP_Z80

Comment out the CCP_INTERNAL line by inserting two slashes at the line's beginning. Then remove the two slashes at the start of the line containing the name of the CCP you intend to use. Save the file.

2 - Copy a matching CCP from the CCP folder to the folder that holds your A folder. Each CCP selection will have two external CCP's, one for 60K and another for 64K. If you have already built the executable, you will need to do it again.

Anytime you wish to change the CCP, you must repeat these steps and rebuild.

IMPORTANT NOTE - CCP-Z80 expects the $$$.SUB to be created on the currently logged drive/user, so when using it, use SUBMITD.COM instead of SUBMIT.COM when starting SUBMIT jobs.

Contents of the "master" disk (A.ZIP) :

The "master" disk, labeled as A.ZIP, serves as the foundational environment for CP/M within RunCPM. It includes the source code for the Console Command Processors (CCPs) and features the EXIT program, which terminates RunCPM when executed.

The master disk also houses the FORMAT program, designed to create a new drive folder, simulating the process of formatting a disk. Importantly, the FORMAT program doesn't affect existing drive folders, ensuring its safe use. Despite its ability to create these drive folders, it doesn't have the capability to remove them from within RunCPM. To remove a drive folder created by the FORMAT program, manual deletion is necessary, which involves accessing the RunCPM folder or SD Card via a host machine.

In addition to these utilities, the master disk contains Z80ASM, a potent Z80 assembler that directly produces .COM files, ready for execution. To further enhance the RunCPM experience, the master disk also includes various CP/M applications not originally part of Digital Research Inc.'s (DRI's) official distribution. A detailed list of these additional applications can be found in the 1STREAD.ME file included on the master disk.


Printing to the PUN: and LST: devices is allowed and will generate files called "PUN.TXT" and "LST.TXT" under user area 0 of disk A:. These files can then be tranferred over to a host computer via XMODEM for real physical printing. These files are created when the first printing occurs, and will be kept open throughout RunCPM usage. They can be erased inside CP/M to trigger the start of a new printing. As of now RunCPM does not support printing to physical devices.

Limitations / Misbehaviors

The objective of RunCPM is not to emulate a Z80 CP/M computer perfectly, but to allow CP/M to be emulated as close as possible while keeping its files on the native (host) filesystem.

This will obviously prevent the accurate physical emulation of disk drives, so applications like MOVCPM and STAT will not be useful.

The master disk, A.ZIP, continues to provide the necessary components to maintain compatibility with Digital Research Inc.'s official CP/M distribution. Currently, only CP/M 2.2 is fully supported, though work is ongoing to bring support for CP/M 3.0.

IN/OUT instructions are designated to facilitate communication between the soft CPU BIOS and BDOS and the equivalent functions within RunCPM, thus these instructions are reserved for this purpose and cannot be used for other tasks. The video monitor in this emulation environment is assumed to be an ANSI/VT100 emulation, which is the standard for DOS/Windows/Linux distributions. This means CP/M applications hard-coded for different terminals may encounter issues with screen rendering.

When using a serial terminal emulator with RunCPM, it is important to configure the emulator to send either a Carriage Return (CR) or a Line Feed (LF) when the Enter key is pressed, but not both (CR+LF). Sending both can disrupt the DIR listing on Digital Research’s Command Control Processor (CCP), consistent with standard CP/M 2.2 behavior.

RunCPM does not support setting files to read-only or applying other CP/M-specific file attributes. All files within the RunCPM environment will be both visible and read/write at all times, necessitating careful file handling. RunCPM does support setting "disks" to read-only, but this read-only status applies only within the context of RunCPM. It does not alter the read/write attributes of the disk’s containing folder on the host system.

Furthermore, some applications, such as Hi-Tech C, may attempt to access user areas numbered higher than 15 to check for a specific CP/M flavor other than 2.2. This action results in the creation of user areas labeled with letters beyond 'F', which is expected behavior and will not be altered in RunCPM.

CP/M Software

CP/M software library here! or here

Having inserted the microSD card and connected the Grand Central appropriately, ensuring both board and port settings are accurate, proceed to build and install onto the Grand Central.

RunCPM provides access to Arduino I/O capabilities through CP/M's BDOS (Basic Disk Operating System) interface. By loading the C register with a function number and a call to address 5, additional functionality that has been added to the system can be accessed.

For these functions, the number of the pin being used is placed in the D register and the value to write (when appropriate) is placed in E. For read functions, the result is returned as noted.


LD C, 220
LD D, pin_number
LD E, mode ;(0 = INPUT, 1 = OUTPUT, 2 = INPUT_PULLUP)


LD C, 221
LD D, pin_number
CALL 5 Returns result in A (0 = LOW, 1 = HIGH). 


LD C, 222
LD D, pin_number
LD E, value ;(0 = LOW, 1 = HIGH)


LD C, 223
LD D, pin_number

Returns result in HL (0 - 1023). 

AnalogWrite (i.e. PWM)

LD C, 224
LD D, pin_number
LD E, value (0-255)
Turning on a LED

Using the provided PinMode and DigitalWrite calls, writing code to control an LED, such as turning it on when connected to pin D8, becomes a straightforward task. To accomplish this, one can use the ED editor to create a file named LED.ASM with the necessary code. This file editing can be done directly on your workstation and saved to the SD card, which is a convenient approach given that ED, the editor, hails from a different era of computing and might feel a bit foreign to modern users accustomed to contemporary text editors.

; Turn on a LED wired to pin 8
org 100h    ;start address
mvi c, 220  ;pinmode
mvi d, 8    ;digital pin number
mvi e, 1    ;value (0 = low, 1 = high)
push d      ;save arguments
call 5      ;call BDOS
pop d       ;restore arguments
mvi c, 222  ;digital write
call 5      ;call BDOS
ret         ;exit to CP/M

Then use the ASM command to assemble it:

A>asm led

RunCPM Version 3.7 (CP/M 2.2 60K)

This produces several files. LED.PRN is a text file containing your assembly language program along with the machine code it assembles to. Each line has 3 columns: address, machine code, and assembly language.

A>type led.prn

0100          org 100h
0100 0EDC     mvi c,220
0102 1608     mvi d,8
0104 1E01     mvi e, 1
0106 D5       push d
0107 CD0500   call 5
010A D1       pop d
010B 0EDE     mvi c, 222
010D CD0500   call 5
0110 C9       ret

There is also now a LED.HEX file. We can use the LOAD command/program to convert it into LED.COM which can be executed.

A> load led

BYTES READ    0011

Now it can executed:


which will turn on the LED connected to pin D8.

So now we can read and write digital and analog I/O from Z80 assembly language code that's running on a Z80 emulated on the Grand Central. That seems pretty round-about.

While that's true, the point is to be able to play around with Z80 assembly language (and CP/M in this case) without having to find or build an actual Z80 system (although that can be its own kind of fun).

Closing Thoughts

One of the most lasting legacies of CP/M is its file system and command-line interface, with its 8-character filename followed by a 3-character file type (e.g., filename.txt), which became a standard that was carried into MS-DOS and later Windows. Its command-line interface, with commands like DIR to list files and REN to rename files, has echoes in the MS-DOS command prompt and persists in modern versions of Windows as the Command Prompt and PowerShell. CP/M was notable for being one of the first operating systems that was largely machine-independent, due to its separation between the operating system and the BIOS (Basic Input/Output System). This made it relatively easy to port CP/M to different computer systems and paved the way for the concept of a software ecosystem that is not tied to a specific set of hardware, a key principle in modern operating system design.

CP/M played a crucial role in the early days of personal computing; before the dominance of MS-DOS and Windows, CP/M was the de facto standard operating system for early microcomputers, fostering the personal computing revolution by making computers more approachable and useful for everyday tasks. When IBM was developing its first personal computer, CP/M was initially considered the operating system of choice, and although IBM ultimately went with MS-DOS (largely due to cost and timing), MS-DOS itself was heavily influenced by CP/M, with many command-line commands being similar and the overall architecture of MS-DOS bearing a strong resemblance to CP/M. This influence extended as MS-DOS evolved into Windows, making CP/M an indirect ancestor of one of the world’s most widely used operating systems. Even after its decline as a primary OS for general-purpose computers, CP/M found a second life in embedded systems and other specialized computing applications due to its lightweight, efficient design, setting the stage for the importance of compact, efficient operating systems in embedded and specialized computing devices, a category that has grown with the proliferation of IoT (Internet of Things) devices. In summary, CP/M stands as an iconic example of how early innovations in computing continue to have ripple effects that extend far into the future.