Market Making·Market Making Fundamentals·Intermediate

Spread Optimization

Dynamically optimize quoted bid-ask spread widths in real time based on a multi-factor model incorporating realized volatility, estimated order flow toxicity, competitor spread levels, and current inventory imbalance to maximize expected spread capture net of adverse selection costs.

market-makingoptimizationorder-book

Market Making: Dynamically Optimizing Bid-Ask Spreads

Introduction

Market making is a strategy where a trader simultaneously places both buy (bid) and sell (ask) limit orders to profit from the bid-ask spread. The core idea is to provide liquidity to the market and capture the difference between the price at which one can buy and the price at which one can sell.

Dynamically optimizing the bid-ask spread is crucial for a market maker's profitability and risk management. A narrow spread attracts more trades but increases the risk of adverse selection (trading against informed participants), while a wide spread reduces trade volume but also lowers adverse selection risk. The optimal spread often depends on various market conditions such as volatility, inventory imbalance, order book depth, and expected price movements.

This notebook will explore a simplified framework for dynamically adjusting bid-ask spreads based on inventory levels and market conditions. We'll implement core functions to simulate market dynamics, calculate spreads, manage inventory, and visualize the strategy's performance.

Key Concepts

ConceptDescription
Market MakingSimultaneously placing bid and ask orders to profit from the spread and provide liquidity.
Bid-Ask SpreadThe difference between the highest price a buyer is willing to pay (bid) and the lowest price a seller is willing to accept (ask).
Inventory ManagementManaging the quantity of assets held. Imbalanced inventory can lead to increased risk.
Adverse SelectionThe risk that a market maker trades with an informed trader who has superior information, leading to losses.
VolatilityThe degree of variation of a trading price series over time, impacting optimal spread size.
Order Book DepthThe quantity of buy and sell orders at various price levels, indicating market liquidity.
Dynamic OptimizationAdjusting parameters (like spread) in real-time based on changing market conditions.
PNL (Profit and Loss)The financial gain or loss of the market making strategy.

Dependency Installation

We'll install loguru for robust logging and tqdm for progress bars during simulations.

[ ]
pip install numpy pandas scipy matplotlib seaborn loguru tqdm
Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2)
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2)
Requirement already satisfied: scipy in /usr/local/lib/python3.12/dist-packages (1.16.3)
Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/dist-packages (3.10.0)
Requirement already satisfied: seaborn in /usr/local/lib/python3.12/dist-packages (0.13.2)
Collecting loguru
  Downloading loguru-0.7.3-py3-none-any.whl.metadata (22 kB)
