In partnership with

Trusted by millions. Actually enjoyed by them too.

Morning Brew makes business news something you’ll actually look forward to — which is why over 4 million people read it every day.

Sure, the Brew’s take on the news is witty and sharp. But the games? Addictive. You might come for the crosswords and quizzes, but you’ll leave knowing the stories shaping your career and life.

Try Morning Brew’s newsletter for free — and join millions who keep up with the news because they want to, not because they have to.

Elite Quant Plan – 14-Day Free Trial (This Week Only)

No card needed. Cancel anytime. Zero risk.

You get immediate access to:

  • Full code from every article (including today’s HMM notebook)

  • Private GitHub repos & templates

  • All premium deep dives (3–5 per month)

  • 2 × 1-on-1 calls with me

  • One custom bot built/fixed for you

Try the entire Elite experience for 14 days — completely free.

→ Start your free trial now 👇

(Doors close in 7 days or when the post goes out of the spotlight — whichever comes first.)

See you on the inside.

👉 Upgrade Now

🔔 Limited-Time Holiday Deal: 20% Off Our Complete 2026 Playbook! 🔔

Level up before the year ends!

AlgoEdge Insights: 30+ Python-Powered Trading Strategies – The Complete 2026 Playbook

30+ battle-tested algorithmic trading strategies from the AlgoEdge Insights newsletter – fully coded in Python, backtested, and ready to deploy. Your full arsenal for dominating 2026 markets.

Special Promo: Use code DECEMBER2025 for 20% off

Valid only until January 20, 2026 — act fast!

👇 Buy Now & Save 👇

Instant access to every strategy we've shared, plus exclusive extras.

— AlgoEdge Insights Team

Premium Members – Your Full Notebook Is Ready

The complete Google Colab notebook from today’s article (with live data, full Hidden Markov Model, interactive charts, statistics, and one-click CSV export) is waiting for you.

Preview of what you’ll get:

Inside:

  • Automatic gold data download (2008 → today)

  • Real 3-state Gaussian HMM for volatility regimes

  • Beautiful interactive Plotly charts

  • Regime duration & performance tables

  • Ready-to-use CSV export

  • Bonus: works on Bitcoin, SPX, or any ticker with one line change

Free readers – you already got the full breakdown and visuals in the article. Paid members – you get the actual tool.

Not upgraded yet? Fix that in 10 seconds here👇

Google Collab Notebook With Full Code Is Available In the End Of The Article Behind The Paywall 👇 (For Paid Subs Only)

Spread costs are one of the most overlooked sources of slippage. Even small bid-ask spreads compound quickly, especially so in low-liquidity and/or volatile markets.

Yet, accurately measuring spreads isn not straightforward. Especially when only relying on open, high, low, and close prices instead of full order book data.

Ardia, Guidotti, and Kroencke (2024), therefore, propose an estimation method that improves previous spread measurements. The technique uses ‘Generalized Method of Moments’ and OHLC price transformations.

This method corrects for price discreteness and minimizes estimation variance. Unlike older models, it adapts to market conditions in real-time.

It’s a valueble tool for deciding whether to use a market order, wait, or place a smarter limit order.

End-to-end Implementation Python Notebook provided below.

Here, we’ll cover:

  • Why Traditional Spread Estimators Fail

  • A Better Approach Using OHLC Prices

  • Implementation in Python

  • Further Applications

If You Could Be Earlier Than 85% of the Market?

Most read the move after it runs. The top 250K start before the bell.

Elite Trade Club turns noise into a five-minute plan—what’s moving, why it matters, and the stocks to watch now. Miss it and you chase.

Catch it and you decide.

By joining, you’ll receive Elite Trade Club emails and select partner insights. See Privacy Policy.

1. Why Traditional Spread Estimators Fail

Traditional methods rely on assumptions that often do not hold in real markets. This leads to biased and inconsistent estimates.

One of the most cited models, the Roll estimator, derives the spread from the negative serial covariance of price changes:

Ct is the closing price at time t. This method assumes:

  • all trades occur at observed prices a

  • the bid-ask bounce is the only source of price variation.

However, in real markets:

  • Prices are not observed continuously.

  • There may be zero or few trades in a given period.

  • Trades are executed at various price levels, not just at the close.

To reduce variance, Corwin & Schultz (2012) proposed an estimator using high and low prices:

B represents the variance of log high-low price ratios. This model assumes:

  • High prices correspond to buys, and low prices to sells.

  • Price movements follow a geometric Brownian motion.

