Market Microstructure·Order Book Analysis·Advanced

Bid Ask Spread Tracker

Track bid-ask spreads in real time across multiple exchanges and trading pairs, analyzing quoted and effective spread patterns by time of day, volatility regime, and scheduled market events for optimal execution cost minimization and venue selection.

market-microstructureorder-book

Bid-Ask Spread Tracker: A Comprehensive Guide

Definition and Importance of Bid-Ask Spread

The bid-ask spread is the difference between the highest price a buyer is willing to pay for an asset (the 'bid' price) and the lowest price a seller is willing to accept (the 'ask' or 'offer' price). It represents the cost of executing a transaction immediately and is a key measure of market liquidity.

Importance:

  • Cost of Trading: The spread is a direct cost to traders. Buyers pay the ask price, sellers receive the bid price, and the difference goes to the market maker or exchange as profit.
  • Market Liquidity: A narrow spread typically indicates a highly liquid market with many buyers and sellers, meaning orders can be filled quickly without significantly impacting the price. A wide spread suggests lower liquidity.
  • Volatility Indicator: Spreads often widen during periods of high market volatility or uncertainty, as market makers demand greater compensation for the increased risk.
  • Market Efficiency: Efficient markets tend to have narrower spreads as information is quickly disseminated and incorporated into prices.
  • Profit for Market Makers: For market makers, the bid-ask spread is their primary source of profit. They aim to buy at the bid and sell at the ask, capturing the spread.

Types of Bid-Ask Spread

1. Nominal (Quoted) Spread

Definition: The nominal spread is the simplest form, representing the direct difference between the best ask and best bid prices available in the market at a given moment.

Formula: Nominal Spread = Ask Price - Bid Price

[ ]
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Creating data with variance in both price and spread
np.random.seed(42)
num_data_points = 50
timestamp = pd.to_datetime(pd.date_range(start='2023-01-01', periods=num_data_points, freq='1min'))

# Base linear trend plus random noise for variance
noise = np.random.normal(0, 0.2, num_data_points)
base_trend = np.linspace(100.00, 105.00, num_data_points) + noise

bid_prices = np.round(base_trend, 2)

# Introduce variance into the spread itself (centered around 0.9)
spread_variance = np.random.normal(0.9, 0.05, num_data_points)
ask_prices = np.round(bid_prices + spread_variance, 2)

data = pd.DataFrame({
    'timestamp': timestamp,
    'bid': bid_prices,
    'ask': ask_prices
})

def calculate_nominal_spread(bid_price: float, ask_price: float) -> float:
    return ask_price - bid_price

# Apply calculations
data['nominal_spread'] = data.apply(lambda row: calculate_nominal_spread(row['bid'], row['ask']), axis=1)

print("--- Data Generated with Price & Spread Variance ---")
display(data.head())
--- Data Generated with Price & Spread Variance ---
timestamp bid ask nominal_spread
0 2023-01-01 00:00:00 100.10 101.02 0.92
1 2023-01-01 00:01:00 100.07 100.95 0.88
2 2023-01-01 00:02:00 100.33 101.20 0.87
3 2023-01-01 00:03:00 100.61 101.54 0.93
4 2023-01-01 00:04:00 100.36 101.31 0.95

2. Percentage Spread

Definition: The percentage spread normalizes the nominal spread by expressing it as a percentage of the midpoint price (average of bid and ask). This allows for easier comparison of liquidity across assets with different price levels.

Formula: Midpoint Price = (Bid Price + Ask Price) / 2 Percentage Spread = (Nominal Spread / Midpoint Price) * 100

[ ]
def calculate_percentage_spread(bid_price: float, ask_price: float) -> float:
    midpoint_price = (bid_price + ask_price) / 2
    nominal_spread = ask_price - bid_price
    if midpoint_price == 0:
        return 0.0
    return (nominal_spread / midpoint_price) * 100