Requirement already satisfied: tqdm in /usr/local/lib/python3.12/dist-packages (4.67.3)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.2)
Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (4.63.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.5.0)
Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (26.2)
Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (11.3.0)
Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (3.3.2)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
Downloading loguru-0.7.3-py3-none-any.whl (61 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 61.6/61.6 kB 5.2 MB/s eta 0:00:00
[?25hInstalling collected packages: loguru
Successfully installed loguru-0.7.3

Library Imports

All necessary libraries are imported here, organized by standard and third-party.

[ ]
# Standard library imports
import collections
import math
import random
import time
import logging

# Third-party imports
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.stats import norm
from loguru import logger
from tqdm.notebook import tqdm

# Configure loguru logger
logger.remove()
logger.add(lambda msg: logging.info(msg.strip()), level="INFO")
logger.add(lambda msg: logging.debug(msg.strip()), level="DEBUG")
logger.add(lambda msg: logging.warning(msg.strip()), level="WARNING")
3

Core Functions

This section defines the core functions for our dynamic bid-ask spread optimization strategy. Each function is in its own block, with a markdown header explaining its purpose, algorithm, parameters, and expected returns.

Function Name: create_market_state

This function initializes the market maker's state, including current inventory, cash balance, initial mid-price, and other parameters crucial for the simulation. It sets up the initial conditions for the market making strategy.

Parameters:

  • initial_cash (float): The starting cash balance for the market maker.
  • initial_inventory (int): The starting inventory of the asset.
  • initial_mid_price (float): The initial mid-price of the asset.
  • tick_size (float): The minimum price increment.
  • max_inventory (int): The maximum allowed inventory for the asset.
  • min_inventory (int): The minimum allowed inventory (can be negative for short positions).
  • risk_aversion (float): A parameter reflecting the market maker's aversion to inventory risk. Higher values mean more aggressive spread adjustment for inventory.
  • volatility (float): The assumed market volatility for spread calculation.
  • kappa (float): A parameter related to order book resilience/arrival rate in spread models.
  • lambda_val (float): A parameter related to order book depth/intensity in spread models.
  • order_book_impact (float): How much order book depth or imbalance influences the spread.

Returns:

  • dict: An initialized state dictionary containing all market maker parameters.
[ ]
def create_market_state(
    initial_cash: float,
    initial_inventory: int,
    initial_mid_price: float,
    tick_size: float = 0.01,
    max_inventory: int = 100,
    min_inventory: int = -100,
    risk_aversion: float = 0.001,
    volatility: float = 0.01,
    kappa: float = 0.01,
    lambda_val: float = 1.0,
    order_book_impact: float = 0.05
) -> dict:
    """
    Initializes the market maker's state dictionary.

    Parameters
    ----------
    initial_cash : float
        The starting cash balance for the market maker.
    initial_inventory : int
        The starting inventory of the asset.
    initial_mid_price : float
        The initial mid-price of the asset.
    tick_size : float, optional
        The minimum price increment, defaults to 0.01.
    max_inventory : int, optional
        The maximum allowed inventory for the asset, defaults to 100.
    min_inventory : int, optional
        The minimum allowed inventory, defaults to -100.
    risk_aversion : float, optional
        A parameter reflecting the market maker's aversion to inventory risk, defaults to 0.001.
    volatility : float, optional
        The assumed market volatility for spread calculation, defaults to 0.01.
    kappa : float, optional
        A parameter related to order book resilience/arrival rate, defaults to 0.01.
    lambda_val : float, optional
        A parameter related to order book depth/intensity, defaults to 1.0.
    order_book_impact : float, optional
        How much order book depth or imbalance influences the spread, defaults to 0.05.

    Returns
    -------
    dict
        An initialized state dictionary containing all market maker parameters.
    """
    logger.info("Initializing market state.")
    state = {
        'cash': initial_cash,
        'inventory': initial_inventory,
        'mid_price': initial_mid_price,
        'tick_size': tick_size,
        'max_inventory': max_inventory,
        'min_inventory': min_inventory,
        'risk_aversion': risk_aversion,
        'volatility': volatility,
        'kappa': kappa,
        'lambda_val': lambda_val,
        'order_book_impact': order_book_impact,
        'time': 0
    }
    logger.debug(f"Market state initialized: {state}")
    return state

Function Name: generate_market_data

This function simulates the evolution of the mid-price and generates synthetic order book data (bid/ask prices, depth). It incorporates random walks for mid-price and introduces some random fluctuations for order book depth. This is crucial for creating a realistic simulation environment.

Parameters:

  • state (dict): The current market state dictionary.
  • num_steps (int): The number of simulation steps to generate data for.

Returns:

  • pd.DataFrame: A DataFrame containing simulated market data (time, mid_price, bid_price, ask_price, bid_volume, ask_volume) for num_steps.
[ ]
def generate_market_data(state: dict, num_steps: int) -> pd.DataFrame:
    """
    Simulates market data including mid-price, bid/ask prices, and order book depth.

    Parameters
    ----------
    state : dict
        The current market state dictionary.
    num_steps : int
        The number of simulation steps to generate data for.

    Returns
    -------
    pd.DataFrame
        A DataFrame containing simulated market data (time, mid_price, bid_price, ask_price, bid_volume, ask_volume)
        for `num_steps`.

    Examples
    --------
    >>> initial_state = create_market_state(10000, 0, 100.0)
    >>> market_data = generate_market_data(initial_state, 10)
    >>> print(market_data.head())
    """
    logger.info(f"Generating {num_steps} steps of market data.")
    mid_prices = [state['mid_price']]
    bid_prices = []
    ask_prices = []
    bid_volumes = []
    ask_volumes = []
    times = [0]

    current_mid_price = state['mid_price']
    tick_size = state['tick_size']

    for i in range(1, num_steps):
        # Simulate mid-price as a random walk
        current_mid_price += np.random.normal(0, state['volatility']) * current_mid_price
        current_mid_price = round(current_mid_price / tick_size) * tick_size
        mid_prices.append(current_mid_price)
        times.append(i)

        # Simulate some base bid/ask spread and depth
        base_spread = 2 * tick_size # A small base spread
        bid = current_mid_price - base_spread / 2 - random.uniform(0, tick_size)
        ask = current_mid_price + base_spread / 2 + random.uniform(0, tick_size)

        bid = round(bid / tick_size) * tick_size
        ask = round(ask / tick_size) * tick_size

        bid_prices.append(bid)
        ask_prices.append(ask)

        # Simulate order book depth with some randomness
        bid_volumes.append(max(1, int(np.random.normal(50, 10))))
        ask_volumes.append(max(1, int(np.random.normal(50, 10))))

    df = pd.DataFrame({
        'time': times,
        'mid_price': mid_prices,
        'bid_price': bid_prices + [0] * (num_steps - len(bid_prices)), # Adjust for initial 0th step, fill with 0, will be overwritten if used with real data
        'ask_price': ask_prices + [0] * (num_steps - len(ask_prices)), # Adjust similarly
        'bid_volume': bid_volumes + [0] * (num_steps - len(bid_volumes)),
        'ask_volume': ask_volumes + [0] * (num_steps - len(ask_volumes))
    })
    # For time 0, set bid/ask prices based on the first mid_price
    if num_steps > 0:
        df.loc[0, 'bid_price'] = df.loc[0, 'mid_price'] - base_spread / 2
        df.loc[0, 'ask_price'] = df.loc[0, 'mid_price'] + base_spread / 2
        df.loc[0, 'bid_volume'] = max(1, int(np.random.normal(50, 10)))
        df.loc[0, 'ask_volume'] = max(1, int(np.random.normal(50, 10)))


    logger.debug(f"Generated market data head: {df.head()}")
    return df

Function Name: calculate_dynamic_spread

This function calculates the dynamic bid-ask spread based on market conditions and the market maker's current inventory. It uses an adaptation of the Avellaneda-Stoikov model principles, where the spread widens with higher inventory imbalance and market volatility, and narrows to attract trades when inventory is balanced.

Parameters:

  • state (dict): The current market state dictionary, containing parameters like risk_aversion, volatility, kappa, lambda_val, inventory, and max_inventory.

Returns:

  • tuple[float, float]: A tuple containing the calculated bid_offset and ask_offset from the current mid_price.
[ ]
def calculate_dynamic_spread(state: dict) -> tuple[float, float]:
    """
    Calculates the dynamic bid-ask spread based on inventory and market parameters.
    This function implements a simplified version of the Avellaneda-Stoikov model for spread calculation.

    Parameters
    ----------
    state : dict
        The current market state dictionary, containing parameters like `risk_aversion`, `volatility`,
        `kappa`, `lambda_val`, `inventory`, and `max_inventory`.

    Returns
    -------
    tuple[float, float]
        A tuple containing the calculated `bid_offset` and `ask_offset` from the current `mid_price`.

    Examples
    --------
    >>> state = create_market_state(10000, 0, 100.0, risk_aversion=0.001, volatility=0.01, kappa=0.01, lambda_val=1.0)
    >>> bid_off, ask_off = calculate_dynamic_spread(state)
    >>> print(f"Bid Offset: {bid_off:.4f}, Ask Offset: {ask_off:.4f}")
    """
    logger.info("Calculating dynamic spread.")

    gamma = state['risk_aversion']
    sigma = state['volatility']
    T = 1.0 # Remaining time horizon (simplified to a constant for steady state)
    q = state['inventory'] # Current inventory
    kappa = state['kappa'] # Order book resilience parameter

    # Optimal spread components based on Avellaneda-Stoikov
    spread_from_vol_risk = gamma * sigma**2 * T
    spread_from_order_flow = (2 / gamma) * math.log(1 + gamma / kappa)
    total_optimal_spread_width = spread_from_vol_risk + spread_from_order_flow

    # Inventory adjustment for reservation price
    reservation_price_offset = -q * gamma * sigma**2 * T

    # Calculate bid and ask offsets from mid_price
    # bid_price = mid_price + reservation_price_offset - 0.5 * total_optimal_spread_width
    # ask_price = mid_price + reservation_price_offset + 0.5 * total_optimal_spread_width
    bid_offset = reservation_price_offset - 0.5 * total_optimal_spread_width
    ask_offset = reservation_price_offset + 0.5 * total_optimal_spread_width

    # Ensure offsets are symmetrical and account for tick size
    tick_size = state['tick_size']

    # Ensure bid_offset is negative and ask_offset is positive relative to mid (after reservation adjustment)
    # And snap to tick size
    bid_offset = round(bid_offset / tick_size) * tick_size
    ask_offset = round(ask_offset / tick_size) * tick_size

    # Ensure at least one tick spread
    if (ask_offset - bid_offset) < tick_size * 2:
        avg_offset = (ask_offset + bid_offset) / 2
        bid_offset = avg_offset - tick_size
        ask_offset = avg_offset + tick_size

    logger.debug(f"Calculated bid_offset: {bid_offset:.4f}, ask_offset: {ask_offset:.4f} for inventory: {q}")
    return bid_offset, ask_offset

Function Name: place_orders

This function simulates placing bid and ask orders in the market. It takes the current market state and the current mid-price from the simulated market data, then uses the calculate_dynamic_spread function to determine the optimal bid and ask prices. It also sets a fixed order quantity for simplicity.

Parameters:

  • state (dict): The current market maker's state dictionary.
  • current_market_mid_price (float): The mid-price observed in the market at the current time step.

Returns:

  • tuple[float, float, int]: A tuple containing the bid_price, ask_price, and order_quantity for each order.
[ ]
def place_orders(state: dict, current_market_mid_price: float) -> tuple[float, float, int]:
    """
    Places bid and ask orders based on dynamic spread calculation.

    Parameters
    ----------
    state : dict
        The current market maker's state dictionary.
    current_market_mid_price : float
        The mid-price observed in the market at the current time step.

    Returns
    -------
    tuple[float, float, int]
        A tuple containing the `bid_price`, `ask_price`, and `order_quantity` for each order.

    Examples
    --------
    >>> state = create_market_state(10000, 0, 100.0)
    >>> bid_p, ask_p, qty = place_orders(state, 100.0)
    >>> print(f"Placed Bid: {bid_p}, Placed Ask: {ask_p}, Quantity: {qty}")
    """
    logger.info(f"Placing orders at market mid_price: {current_market_mid_price:.4f}")
    bid_offset, ask_offset = calculate_dynamic_spread(state)

    # Adjust bid and ask prices based on current market mid-price and calculated offsets
    bid_price = round((current_market_mid_price + bid_offset) / state['tick_size']) * state['tick_size']
    ask_price = round((current_market_mid_price + ask_offset) / state['tick_size']) * state['tick_size']

    order_quantity = 10 # Fixed order quantity for simplicity

    logger.debug(f"Placed orders: Bid={bid_price:.4f}, Ask={ask_price:.4f}, Qty={order_quantity}")
    return bid_price, ask_price, order_quantity

Function Name: execute_trade

This function simulates the execution of a trade. It takes the market maker's placed orders and checks them against the current market's best bid and ask prices. If a trade can be filled (e.g., market's best bid is >= market maker's ask, or market's best ask is <= market maker's bid), it updates the market maker's cash and inventory. It also includes random jitter for simulating trade latency or backoff.

Parameters:

  • state (dict): The current market maker's state dictionary.
  • mm_bid_price (float): The bid price placed by the market maker.
  • mm_ask_price (float): The ask price placed by the market maker.
  • order_quantity (int): The quantity of each order.
  • market_best_bid (float): The best bid price currently in the market.
  • market_best_ask (float): The best ask price currently in the market.

Returns:

  • dict: The updated market maker's state dictionary after potential trades.
  • int: The number of shares bought.
  • int: The number of shares sold.
[ ]
def execute_trade(
    state: dict,
    mm_bid_price: float,
    mm_ask_price: float,
    order_quantity: int,
    market_best_bid: float,
    market_best_ask: float
) -> tuple[dict, int, int]:
    """
    Simulates trade execution against market's best bid/ask and updates state.
    Includes random jitter for timing/backoff mechanisms.

    Parameters
    ----------
    state : dict
        The current market maker's state dictionary.
    mm_bid_price : float
        The bid price placed by the market maker.
    mm_ask_price : float
        The ask price placed by the market maker.
    order_quantity : int
        The quantity of each order.
    market_best_bid : float
        The best bid price currently in the market.
    market_best_ask : float
        The best ask price currently in the market.

    Returns
    -------
    tuple[dict, int, int]
        The updated market maker's state dictionary after potential trades, and the number of shares bought and sold.

    Examples
    --------
    >>> state = create_market_state(10000, 0, 100.0)
    >>> updated_state, bought, sold = execute_trade(state, 99.9, 100.1, 10, 100.05, 100.15)
    >>> print(f"Inventory: {updated_state['inventory']}, Cash: {updated_state['cash']}")
    """
    logger.info(f"Attempting trade execution. MM Bid: {mm_bid_price:.4f}, MM Ask: {mm_ask_price:.4f}")
    shares_bought = 0
    shares_sold = 0

    # Introduce random jitter for simulating latency/backoff
    time.sleep(random.uniform(0.001, 0.005))

    # Check for fill on the market maker's ask (someone buys from us)
    if market_best_bid >= mm_ask_price:
        # Ensure we don't oversell more than we have or beyond min_inventory
        if state['inventory'] - order_quantity >= state['min_inventory']:
            state['cash'] += mm_ask_price * order_quantity
            state['inventory'] -= order_quantity
            shares_sold = order_quantity
            logger.debug(f"Sold {order_quantity} at {mm_ask_price:.4f}. New inventory: {state['inventory']}, cash: {state['cash']:.2f}")
        else:
            logger.warning(f"Attempted to sell {order_quantity} but would exceed min_inventory. Current inventory: {state['inventory']}")

    # Check for fill on the market maker's bid (we buy from someone)
    elif market_best_ask <= mm_bid_price:
        # Ensure we don't overbuy beyond max_inventory
        if state['inventory'] + order_quantity <= state['max_inventory']:
            state['cash'] -= mm_bid_price * order_quantity
            state['inventory'] += order_quantity
            shares_bought = order_quantity
            logger.debug(f"Bought {order_quantity} at {mm_bid_price:.4f}. New inventory: {state['inventory']}, cash: {state['cash']:.2f}")
        else:
            logger.warning(f"Attempted to buy {order_quantity} but would exceed max_inventory. Current inventory: {state['inventory']}")

    return state, shares_bought, shares_sold

Function Name: calculate_pnl

This function calculates the market maker's current Profit and Loss (PnL). PnL is composed of the current cash balance and the mark-to-market value of the current inventory. The mark-to-market value is calculated by multiplying the current inventory by the current mid-price.

Parameters:

  • state (dict): The current market maker's state dictionary.
  • current_mid_price (float): The current mid-price of the asset in the market.

Returns:

  • float: The current total PnL.
[ ]
def calculate_pnl(state: dict, current_mid_price: float) -> float:
    """
    Calculates the market maker's current Profit and Loss (PnL).

    Parameters
    ----------
    state : dict
        The current market maker's state dictionary.
    current_mid_price : float
        The current mid-price of the asset in the market.

    Returns
    -------
    float
        The current total PnL.

    Examples
    --------
    >>> state = create_market_state(10000, 10, 100.0)
    >>> pnl = calculate_pnl(state, 100.5)
    >>> print(f"Current PnL: {pnl:.2f}")
    """
    logger.info(f"Calculating PnL at mid_price: {current_mid_price:.4f}")
    # PnL = Cash + Inventory * Current Mid Price
    pnl = state['cash'] + (state['inventory'] * current_mid_price)
    logger.debug(f"Current PnL: {pnl:.2f} (Cash: {state['cash']:.2f}, Inventory: {state['inventory']}, Mid: {current_mid_price:.4f})")
    return pnl

Function Name: update_metrics

This function tracks and updates various performance metrics and market state over time. It collects data points like current PnL, cash, inventory, bid/ask prices, and spreads at each simulation step, storing them in a metrics_history dictionary for later analysis and visualization.

Parameters:

  • metrics_history (dict): A dictionary to store historical metrics.
  • state (dict): The current market maker's state dictionary.
  • current_market_mid_price (float): The mid-price from the market simulation at the current step.
  • mm_bid_price (float): The market maker's bid price at the current step.
  • mm_ask_price (float): The market maker's ask price at the current step.
  • shares_bought (int): Shares bought in the current step.
  • shares_sold (int): Shares sold in the current step.

Returns:

  • dict: The updated metrics_history dictionary.
[ ]
def update_metrics(
    metrics_history: dict,
    state: dict,
    current_market_mid_price: float,
    mm_bid_price: float,
    mm_ask_price: float,
    shares_bought: int,
    shares_sold: int
) -> dict:
    """
    Updates a dictionary of historical metrics with the current state and trade information.

    Parameters
    ----------
    metrics_history : dict
        A dictionary to store historical metrics.
    state : dict
        The current market maker's state dictionary.
    current_market_mid_price : float
        The mid-price from the market simulation at the current step.
    mm_bid_price : float
        The market maker's bid price at the current step.
    mm_ask_price : float
        The market maker's ask price at the current step.
    shares_bought : int
        Shares bought in the current step.
    shares_sold : int
        Shares sold in the current step.

    Returns
    -------
    dict
        The updated `metrics_history` dictionary.

    Examples
    --------
    >>> history = collections.defaultdict(list)
    >>> state = create_market_state(10000, 0, 100.0)
    >>> updated_history = update_metrics(history, state, 100.0, 99.9, 100.1, 0, 0)
    >>> print(updated_history['pnl'][-1])
    """
    logger.info("Updating metrics history.")
    current_pnl = calculate_pnl(state, current_market_mid_price)
    spread = mm_ask_price - mm_bid_price

    metrics_history['time'].append(state['time'])
    metrics_history['mid_price'].append(current_market_mid_price)
    metrics_history['mm_bid_price'].append(mm_bid_price)
    metrics_history['mm_ask_price'].append(mm_ask_price)
    metrics_history['mm_spread'].append(spread)
    metrics_history['inventory'].append(state['inventory'])
    metrics_history['cash'].append(state['cash'])
    metrics_history['pnl'].append(current_pnl)
    metrics_history['shares_bought'].append(shares_bought)
    metrics_history['shares_sold'].append(shares_sold)
    logger.debug(f"Metrics updated: PnL={current_pnl:.2f}, Inventory={state['inventory']}, Spread={spread:.4f}")
    return metrics_history

Function Name: run_simulation

This function orchestrates the entire market making simulation. It iterates through the generated market data, places orders using place_orders, executes potential trades with execute_trade, and records performance metrics using update_metrics. It simulates the dynamic interaction between the market maker and the fluctuating market conditions.

Parameters:

  • initial_state (dict): The initial market maker's state.
  • market_data_df (pd.DataFrame): A DataFrame containing simulated market data for the simulation period.

Returns:

  • pd.DataFrame: A DataFrame containing the full history of recorded metrics during the simulation.
[ ]
def run_simulation(initial_state: dict, market_data_df: pd.DataFrame) -> pd.DataFrame:
    """
    Runs the market making simulation over the provided market data.

    Parameters
    ----------
    initial_state : dict
        The initial market maker's state.
    market_data_df : pd.DataFrame
        A DataFrame containing simulated market data for the simulation period.

    Returns
    -------
    pd.DataFrame
        A DataFrame containing the full history of recorded metrics during the simulation.

    Examples
    --------
    >>> state = create_market_state(10000, 0, 100.0)
    >>> market_df = generate_market_data(state, 100)
    >>> simulation_results = run_simulation(state, market_df)
    >>> print(simulation_results.head())
    """
    logger.info("Starting market making simulation.")
    current_state = initial_state.copy()
    metrics_history = collections.defaultdict(list)

    for index, row in tqdm(market_data_df.iterrows(), total=len(market_data_df), desc="Simulating Market Making"):
        current_state['time'] = row['time']
        current_state['mid_price'] = row['mid_price']

        mm_bid_price, mm_ask_price, order_quantity = place_orders(current_state, row['mid_price'])

        # Execute trades against the simulated market's best bid/ask
        current_state, shares_bought, shares_sold = execute_trade(
            current_state,
            mm_bid_price,
            mm_ask_price,
            order_quantity,
            row['bid_price'], # Use market's bid for our ask fill
            row['ask_price']  # Use market's ask for our bid fill
        )

        metrics_history = update_metrics(
            metrics_history,
            current_state,
            row['mid_price'],
            mm_bid_price,
            mm_ask_price,
            shares_bought,
            shares_sold
        )
    logger.info("Market making simulation completed.")
    return pd.DataFrame(metrics_history)

Demonstration/Visualization

This section demonstrates the market making strategy through a simulation and visualizes the key aspects of its performance, including dynamic spread, inventory, cash, PnL, and trade activity. We'll use matplotlib and seaborn for plotting.

[ ]
# --- Simulation Parameters ---
SIM_STEPS = 500
INITIAL_CASH = 100000.0
INITIAL_INVENTORY = 0
INITIAL_MID_PRICE = 100.0
RISK_AVERSION = 0.5 # Increased risk aversion for more sensitive spreads
VOLATILITY = 0.02 # Increased market volatility
KAPPA = 10.0 # Increased order book resilience parameter
LAMBDA_VAL = 1.0 # Order arrival rate intensity (kept similar or slightly adjusted for balance)

logger.info("Setting up initial market state and generating market data.")
# Initialize market state
initial_mm_state = create_market_state(
    initial_cash=INITIAL_CASH,
    initial_inventory=INITIAL_INVENTORY,
    initial_mid_price=INITIAL_MID_PRICE,
    risk_aversion=RISK_AVERSION,
    volatility=VOLATILITY,
    kappa=KAPPA,
    lambda_val=LAMBDA_VAL
)

# Generate simulated market data
simulated_market_data = generate_market_data(initial_mm_state, SIM_STEPS)

# Run the simulation
simulation_results_df = run_simulation(initial_mm_state, simulated_market_data)

logger.info("Simulation complete. Displaying summary statistics.")
# Display summary statistics of the simulation results
print("\n--- Simulation Results Summary ---")
display(simulation_results_df.describe())
Simulating Market Making:   0%|          | 0/500 [00:00<?, ?it/s]
WARNING:root:2026-06-08 12:12:56.336 | WARNING  | __main__:execute_trade:55 - Attempted to sell 10 but would exceed min_inventory. Current inventory: -100

--- Simulation Results Summary ---
time mid_price mm_bid_price mm_ask_price mm_spread inventory cash pnl shares_bought shares_sold
count 500.000000 500.000000 500.00000 500.000000 500.000000 500.000000 500.000000 500.000000 500.000000 500.00000
mean 249.500000 107.502600 107.40828 107.604580 0.196300 -18.900000 99581.929600 97325.798400 4.820000 4.78000
std 144.481833 8.727551 8.73278 8.733017 0.004833 34.551086 4498.776002 1480.404314 5.001763 5.00016
min 0.000000 81.400000 81.29000 81.490000 0.190000 -100.000000 91735.700000 94817.500000 0.000000 0.00000
25% 124.750000 102.157500 102.05750 102.247500 0.190000 -50.000000 95896.125000 95952.075000 0.000000 0.00000
50% 249.500000 107.530000 107.43000 107.630000 0.200000 -10.000000 98501.150000 97011.900000 0.000000 0.00000
75% 374.250000 113.445000 113.36000 113.552500 0.200000 10.000000 103205.300000 98834.425000 10.000000 10.00000
max 499.000000 130.300000 130.22000 130.410000 0.200000 60.000000 110952.600000 100001.000000 10.000000 10.00000

Visualization 1: Mid-Price, Market Maker's Bid/Ask, and Dynamic Spread

[ ]
plt.figure(figsize=(15, 8))
sns.set_style("whitegrid")

# Plot mid-price
sns.lineplot(x='time', y='mid_price', data=simulation_results_df, label='Market Mid-Price', color='black', alpha=0.7)

# Plot market maker's bid and ask prices
sns.lineplot(x='time', y='mm_bid_price', data=simulation_results_df, label='MM Bid Price', color='red', linestyle='--')
sns.lineplot(x='time', y='mm_ask_price', data=simulation_results_df, label='MM Ask Price', color='green', linestyle='--')

plt.title('Market Mid-Price vs. Market Maker Bid/Ask Prices')
plt.xlabel('Time Step')
plt.ylabel('Price')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
cell output

Visualization 2: Inventory and PnL Evolution

[ ]
fig, axes = plt.subplots(2, 1, figsize=(15, 10), sharex=True)
sns.set_style("whitegrid")

# Plot Inventory
sns.lineplot(x='time', y='inventory', data=simulation_results_df, ax=axes[0], color='purple')
axes[0].axhline(y=0, color='gray', linestyle=':', linewidth=0.8, label='Zero Inventory')
axes[0].set_title('Market Maker Inventory Over Time')
axes[0].set_ylabel('Inventory (Shares)')
axes[0].legend()

# Plot PnL
sns.lineplot(x='time', y='pnl', data=simulation_results_df, ax=axes[1], color='orange')
axes[1].axhline(y=INITIAL_CASH, color='gray', linestyle=':', linewidth=0.8, label='Initial Capital')
axes[1].set_title('Market Maker PnL Over Time')
axes[1].set_xlabel('Time Step')
axes[1].set_ylabel('PnL (USD)')
axes[1].legend()

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

Visualization 3: Dynamic Spread Changes and Trade Activity

[ ]
fig, axes = plt.subplots(2, 1, figsize=(15, 10), sharex=True)
sns.set_style("whitegrid")

# Plot Dynamic Spread
sns.lineplot(x='time', y='mm_spread', data=simulation_results_df, ax=axes[0], color='teal')
axes[0].set_title('Market Maker Dynamic Bid-Ask Spread')
axes[0].set_ylabel('Spread (USD)')

# Plot Trade Activity (Bought/Sold)
axes[1].fill_between(simulation_results_df['time'], 0, simulation_results_df['shares_bought'], color='blue', alpha=0.3, label='Shares Bought')
axes[1].fill_between(simulation_results_df['time'], 0, -simulation_results_df['shares_sold'], color='red', alpha=0.3, label='Shares Sold')
axes[1].set_title('Trade Activity (Shares Bought/Sold per Step)')
axes[1].set_xlabel('Time Step')
axes[1].set_ylabel('Shares')
axes[1].legend()

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

Conclusion

This notebook has provided a foundational framework for understanding and implementing a dynamic bid-ask spread optimization strategy for market making. We've covered:

  1. State Initialization: Setting up the market maker's initial capital, inventory, and strategy parameters.
  2. Market Data Simulation: Generating a synthetic market environment with fluctuating mid-prices and order book dynamics.
  3. Dynamic Spread Calculation: Implementing a simplified model to adjust bid and ask offsets based on inventory and market parameters.
  4. Order Placement and Execution: Simulating the process of placing orders and executing trades against the market.
  5. PnL and Metrics Tracking: Calculating and recording key performance indicators like PnL, cash, and inventory over time.
  6. Visualization: Illustrating the strategy's behavior and performance through various plots.

While this simulation uses simplified models, it highlights the core principles of how a market maker can dynamically adjust their quotes to manage inventory risk and capture spread profits. Further enhancements could include more sophisticated models for price prediction, incorporating actual order book dynamics, and advanced risk management techniques.

Spread Optimization · BitPredict