Both assumptions fail in markets with price jumps and volatility clustering. This makes it an unreliable estimator.

An improvement by Abdi & Ranaldo (2017) integrates closing, high, and low prices:

where ηt is the mid-price. This reduces bias, but it still fails to fully account for discrete price observations.

This leads to inaccuracies in low-trading-frequency markets.

2. A More Accurate Approach: Using OHLC Prices

Ardia et al. (2024) address these limitations by incorporating all OHLC price observations.

Their key innovation is in using moment conditions to isolate bid-ask spread effects while controlling for fundamental price variations.

The approach defines multiple log-returns based on OHLC prices:

ot, ct, Ht, Lt are log-transformed open, close, high, and low prices.

A correction is introduced via an indicator for price variation to adjust for cases where prices remain unchanged:

The method avoids overestimating spreads when price changes are minimal. This is meant for addressing key weaknesses in prior models.

Dalio: “Stocks Only Look Strong in Dollar Terms.” Here’s a Globally Priced Alternative for Diversification.

Ray Dalio recently reported that much of the S&P 500’s 2025 gains came not from real growth, but from the dollar quietly losing value. Reportedly down 10% last year!

He’s not alone. Several BlackRock, Fidelity, and Bloomberg analysts say to expect further dollar decline in 2026.

So, even when your U.S. assets look “up,” your purchasing power may actually be down.

Which is why many investors are adding globally priced, scarce assets to their portfolios—like art.

Art is traded on a global stage, making it largely resistant to currency swings.

Now, Masterworks is opening access to invest in artworks featuring legends like Banksy, Basquiat, and Picasso as a low-correlation asset class with attractive appreciation historically (1995-2025).*

Masterworks’ 26 sales have yielded annualized net returns like 14.6%, 17.6%, and 17.8%.

They handle the sourcing, storage, and sale. You just click to invest.

Special offer for my subscribers:

*Based on Masterworks data. Investing involves risk. Past performance is not indicative of future returns. Important Reg A disclosures: masterworks.com/cd.

How the Rolling Estimator Works

The estimator improves bid-ask spread measurement by (i) leveraging simply OHLC prices and (ii) a rolling window to track liquidity trends dynamically.

The spread estimate is computed as:

ηt is the midpoint log-price, and τt​ accounts for price variation.

Key advantages:

  • GMM weighting minimizes variance:

  • Rolling updates ensure time-varying accuracy.

  • Non-negativity is enforced as (resets negative estimates to zero):

The Ardia et al. (2024) method outperforms traditional estimators, especially in low-liquidity markets.

3. Implementation in Python

The implementation below borrows its core logic from the original repository by Ardia et al. (2024).

3.1 Rolling Estimator Function

This function computes rolling bid-ask spread estimates using OHLC prices.

The main implementation function is defined below.

import numpy as np
import pandas as pd
import warnings
import warnings
warnings.filterwarnings('ignore')