data['percentage_spread'] = data.apply(lambda row: calculate_percentage_spread(row['bid'], row['ask']), axis=1)
display(data.head())
timestamp bid ask nominal_spread percentage_spread
0 2023-01-01 00:00:00 100.10 101.02 0.92 0.914877
1 2023-01-01 00:01:00 100.07 100.95 0.88 0.875535
2 2023-01-01 00:02:00 100.33 101.20 0.87 0.863395
3 2023-01-01 00:03:00 100.61 101.54 0.93 0.920109
4 2023-01-01 00:04:00 100.36 101.31 0.95 0.942133

3. Effective Spread

Definition: The effective spread measures the actual cost of trading by comparing the trade execution price to the midpoint of the bid and ask prices at the time the order was placed. It accounts for orders that might be filled inside the quoted spread (price improvement) or outside due to market impact.

Formula:

  • For a buy order: Effective Spread = 2 * (Trade Price - Midpoint Price at Order Entry)
  • For a sell order: Effective Spread = 2 * (Midpoint Price at Order Entry - Trade Price)
[ ]
np.random.seed(43)
trade_prices = []
trade_types = []

for i in range(len(data)):
    mid_price = (data.loc[i, 'bid'] + data.loc[i, 'ask']) / 2
    if np.random.rand() > 0.5:
        trade_type = 'buy'
        trade_price = np.round(np.random.uniform(mid_price, data.loc[i, 'ask']), 2)
    else:
        trade_type = 'sell'
        trade_price = np.round(np.random.uniform(data.loc[i, 'bid'], mid_price), 2)
    trade_prices.append(trade_price)
    trade_types.append(trade_type)

data['trade_price'] = trade_prices
data['trade_type'] = trade_types

def calculate_effective_spread(bid_price_at_order: float, ask_price_at_order: float, trade_price: float, trade_type: str) -> float:
    midpoint_at_order = (bid_price_at_order + ask_price_at_order) / 2
    if trade_type == 'buy':
        return 2 * (trade_price - midpoint_at_order)
    elif trade_type == 'sell':
        return 2 * (midpoint_at_order - trade_price)
    return 0.0

data['effective_spread'] = data.apply(lambda row: calculate_effective_spread(row['bid'], row['ask'], row['trade_price'], row['trade_type']), axis=1)
display(data.head())
timestamp bid ask nominal_spread percentage_spread trade_price trade_type effective_spread
0 2023-01-01 00:00:00 100.10 101.02 0.92 0.914877 100.38 sell 0.36
1 2023-01-01 00:01:00 100.07 100.95 0.88 0.875535 100.18 sell 0.66
2 2023-01-01 00:02:00 100.33 101.20 0.87 0.863395 100.70 sell 0.13
3 2023-01-01 00:03:00 100.61 101.54 0.93 0.920109 101.33 buy 0.51
4 2023-01-01 00:04:00 100.36 101.31 0.95 0.942133 100.71 sell 0.25
[ ]
np.random.seed(43)

# Mock trade data, assuming some price improvement or slippage
trade_prices = []
trade_types = []

for i in range(num_data_points):
    mid_price = (data.loc[i, 'bid'] + data.loc[i, 'ask']) / 2
    if np.random.rand() > 0.5: # Simulate a buy order
        trade_type = 'buy'
        # Trade price slightly above mid, sometimes slightly below ask
        trade_price = np.round(np.random.uniform(mid_price, data.loc[i, 'ask']), 2)
    else: # Simulate a sell order
        trade_type = 'sell'
        # Trade price slightly below mid, sometimes slightly above bid
        trade_price = np.round(np.random.uniform(data.loc[i, 'bid'], mid_price), 2)
    trade_prices.append(trade_price)
    trade_types.append(trade_type)

data['trade_price'] = trade_prices
data['trade_type'] = trade_types

def calculate_effective_spread(bid_price_at_order: float, ask_price_at_order: float, trade_price: float, trade_type: str) -> float:
    """
    Calculates the effective bid-ask spread.

    Args:
        bid_price_at_order (float): Bid price at the time the order was placed.
        ask_price_at_order (float): Ask price at the time the order was placed.
        trade_price (float): The actual execution price of the trade.
        trade_type (str): Type of the trade, either 'buy' or 'sell'.

    Returns:
        float: The effective bid-ask spread.
    """
    midpoint_at_order = (bid_price_at_order + ask_price_at_order) / 2
    if trade_type == 'buy':
        return 2 * (trade_price - midpoint_at_order)
    elif trade_type == 'sell':
        return 2 * (midpoint_at_order - trade_price)
    else:
        raise ValueError("Trade type must be 'buy' or 'sell'.")

# Apply the function to mock data
data['effective_spread'] = data.apply(lambda row:
    calculate_effective_spread(row['bid'], row['ask'], row['trade_price'], row['trade_type']),
    axis=1
)

print("--- Effective Spread Calculation ---")
display(data.head())
print("\nInterpretation: The effective spread provides a more accurate picture of transaction costs by comparing the actual trade price to the mid-price at the time of the order. A negative effective spread for a buy order would indicate a highly favorable execution (bought below mid-price).")
--- Effective Spread Calculation ---
timestamp bid ask nominal_spread percentage_spread trade_price trade_type effective_spread
0 2023-01-01 00:00:00 101.49 101.69 0.20 0.196870 101.55 sell 0.08
1 2023-01-01 00:01:00 103.49 103.66 0.17 0.164132 103.51 sell 0.13
2 2023-01-01 00:02:00 105.49 105.68 0.19 0.179950 105.57 sell 0.03
3 2023-01-01 00:03:00 107.49 107.67 0.18 0.167317 107.63 buy 0.10
4 2023-01-01 00:04:00 109.48 109.62 0.14 0.127796 109.53 sell 0.04

Interpretation: The effective spread provides a more accurate picture of transaction costs by comparing the actual trade price to the mid-price at the time of the order. A negative effective spread for a buy order would indicate a highly favorable execution (bought below mid-price).
[ ]
np.random.seed(44)

# Simulate post-trade midpoint price (e.g., 5 minutes after the trade)
# For simplicity, we'll shift the mid_price or add some noise
post_trade_mid_prices = []
for i in range(num_data_points):
    current_mid = (data.loc[i, 'bid'] + data.loc[i, 'ask']) / 2
    # Simulate a slight random walk for post-trade mid price
    post_trade_mid = current_mid + np.random.uniform(-0.02, 0.02)
    post_trade_mid_prices.append(np.round(post_trade_mid, 2))

data['post_trade_mid_price'] = post_trade_mid_prices

def calculate_realized_spread(trade_price: float, post_trade_mid_price: float, trade_type: str) -> float:
    """
    Calculates the realized spread from the perspective of the trade initiator.

    Args:
        trade_price (float): The actual execution price of the trade.
        post_trade_mid_price (float): The midpoint price a short period after the trade (e.g., T+5).
        trade_type (str): Type of the trade, either 'buy' or 'sell'.

    Returns:
        float: The realized spread (from trader's perspective).
    """
    if trade_type == 'buy':
        return trade_price - post_trade_mid_price
    elif trade_type == 'sell':
        return post_trade_mid_price - trade_price
    else:
        raise ValueError("Trade type must be 'buy' or 'sell'.")

# Apply the function to mock data
data['realized_spread'] = data.apply(lambda row:
    calculate_realized_spread(row['trade_price'], row['post_trade_mid_price'], row['trade_type']),
    axis=1
)

print("--- Realized Spread Calculation ---")
display(data.head())
print("\nInterpretation: The realized spread quantifies the adverse selection component. A negative realized spread for a buy order indicates that the mid-price rose after your purchase, meaning you paid less relative to the future market price. A positive value means you paid more relative to the future market price.")
--- Realized Spread Calculation ---
timestamp bid ask nominal_spread percentage_spread trade_price trade_type effective_spread post_trade_mid_price realized_spread
0 2023-01-01 00:00:00 101.49 101.69 0.20 0.196870 101.55 sell 0.08 101.60 0.05
1 2023-01-01 00:01:00 103.49 103.66 0.17 0.164132 103.51 sell 0.13 103.56 0.05
2 2023-01-01 00:02:00 105.49 105.68 0.19 0.179950 105.57 sell 0.03 105.59 0.02
3 2023-01-01 00:03:00 107.49 107.67 0.18 0.167317 107.63 buy 0.10 107.57 0.06
4 2023-01-01 00:04:00 109.48 109.62 0.14 0.127796 109.53 sell 0.04 109.54 0.01