def edge_rolling(data: pd.DataFrame, window: int, sign: bool = False, **kwargs) -> pd.Series:
    """
    Compute rolling bid-ask spread estimates from OHLC prices.

    This function uses a rolling window to estimate the bid-ask spread as described in
    Ardia, Guidotti, & Kroencke (2024). A value of 0.01 means a 1% spread.

    Parameters:
        data : pd.DataFrame
            DataFrame with columns 'open', 'high', 'low', and 'close' (case-insensitive).
        window : int or valid rolling window parameter
            Size of the moving window.
        sign : bool, default False
            If True, the returned spread keeps its sign.
        kwargs : dict
            Extra parameters for the pandas rolling function.

    Returns:
        pd.Series: Rolling spread estimates.
    """
    # Standardize column names and take log-prices.
    df = data.rename(columns=str.lower, inplace=False)
    log_open  = np.log(df['open'])
    log_high  = np.log(df['high'])
    log_low   = np.log(df['low'])
    log_close = np.log(df['close'])
    log_mid   = (log_high + log_low) / 2.0

    # Create lagged series for previous period prices.
    log_high_prev  = log_high.shift(1)
    log_low_prev   = log_low.shift(1)
    log_close_prev = log_close.shift(1)
    log_mid_prev   = log_mid.shift(1)

    # Compute various log-returns.
    r1 = log_mid - log_open        # mid - open
    r2 = log_open - log_mid_prev     # open - previous mid
    r3 = log_mid - log_close_prev    # mid - previous close
    r4 = log_close_prev - log_mid_prev  # previous close - previous mid
    r5 = log_open - log_close_prev   # open - previous close

    # Create an indicator for non-flat periods.
    tau = np.where(
        np.isnan(log_high) | np.isnan(log_low) | np.isnan(log_close_prev),
        np.nan,
        (log_high != log_low) | (log_low != log_close_prev)
    )
    # Indicators to check if open or previous close differ from high/low.
    ind_o_h = tau * np.where(np.isnan(log_open) | np.isnan(log_high), np.nan, log_open != log_high)
    ind_o_l = tau * np.where(np.isnan(log_open) | np.isnan(log_low), np.nan, log_open != log_low)
    ind_c_h = tau * np.where(np.isnan(log_close_prev) | np.isnan(log_high_prev), np.nan, log_close_prev != log_high_prev)
    ind_c_l = tau * np.where(np.isnan(log_close_prev) | np.isnan(log_low_prev), np.nan, log_close_prev != log_low_prev)

    # Calculate products of returns that will be used in moment conditions.
    prod_12 = r1 * r2
    prod_34 = r3 * r4
    prod_15 = r1 * r5
    prod_45 = r4 * r5
    tau_r1  = tau * r1
    tau_r2  = tau * r2
    tau_r4  = tau * r4
    tau_r5  = tau * r5

    # Collect all intermediate values in a DataFrame.
    vals = pd.DataFrame({
        'prod_12': prod_12,
        'prod_34': prod_34,
        'prod_15': prod_15,
        'prod_45': prod_45,
        'tau': tau,
        'r1': r1,
        'tau_r2': tau_r2,
        'r3': r3,
        'tau_r4': tau_r4,
        'r5': r5,
        'prod_12_sq': prod_12 ** 2,
        'prod_34_sq': prod_34 ** 2,
        'prod_15_sq': prod_15 ** 2,
        'prod_45_sq': prod_45 ** 2,
        'prod_12_34': prod_12 * prod_34,
        'prod_15_45': prod_15 * prod_45,
        'tau_r2_r2': tau_r2 * r2,
        'tau_r4_r4': tau_r4 * r4,
        'tau_r5_r5': tau_r5 * r5,
        'tau_r2_prod12': tau_r2 * prod_12,
        'tau_r4_prod34': tau_r4 * prod_34,
        'tau_r5_prod15': tau_r5 * prod_15,
        'tau_r4_prod45': tau_r4 * prod_45,
        'tau_r4_prod12': tau_r4 * prod_12,
        'tau_r2_prod34': tau_r2 * prod_34,
        'tau_r2_r4': tau_r2 * r4,
        'tau_r1_prod45': tau_r1 * prod_45,
        'tau_r5_prod45': tau_r5 * prod_45,
        'tau_r4_r5': tau_r4 * r5,
        'tau_r5_only': tau_r5,
        'ind_o_h': ind_o_h,
        'ind_o_l': ind_o_l,
        'ind_c_h': ind_c_h,
        'ind_c_l': ind_c_l
    }, index=df.index)

    # The first observation is not usable (due to shifting).
    vals.iloc[0] = np.nan

    # Adjust window length and min_periods to account for the lag.
    window_adj = window - 1 if isinstance(window, (int, np.integer)) else window
    if 'min_periods' in kwargs and isinstance(kwargs['min_periods'], (int, np.integer)):
        kwargs['min_periods'] = max(0, kwargs['min_periods'] - 1)

    # Compute rolling means for each column.
    roll_vals = vals.rolling(window=window_adj, **kwargs).mean()

    # Calculate probabilities needed for the estimator.
    p_tau = roll_vals['tau']
    p_open = roll_vals['ind_o_h'] + roll_vals['ind_o_l']
    p_close = roll_vals['ind_c_h'] + roll_vals['ind_c_l']

    # Count valid tau observations.
    count_tau = vals['tau'].rolling(window=window_adj, **kwargs).sum()
    # Mark window as missing if there are fewer than 2 valid periods or zero probabilities.
    roll_vals[(count_tau < 2) | (p_open == 0) | (p_close == 0)] = np.nan

    # Compute coefficients from the rolling means.
    a1 = -4.0 / p_open
    a2 = -4.0 / p_close
    a3 = roll_vals['r1'] / p_tau
    a4 = roll_vals['tau_r4'] / p_tau
    a5 = roll_vals['r3'] / p_tau
    a6 = roll_vals['r5'] / p_tau

    a12 = 2 * a1 * a2
    a11 = a1 ** 2
    a22 = a2 ** 2
    a33 = a3 ** 2
    a55 = a5 ** 2
    a66 = a6 ** 2

    # Compute expectations from moment conditions.
    E1 = a1 * (roll_vals['prod_12'] - a3 * roll_vals['tau_r2']) + \
         a2 * (roll_vals['prod_34'] - a4 * roll_vals['r3'])
    E2 = a1 * (roll_vals['prod_15'] - a3 * roll_vals['tau_r5_only']) + \
         a2 * (roll_vals['prod_45'] - a4 * roll_vals['r5'])

    # Compute variances from the moments.
    V1 = - E1**2 + (
        a11 * (roll_vals['prod_12_sq'] - 2 * a3 * roll_vals['tau_r2_prod12'] + a33 * roll_vals['tau_r2_r2']) +
        a22 * (roll_vals['prod_34_sq'] - 2 * a5 * roll_vals['tau_r4_prod34'] + a55 * roll_vals['tau_r4_r4']) +
        a12 * (roll_vals['prod_12_34'] - a3 * roll_vals['tau_r2_prod34'] - a5 * roll_vals['tau_r4_prod12'] + a3 * a5 * roll_vals['tau_r2_r4'])
    )
    V2 = - E2**2 + (
        a11 * (roll_vals['prod_15_sq'] - 2 * a3 * roll_vals['tau_r5_prod15'] + a33 * roll_vals['tau_r5_r5']) +
        a22 * (roll_vals['prod_45_sq'] - 2 * a6 * roll_vals['tau_r4_prod45'] + a66 * roll_vals['tau_r4_r4']) +
        a12 * (roll_vals['prod_15_45'] - a3 * roll_vals['tau_r5_prod45'] - a6 * roll_vals['tau_r4_r5'] + a3 * a6 * roll_vals['tau_r4_r5'])
    )

    tot_var = V1 + V2
    # If variance is positive, use a weighted combination; otherwise, take a simple average.
    s2 = np.where(tot_var > 0, (V2 * E1 + V1 * E2) / tot_var, (E1 + E2) / 2.0)
    spread = np.sqrt(np.abs(s2))
    if sign:
        spread *= np.sign(s2)

    return pd.Series(spread, index=df.index)