Interpretation: The realized spread quantifies the adverse selection component. A negative realized spread for a buy order indicates that the mid-price rose after your purchase, meaning you paid less relative to the future market price. A positive value means you paid more relative to the future market price.
[ ]
fig, axes = plt.subplots(1, 2, figsize=(16, 6))

# Plot 1: Distribution of Effective Spread
sns.histplot(data['effective_spread'], kde=True, ax=axes[0], color='blue', label='Effective Spread')
axes[0].axvline(data['effective_spread'].mean(), color='red', linestyle='--', label=f'Mean: {data["effective_spread"].mean():.4f}')
axes[0].set_title('Distribution of Effective Spread')
axes[0].set_xlabel('Effective Spread Value')
axes[0].set_ylabel('Frequency / Density')
axes[0].legend()
axes[0].grid(True, linestyle='--', alpha=0.7);

# Plot 2: Distribution of Realized Spread
sns.histplot(data['realized_spread'], kde=True, ax=axes[1], color='green', label='Realized Spread')
axes[1].axvline(data['realized_spread'].mean(), color='purple', linestyle='--', label=f'Mean: {data["realized_spread"].mean():.4f}')
axes[1].set_title('Distribution of Realized Spread')
axes[1].set_xlabel('Realized Spread Value')
axes[1].set_ylabel('Frequency / Density')
axes[1].legend()
axes[1].grid(True, linestyle='--', alpha=0.7);

plt.tight_layout()
plt.show()

print("Interpretation:")
print("The first histogram shows the distribution of the effective spread. A distribution centered near zero or slightly positive suggests efficient execution costs. A longer tail on the positive side indicates trades that incurred higher-than-expected costs relative to the mid-price at order entry. The mean effective spread helps quantify the average transaction cost incurred by traders.")
print("\nThe second histogram displays the distribution of the realized spread. This distribution's mean and shape reveal how often trades were subject to adverse selection (market moving against the trader post-trade) or benefited from favorable price movements. A mean near zero suggests market makers are generally able to capture the spread without significant losses to adverse selection, or that traders' profits/losses from post-trade price movements balance out. Negative values for a buyer's realized spread (meaning the post-trade mid-price was higher than their trade price) indicate a 'good' trade from the perspective of beating future market movement.")
cell output
Interpretation:
The first histogram shows the distribution of the effective spread. A distribution centered near zero or slightly positive suggests efficient execution costs. A longer tail on the positive side indicates trades that incurred higher-than-expected costs relative to the mid-price at order entry. The mean effective spread helps quantify the average transaction cost incurred by traders.

The second histogram displays the distribution of the realized spread. This distribution's mean and shape reveal how often trades were subject to adverse selection (market moving against the trader post-trade) or benefited from favorable price movements. A mean near zero suggests market makers are generally able to capture the spread without significant losses to adverse selection, or that traders' profits/losses from post-trade price movements balance out. Negative values for a buyer's realized spread (meaning the post-trade mid-price was higher than their trade price) indicate a 'good' trade from the perspective of beating future market movement.

4. Realized Spread

Definition: The realized spread measures the profit or loss made by a market maker from a trade, considering how the midpoint price moves after the trade. It captures the impact of adverse selection (when a market maker trades with someone who has better information).

Formula:

  • For a buy order executed by market maker (you sold): Realized Spread = Midpoint Price T+5 - Trade Price
  • For a sell order executed by market maker (you bought): Realized Spread = Trade Price - Midpoint Price T+5

(Note: T+5 typically refers to 5 minutes after the trade, but can be any short-term future point.)

For our purposes, we'll calculate it from the perspective of the trader who initiated the trade:

  • For a buy order: Realized Spread (Trader) = Trade Price - Midpoint Price T+5
  • For a sell order: Realized Spread (Trader) = Midpoint Price T+5 - Trade Price

A positive realized spread for the trader means the market moved favorably after their trade, effectively making their transaction cost lower than initially perceived. A negative value suggests the market moved against them, indicating a higher true cost.

[ ]
np.random.seed(44)
post_trade_mid_prices = []
for i in range(len(data)):
    current_mid = (data.loc[i, 'bid'] + data.loc[i, 'ask']) / 2
    post_trade_mid = current_mid + np.random.uniform(-0.1, 0.1)
    post_trade_mid_prices.append(np.round(post_trade_mid, 2))

data['post_trade_mid_price'] = post_trade_mid_prices

def calculate_realized_spread(trade_price: float, post_trade_mid_price: float, trade_type: str) -> float:
    if trade_type == 'buy':
        return trade_price - post_trade_mid_price
    elif trade_type == 'sell':
        return post_trade_mid_price - trade_price
    return 0.0

data['realized_spread'] = data.apply(lambda row: calculate_realized_spread(row['trade_price'], row['post_trade_mid_price'], row['trade_type']), axis=1)
display(data.head())
timestamp bid ask nominal_spread percentage_spread trade_price trade_type effective_spread post_trade_mid_price realized_spread
0 2023-01-01 00:00:00 100.10 101.02 0.92 0.914877 100.38 sell 0.36 100.63 0.25
1 2023-01-01 00:01:00 100.07 100.95 0.88 0.875535 100.18 sell 0.66 100.43 0.25
2 2023-01-01 00:02:00 100.33 101.20 0.87 0.863395 100.70 sell 0.13 100.81 0.11
3 2023-01-01 00:03:00 100.61 101.54 0.93 0.920109 101.33 buy 0.51 101.05 0.28
4 2023-01-01 00:04:00 100.36 101.31 0.95 0.942133 100.71 sell 0.25 100.81 0.10

Visualizations

1. Bid, Ask, Mid-Price, and Nominal Spread Over Time

This multi-panel plot illustrates the movement of bid, ask, and mid-prices, along with the nominal spread, providing an immediate visual understanding of price dynamics and liquidity changes.

[ ]
data['mid_price'] = (data['bid'] + data['ask']) / 2
fig, axes = plt.subplots(3, 1, figsize=(15, 14), sharex=True)

# Panel 1
axes[0].plot(data['timestamp'], data['bid'], label='Bid Price', color='red', linewidth=1, marker='o', markersize=2)
axes[0].plot(data['timestamp'], data['ask'], label='Ask Price', color='green', linewidth=1, marker='x', markersize=2)
axes[0].plot(data['timestamp'], data['mid_price'], label='Mid Price', color='blue', linestyle='--')
axes[0].fill_between(data['timestamp'], data['bid'], data['ask'], color='yellow', alpha=0.2)
axes[0].set_title('Bid, Ask, and Mid Prices (Non-linear with variable spread)')
axes[0].set_ylabel('Price')
axes[0].legend(loc='upper left')
axes[0].grid(True, linestyle='--', alpha=0.5)

# Panel 2: Nominal Spread with Variance
axes[1].plot(data['timestamp'], data['nominal_spread'], label='Nominal Spread', color='purple', linewidth=1.5, marker='s', markersize=3)
axes[1].set_ylim(0.5, 1.3)
axes[1].set_title('Nominal Bid-Ask Spread (Variable around 0.9)')
axes[1].set_ylabel('Spread Value')
axes[1].legend()
axes[1].grid(True, linestyle='--', alpha=0.7)

# Panel 3
axes[2].plot(data['timestamp'], data['percentage_spread'], label='Percentage Spread (%)', color='brown')
axes[2].set_title('Percentage Bid-Ask Spread')
axes[2].set_xlabel('Time')
axes[2].set_ylabel('Spread (%)')
axes[2].legend()
axes[2].grid(True, linestyle='--', alpha=0.7)

plt.tight_layout()
plt.show()
cell output

2. Distribution of Effective and Realized Spreads

This plot uses histograms and kernel density estimates (KDE) to show the distribution of effective and realized spreads. It helps in understanding the common transaction costs and the impact of post-trade price movements, including potential price improvement or adverse selection.

[ ]
fig, axes = plt.subplots(1, 2, figsize=(16, 6))