3.2 Downloading OHCL Data

We obtain historical OHLC data from Yahoo Finance. We use AAPL as an example.

Analysts are encouraged to try the methodology on less liquid stocks.

import yfinance as yf
import pandas as pd

ticker = "AAPL"
data = yf.download(ticker, start="2015-01-01", end="2025-12-31", auto_adjust=True)

#print("Downloaded data:")
print(data.head())
#print("Columns:", data.columns)

# Check if the columns are a MultiIndex
if isinstance(data.columns, pd.MultiIndex):
    #print("MultiIndex detected.")
    #print("Level 0 values:", data.columns.get_level_values(0).unique())
    #print("Level 1 values:", data.columns.get_level_values(1).unique())
    # In the new structure, level 0 contains the actual field names.
    data.columns = data.columns.get_level_values(0)
    #print("Columns after resetting:", data.columns)

# Extract the OHLC data.
ohlc = data[['Open', 'High', 'Low', 'Close']]
#print("OHLC data:")
ohlc

Figure 1. Raw OHLC data for AAPL from 2015 to 2025 used as input for bid-ask spread estimation.

3.3 Calculate Rolling Spreads

Now that we have the OHLC data, we can apply the rolling spread estimator to compute bid-ask spread estimates over a moving window.

Here, we use a 20-day rolling window with a minimum of 10 periods:

# Ensure we have the required data
ohlc = data[['Open', 'High', 'Low', 'Close']]
volume = data['Volume']

# Compute rolling spread with a 20-day window
rolling_spreads = edge_rolling(ohlc, window=20, min_periods=10, sign=False)

rolling_spreads

Figure 2. Rolling bid-ask spread estimates computed using the OHLC-based rolling estimator. Initial values are NaN due to the rolling window.

3.4 Visualize Rolling Spread, Volume, Close Price, and Volatility

To better observe the relationship between spread, volume, closing price, and volatility, we plot them using Matplotlib.

This visualization helps identify spread fluctuations, how they relate to price movements, and whether volatility could be affect transaction costs.

import matplotlib.pyplot as plt
import matplotlib.dates as mdates

# Compute daily returns and rolling volatility (20-day window)
returns = ohlc['Close'].pct_change()
rolling_vol = returns.rolling(window=20).std()

# Compute upper and lower spread bands around close price
upper_band = ohlc['Close'] * (1 + rolling_spreads / 2)
lower_band = ohlc['Close'] * (1 - rolling_spreads / 2)

# Use dark theme
plt.style.use('dark_background')

fig, ax1 = plt.subplots(figsize=(26, 7))

# --- 1) Volume (Left y-axis) ---
ax1.bar(ohlc.index, volume, color='gray', alpha=0.2, label='Volume')
ax1.set_ylabel("Volume", color='gray')
ax1.tick_params(axis='y', colors='gray')

# --- 2) Close Price (Right y-axis) ---
ax2 = ax1.twinx()
ax2.plot(ohlc.index, ohlc['Close'], color='lime', linewidth=2, label='Close Price')
ax2.set_ylabel("Close Price", color='lime')
ax2.tick_params(axis='y', colors='lime')

# Spread bands on the same axis as Close Price
ax2.fill_between(ohlc.index, upper_band, lower_band, color='gray', alpha=0.3, label='Spread Band')

# --- 3) Rolling Volatility (Offset right y-axis) ---
ax3 = ax1.twinx()
ax3.spines["right"].set_position(("outward", 60))  # first offset on right
ax3.plot(rolling_vol.index, rolling_vol, color='orange', linestyle='dashed', linewidth=1, alpha = 0.3, label='Rolling Volatility')
ax3.set_ylabel("Rolling Volatility", color='orange')
ax3.tick_params(axis='y', colors='orange')

# --- 4) Rolling Spread (Further offset right y-axis) ---
ax4 = ax1.twinx()
ax4.spines["right"].set_position(("outward", 120))  # second offset on right, avoids overlap
ax4.plot(rolling_spreads.index, rolling_spreads, color='blue', linewidth=1.5, label='Rolling Spread')
ax4.set_ylabel("Rolling Spread", color='blue')
ax4.tick_params(axis='y', colors='blue')

# Format x-axis for readability
ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
ax1.xaxis.set_major_locator(mdates.YearLocator())

# Title
fig.suptitle("Rolling Spread, Volume, Close Price, and Rolling Volatility", fontsize=14)

# --- Legend placements (stacked in upper-left corner) ---
ax1.legend(loc="upper left", bbox_to_anchor=(0, 1))       # Volume
ax2.legend(loc="upper left", bbox_to_anchor=(0, 0.85))    # Close Price & Spread Band
ax3.legend(loc="upper left", bbox_to_anchor=(0, 0.7))     # Rolling Vol
ax4.legend(loc="upper left", bbox_to_anchor=(0, 0.55))    # Rolling Spread

plt.show()

Figure 3. Visualization of rolling spread (blue), volume (gray), closing price (green), and rolling volatility (orange). The chart highlights how spreads and volatility fluctuate over time.

3.5 Exploratory Analysis of Lagged Relationships

We now conduct a very preliminary exploratory analysis on the relationship between spreads, returns and volume.

We explore whether lagged spreads could affect returns, volume affects spreads, or volatility influences liquidity.

For this we conduct a simple lagged correlation analysis.

import matplotlib.pyplot as plt
import numpy as np

# Compute daily returns and rolling volatility (20-day window)
returns = ohlc['Close'].pct_change()
rolling_vol = returns.rolling(window=20).std()

# --- Set adjustable lag period ---
lag_period = 4  # Change this value to adjust the lag

# Create lagged series.
lagged_spreads = rolling_spreads.shift(lag_period)
lagged_volume = volume.shift(lag_period)
lagged_vol = rolling_vol.shift(lag_period)

# --- Helper function for labeling ---
def label_text(var_name, lag):
    return f"{var_name} (lag={lag})" if lag > 1 else f"Lagged {var_name}"

# Define subplot titles
titles = [
    f"Returns vs {label_text('Spreads', lag_period)}",
    f"Spreads vs {label_text('Volume', lag_period)}",
    f"Returns vs {label_text('Volume', lag_period)}",
    f"Volatility vs {label_text('Spreads', lag_period)}",
    f"{label_text('Volatility', lag_period)} vs Spreads"
]

# --- Prepare data for each plot ---
data_pairs = [
    (lagged_spreads, returns),      # Plot 1: Returns vs Lagged Spreads
    (lagged_volume, rolling_spreads),  # Plot 2: Spreads vs Lagged Volume
    (lagged_volume, returns),       # Plot 3: Returns vs Lagged Volume
    (lagged_spreads, rolling_vol),  # Plot 4: Rolling Volatility vs Lagged Spreads
    (lagged_vol, rolling_spreads)   # Plot 5: Lagged Volatility vs Current Spreads
]