# Plot 1: Distribution of Effective Spread
sns.histplot(data['effective_spread'], kde=True, ax=axes[0], color='blue', label='Effective Spread')
axes[0].axvline(data['effective_spread'].mean(), color='red', linestyle='--', label=f'Mean: {data["effective_spread"].mean():.4f}')
axes[0].set_title('Distribution of Effective Spread')
axes[0].set_xlabel('Effective Spread Value')
axes[0].set_ylabel('Frequency / Density')
axes[0].legend()
axes[0].grid(True, linestyle='--', alpha=0.7);

# Plot 2: Distribution of Realized Spread
sns.histplot(data['realized_spread'], kde=True, ax=axes[1], color='green', label='Realized Spread')
axes[1].axvline(data['realized_spread'].mean(), color='purple', linestyle='--', label=f'Mean: {data["realized_spread"].mean():.4f}')
axes[1].set_title('Distribution of Realized Spread')
axes[1].set_xlabel('Realized Spread Value')
axes[1].set_ylabel('Frequency / Density')
axes[1].legend()
axes[1].grid(True, linestyle='--', alpha=0.7);

plt.tight_layout()
plt.show()

print("Interpretation:")
print("The first histogram shows the distribution of the effective spread. A distribution centered near zero or slightly positive suggests efficient execution costs. A longer tail on the positive side indicates trades that incurred higher-than-expected costs relative to the mid-price at order entry. The mean effective spread helps quantify the average transaction cost incurred by traders.")
print("\nThe second histogram displays the distribution of the realized spread. This distribution's mean and shape reveal how often trades were subject to adverse selection (market moving against the trader post-trade) or benefited from favorable price movements. A mean near zero suggests market makers are generally able to capture the spread without significant losses to adverse selection, or that traders' profits/losses from post-trade price movements balance out. Negative values for a buyer's realized spread (meaning the post-trade mid-price was higher than their trade price) indicate a 'good' trade from the perspective of beating future market movement.")
cell output
Interpretation:
The first histogram shows the distribution of the effective spread. A distribution centered near zero or slightly positive suggests efficient execution costs. A longer tail on the positive side indicates trades that incurred higher-than-expected costs relative to the mid-price at order entry. The mean effective spread helps quantify the average transaction cost incurred by traders.

The second histogram displays the distribution of the realized spread. This distribution's mean and shape reveal how often trades were subject to adverse selection (market moving against the trader post-trade) or benefited from favorable price movements. A mean near zero suggests market makers are generally able to capture the spread without significant losses to adverse selection, or that traders' profits/losses from post-trade price movements balance out. Negative values for a buyer's realized spread (meaning the post-trade mid-price was higher than their trade price) indicate a 'good' trade from the perspective of beating future market movement.

Conclusion

This notebook has provided a comprehensive overview of bid-ask spreads, their various types, and their significance in financial markets. We've explored the following key concepts:

  • Definition and Importance: The bid-ask spread is a fundamental measure of market liquidity and a direct cost of trading, impacting efficiency and profitability for both traders and market makers.

  • Types of Spreads:

    • Nominal (Quoted) Spread: The basic difference between the highest bid and lowest ask, representing the instantaneous cost.
    • Percentage Spread: Normalizes the nominal spread by expressing it as a percentage of the midpoint, allowing for cross-asset comparisons.
    • Effective Spread: Measures the actual cost of a trade by comparing the execution price to the midpoint at the time of the order, accounting for price improvement or slippage.
    • Realized Spread: Quantifies the profit/loss from a trade relative to a future midpoint price, reflecting the impact of adverse selection or favorable post-trade price movements for the trader.
  • Visualizations: We've used various plots to illustrate:

    • The dynamic movement of bid, ask, and mid-prices over time.
    • The behavior of nominal and percentage spreads, highlighting their variability.
    • The distributions of effective and realized spreads, offering insights into transaction costs and the degree of adverse selection.

Understanding these different types of bid-ask spreads is crucial for traders to accurately assess transaction costs, evaluate execution quality, and make informed trading decisions. For market participants, these metrics provide valuable insights into market liquidity, efficiency, and the overall health of an asset's trading environment.