# Colors for each plot
colors = ["blue", "red", "green", "orange", "magenta"]

# Create subplots: 1 row, 5 columns
fig, axes = plt.subplots(1, 5, figsize=(25, 6))
plt.style.use('dark_background')  # Dark theme

for i, ax in enumerate(axes):
    # Mask NaN values
    x, y = data_pairs[i]
    mask = ~np.isnan(x) & ~np.isnan(y)
    ax.scatter(x[mask], y[mask], color=colors[i], alpha=0.7, s=10)  # Scatter plot

    # Titles and labels
    ax.set_title(titles[i], fontsize=12)
    ax.set_xlabel(label_text("Spreads" if i in [0, 3] else "Volume", lag_period), fontsize=10)
    ax.set_ylabel("Returns" if i in [0, 2] else "Spreads" if i in [1, 4] else "Volatility", fontsize=10)

# Adjust layout for better readability
plt.tight_layout()
plt.suptitle(f"Lagged Value Analysis (lag={lag_period})", fontsize=14, y=1.02)

plt.show()

Figure 4. Lagged value analysis showing relationships between spreads, returns, volume, and volatility. Scatter plots suggest weak correlation between spreads and returns but stronger ties between spreads and volatility.

We observe a relationship between volatility and spreads. Higher volatility generally leads to wider spreads, as expected.

However, this is a basic observation, not a full analysis. The goal of this article is to estimate spreads, not interpret their impact.

Analysts should conduct deeper research to explore causality and market dynamics.

3.6 Lagged Correlations Over Time

Recently, lagged volatility appears to be the strongest driver of spreads, showing a close correlation.

Historically, volume played a key role, but in 2025, its impact seems to have weakened.

import matplotlib.pyplot as plt
import pandas as pd

# Set parameters
window_corr = 60  # Rolling window for correlation analysis
lag_period = 1    # Adjust this value to set the lag

# Compute lagged series
lagged_volume = volume.shift(lag_period)
lagged_vol = rolling_vol.shift(lag_period)

# Compute rolling correlations using lagged values
rolling_corr_volume_spread = lagged_volume.rolling(window=window_corr).corr(rolling_spreads)
rolling_corr_volatility_spread = lagged_vol.rolling(window=window_corr).corr(rolling_spreads)

# Use dark theme
plt.style.use('dark_background')

# Create figure
fig, ax = plt.subplots(figsize=(25, 6))

# Plot rolling correlation (lagged volume vs spread)
ax.plot(rolling_corr_volume_spread.index, rolling_corr_volume_spread, color='red', linewidth=2,
        label=f"Rolling Corr (Lagged Volume vs Spread, lag={lag_period}, window={window_corr})")

# Plot rolling correlation (lagged volatility vs spread)
ax.plot(rolling_corr_volatility_spread.index, rolling_corr_volatility_spread, color='orange', linewidth=2,
        label=f"Rolling Corr (Lagged Volatility vs Spread, lag={lag_period}, window={window_corr})")

# Labels and title
ax.set_title(f"Rolling Correlations with Lag (Window = {window_corr}, Lag = {lag_period})", fontsize=14)
ax.set_xlabel("Date")
ax.set_ylabel("Correlation")

# Add legend
ax.legend(loc="upper left")

# Improve x-axis readability
plt.xticks(rotation=45)

# Show plot
plt.show()

Figure 5. Rolling correlations of lagged volume and volatility against spreads. Volatility shows a stronger, more consistent relationship with spreads over time.

4. Further Applications

Bid-ask spread estimation has more applications across trading, liquidity analysis, and risk management. For example,

  • Trading Strategy Optimization: Reduce execution costs by avoiding high-spread periods.

  • Market Liquidity Analysis: Identify liquidity shifts due to macro events and market stress.

  • Risk Management: Acts as an early warning for volatility and liquidity risks.

  • High-Frequency Trading & Market Making: Enhances pricing models and inventory management.

  • Regulatory & Academic Research: Improves studies on market efficiency and transaction costs.

Concluding Thoughts

The OHLC-based rolling estimator provides a simple and reliable way to capture price discreteness and market shifts.

This article focused on the how to implement it, but there’s plenty more to explore.

logo

Subscribe to our premium content to read the rest.

Become a paying subscriber to get access to this post and other subscriber-only content.

Upgrade