Avellaneda Stoikov MM
Implement the canonical Avellaneda-Stoikov stochastic optimal control market making model that dynamically computes optimal bid and ask quote placement depths and sizes in closed form as a function of current inventory position, volatility, and risk aversion parameter calibration.
Avellaneda-Stoikov Market Making Model
Introduction
This notebook explores the Avellaneda-Stoikov market making model, a foundational quantitative finance framework for optimal quoting strategies in financial markets. Developed by Marco Avellaneda and Sasha Stoikov, this model provides a methodology for a market maker to strategically post bid and ask prices to maximize their expected utility, taking into account their current inventory and risk aversion.
The core idea is to balance two competing objectives: attracting trades to earn the bid-ask spread and managing inventory risk. If a market maker accumulates too much inventory, they face increased exposure to price fluctuations; if they have too little, they miss out on potential profits from facilitating trades. The model uses stochastic control theory to derive optimal bid and ask quotes that dynamically adjust based on market conditions, inventory levels, and the market maker's risk appetite.
Key Concepts
| Concept | Description |
|---|---|
| Market Making | The process of providing liquidity to a market by simultaneously quoting both a buy (bid) and a sell (ask) price for an asset. Market makers profit from the bid-ask spread. |
| Optimal Quoting | Determining the best bid and ask prices to post at any given moment, considering various market and internal factors. |
| Inventory Risk | The risk associated with holding an open position (either long or short) in an asset, exposed to adverse price movements. |
| Reservation Price | The internal price at which a market maker is indifferent between buying or selling an asset, based on their current inventory and risk aversion. It acts as a reference point for setting quotes. |
| Optimal Spread | The dynamic spread around the reservation price that maximizes the market maker's expected utility, balancing profit from trades against inventory risk. |
| Risk Aversion ($\gamma$) | A parameter reflecting the market maker's reluctance to hold inventory. Higher $\gamma$ means more aggressive inventory reduction. |
| Inventory ($q$) | The current quantity of the asset held by the market maker. Positive for long positions, negative for short positions. |
| Arrival Rates ($\lambda_a, \lambda_b$) | The rates at which market orders (asks and bids, respectively) arrive, influenced by the distance of the market maker's quotes from the fair price. |
| Order Book Volatility ($\sigma$) | The volatility of the underlying asset's price, impacting the inventory risk. |
Dependency Installation
We will install necessary libraries for numerical computations, data handling, and plotting.
pip install numpy scipy matplotlib pandasRequirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.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: pandas in /usr/local/lib/python3.12/dist-packages (2.2.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: python-dateutil>=2.7 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (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: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.7->matplotlib) (1.17.0)
Library Imports
Import all standard and third-party libraries required for the notebook.
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import math
import logging
from collections import deque
import time
import randomCore Functions
This section defines the core functions for the Avellaneda-Stoikov market making model, adhering to the specified format for documentation, type hinting, and state management.
Function Name: create_market_state
This function initializes the market environment and the market maker's initial state. It sets up parameters like initial asset price, inventory, time horizon, risk aversion, and order book characteristics.
Parameters:
S0(float): Initial mid-price of the asset.q0(int): Initial inventory of the market maker.T(float): Total time horizon for the simulation.dt(float): Time step increment for the simulation.gamma(float): Risk aversion parameter.kappa(float): Order book liquidity parameter, related to the sensitivity of order arrival rates to quote deviations.sigma(float): Volatility of the underlying asset price.lambda_0(float): Baseline market order arrival rate.
Returns:
- (dict): A dictionary representing the initial market state.
def create_market_state(
S0: float = 100.0,
q0: int = 0,
T: float = 1.0,
dt: float = 0.01,
gamma: float = 0.1,
kappa: float = 1.5,
sigma: float = 0.2,
lambda_0: float = 1.0
) -> dict:
"""
Initializes the market environment and the market maker's initial state.
Parameters
----------
S0 : float, optional
Initial mid-price of the asset, defaults to 100.0.
q0 : int, optional
Initial inventory of the market maker, defaults to 0.
T : float, optional
Total time horizon for the simulation, defaults to 1.0.
dt : float, optional
Time step increment for the simulation, defaults to 0.01.
gamma : float, optional
Risk aversion parameter, defaults to 0.1.
kappa : float, optional
Order book liquidity parameter, related to the sensitivity of order arrival rates to quote deviations, defaults to 1.5.
sigma : float, optional
Volatility of the underlying asset price, defaults to 0.2.
lambda_0 : float, optional
Baseline market order arrival rate, defaults to 1.0.
Returns
-------
dict
A dictionary representing the initial market state.
"""
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
state = {
'current_time': 0.0,
'mid_price': S0,
'inventory': q0,
'total_time': T,
'time_step': dt,
'risk_aversion': gamma,
'kappa': kappa,
'volatility': sigma,
'lambda_0': lambda_0,
'cash': 0.0, # Initial cash, updated with trades
'trades': [], # To store trade history
'inventory_history': [q0],
'mid_price_history': [S0],
'reservation_price_history': [],
'bid_quote_history': [],
'ask_quote_history': [],
'spread_history': [],
'time_history': [0.0]
}
logging.info(f"Market state initialized: S0={S0}, q0={q0}, T={T}, dt={dt}")
return stateFunction Name: calculate_reservation_price
This function computes the market maker's reservation price. The reservation price is the internal fair value of the asset for the market maker, adjusted for their current inventory and risk aversion. It shifts away from the mid-price to compensate for holding inventory.
Parameters:
state(dict): The current market state dictionary.
Returns:
- (dict): The updated state dictionary including the calculated reservation price.
def calculate_reservation_price(state: dict) -> dict:
"""
Calculates the market maker's reservation price based on the Avellaneda-Stoikov model.
Parameters
----------
state : dict
Current market state dictionary.
Returns
-------
dict
Updated state with the calculated reservation price.
"""
S = state['mid_price']
q = state['inventory']
gamma = state['risk_aversion']
sigma = state['volatility']
T = state['total_time']
t = state['current_time']
# Avellaneda-Stoikov reservation price formula
reservation_price = S - q * gamma * sigma**2 * (T - t)
state['reservation_price'] = reservation_price
# Removed: state['reservation_price_history'].append(reservation_price) -> now handled in run_simulation_step
logging.debug(f"Time: {t:.2f}, Mid Price: {S:.2f}, Inventory: {q}, Reservation Price: {reservation_price:.2f}")
return stateFunction Name: calculate_optimal_spread
This function determines the optimal spread around the reservation price. The optimal spread depends on the market maker's risk aversion, volatility, liquidity parameters, and the remaining time to maturity. It represents the width of the bid-ask quotes to balance earning spread and managing inventory.
Parameters:
state(dict): The current market state dictionary.
Returns:
- (dict): The updated state dictionary including the calculated optimal spread.
Note: There's a common approximation/alternative formulation for the optimal spread. We'll use the one often seen in Avellaneda-Stoikov literature involving the logarithm term. The commonly used formula is actually:
optimal_spread = gamma * sigma^2 * (T - t) + (2 / gamma) * np.log(1 + gamma / kappa).
However, a more complete derivation might lead to a form where np.log(1 + gamma / kappa) / (total_time - current_time) is more appropriate for some contexts. For simplicity, we will use the common form gamma * sigma^2 * (T - t) + (2/gamma) * np.log(1 + gamma/kappa). Let's refine this to match typical derivations that do not include division by (T-t) for the log part.
The common formula is:
optimal_spread = (gamma * sigma**2 * (T - t)) + (2/gamma) * np.log(1 + gamma/kappa)
Let's check sources to ensure the correct formula, specifically the dependency on (T-t). The paper often presents a spread that depends on T-t in the first term but not the second log term directly divided by T-t.
A more standard formula for the half-spread, $\delta$, is:
$\delta = \frac{\gamma \sigma^2 (T-t)}{2} - \frac{1}{\eta} \ln \left( \frac{\lambda_0}{\kappa} \left( 1 + \frac{1}{\eta} \right) \right)$
This is getting complex. Let's use the simplified version often cited for the total spread, derived from the reservation price and optimal quotes:
delta = (gamma * sigma**2 * (T - t) / 2) + (1 / kappa) * np.log(1 + kappa / gamma) (this is for half spread)
Let's stick to a common form that is easily derivable from the bid and ask quotes, which are usually centered around the reservation price with an additional term for inventory.
A robust approach for the optimal half-spread ($\delta$) is often derived as:
delta = 0.5 * gamma * sigma**2 * (T - t) + (1 / kappa) * np.log(1 + kappa / gamma)
And the total spread is 2 * delta. Let's use this for optimal_spread.
So, optimal_spread = gamma * sigma**2 * (T - t) + (2 / kappa) * np.log(1 + kappa / gamma)
This form aligns with common interpretations for the total spread as the distance between the optimal bid and ask from the reservation price.
def calculate_optimal_spread(state: dict) -> dict:
"""
Calculates the optimal spread for the market maker's quotes.
Parameters
----------
state : dict
Current market state dictionary.
Returns
-------
dict
Updated state with the calculated optimal spread.
"""
gamma = state['risk_aversion']
sigma = state['volatility']
kappa = state['kappa']
T = state['total_time']
t = state['current_time']
# Avoid division by zero or log of zero if T - t is very small
time_to_maturity = max(T - t, 1e-9) # Use a small epsilon instead of 0
# Avellaneda-Stoikov optimal *total* spread formula
optimal_spread = gamma * sigma**2 * time_to_maturity + (2 / kappa) * np.log(1 + kappa / gamma)
# Ensure spread is non-negative (can be negative if inventory is heavily skewed)
optimal_spread = max(0.0, optimal_spread)
state['optimal_spread'] = optimal_spread
# Removed: state['spread_history'].append(optimal_spread) -> now handled in run_simulation_step
logging.debug(f"Time: {t:.2f}, Optimal Spread: {optimal_spread:.4f}")
return stateFunction Name: get_optimal_quotes
This function determines the optimal bid and ask quotes based on the calculated reservation price and optimal spread. The quotes are set symmetrically around the reservation price by half of the optimal spread.
Parameters:
state(dict): The current market state dictionary, which must contain 'reservation_price' and 'optimal_spread'.
Returns:
- (dict): The updated state dictionary including the calculated optimal bid and ask quotes.
def get_optimal_quotes(state: dict) -> dict:
"""
Determines the optimal bid and ask quotes based on the reservation price and optimal spread.
Parameters
----------
state : dict
Current market state dictionary, must contain 'reservation_price' and 'optimal_spread'.
Returns
-------
dict
Updated state with the calculated optimal bid and ask quotes.
"""
reservation_price = state['reservation_price']
optimal_spread = state['optimal_spread']
optimal_bid = reservation_price - optimal_spread / 2
optimal_ask = reservation_price + optimal_spread / 2
state['optimal_bid'] = optimal_bid
state['optimal_ask'] = optimal_ask
# Removed: state['bid_quote_history'].append(optimal_bid) -> now handled in run_simulation_step
# Removed: state['ask_quote_history'].append(optimal_ask) -> now handled in run_simulation_step
logging.debug(f"Time: {state['current_time']:.2f}, Optimal Bid: {optimal_bid:.2f}, Optimal Ask: {optimal_ask:.2f}")
return stateFunction Name: simulate_order_arrival
This function simulates the arrival of market orders (buy or sell) based on the current optimal bid and ask quotes. It uses Poisson processes, where the intensity of order arrival depends on how aggressive the market maker's quotes are (i.e., how close they are to the mid-price).
Parameters:
state(dict): The current market state dictionary.
Returns:
- (tuple): A tuple containing a string indicating the trade type ('buy', 'sell', or 'none') and the price at which the trade would occur (if any). Returns
('none', None)if no order arrives.
import random
import numpy as np # Adding np import for robustness, though it's typically available globally
def simulate_order_arrival(state: dict) -> tuple[str, float | None]:
"""
Simulates the arrival of market orders (buy or sell).
Parameters
----------
state : dict
Current market state dictionary.
Returns
-------
tuple
A tuple (trade_type, trade_price). trade_type is 'buy', 'sell', or 'none'.
trade_price is the price at which the trade would occur, or None if no trade.
"""
S = state['mid_price']
optimal_bid = state['optimal_bid']
optimal_ask = state['optimal_ask']
lambda_0 = state['lambda_0']
kappa = state['kappa']
dt = state['time_step']
# Calculate depths (distances from mid-price)
delta_b = S - optimal_bid
delta_a = optimal_ask - S
# Calculate arrival rates for buy and sell orders
lambda_buy = lambda_0 * np.exp(-kappa * delta_a) # Market buy hits our ask
lambda_sell = lambda_0 * np.exp(-kappa * delta_b) # Market sell hits our bid
# Simulate arrivals using Poisson probabilities P = lambda * dt
# Add random jitter to simulate real-world arrival times / retry delays if this were a retry mechanism
# For simple Poisson simulation, we compare random number to probability.
jitter = random.uniform(-0.01, 0.01) # Small jitter for event probability
# Simulate market buy order (hits our ask)
if random.random() < (lambda_buy * dt + jitter):
logging.debug(f"Time: {state['current_time']:.2f}, Market buy order arrived at Ask: {optimal_ask:.2f}")
return 'buy', optimal_ask
# Simulate market sell order (hits our bid)
if random.random() < (lambda_sell * dt + jitter):
logging.debug(f"Time: {state['current_time']:.2f}, Market sell order arrived at Bid: {optimal_bid:.2f}")
return 'sell', optimal_bid
return 'none', NoneFunction Name: execute_trade
This function updates the market maker's state (inventory and cash) after a trade occurs. It records the trade details and logs the changes.
Parameters:
state(dict): The current market state dictionary.trade_type(str): The type of trade ('buy' or 'sell').trade_price(float): The price at which the trade occurred.
Returns:
- (dict): The updated state dictionary after the trade.
def execute_trade(state: dict, trade_type: str, trade_price: float) -> dict:
"""
Updates the market maker's state (inventory and cash) after a trade.
Parameters
----------
state : dict
Current market state dictionary.
trade_type : str
The type of trade ('buy' from market maker's perspective means market maker sells, 'sell' means market maker buys).
trade_price : float
The price at which the trade occurred.
Returns
-------
dict
Updated state dictionary after the trade.
"""
if trade_type == 'buy': # Market order buys from us (we sell from inventory)
state['inventory'] -= 1
state['cash'] += trade_price
logging.info(f"Time: {state['current_time']:.2f}, Sold 1 unit at {trade_price:.2f}. New Inventory: {state['inventory']}, New Cash: {state['cash']:.2f}")
elif trade_type == 'sell': # Market order sells to us (we buy into inventory)
state['inventory'] += 1
state['cash'] -= trade_price
logging.info(f"Time: {state['current_time']:.2f}, Bought 1 unit at {trade_price:.2f}. New Inventory: {state['inventory']}, New Cash: {state['cash']:.2f}")
state['trades'].append({
'time': state['current_time'],
'type': trade_type,
'price': trade_price,
'inventory_after': state['inventory'],
'cash_after': state['cash']
})
state['inventory_history'].append(state['inventory'])
return stateFunction Name: update_mid_price
This function simulates the movement of the underlying asset's mid-price. It uses a Geometric Brownian Motion (GBM) model, which is common for simulating stock prices, to introduce random fluctuations.
Parameters:
state(dict): The current market state dictionary.
Returns:
- (dict): The updated state dictionary with the new mid-price.
def update_mid_price(state: dict) -> dict:
"""
Simulates the movement of the underlying asset's mid-price using Geometric Brownian Motion.
Parameters
----------
state : dict
Current market state dictionary.
Returns
-------
dict
Updated state with the new mid-price.
"""
S = state['mid_price']
sigma = state['volatility']
dt = state['time_step']
# Geometric Brownian Motion update (assuming zero drift for short-term MM)
dW = np.random.normal(0, np.sqrt(dt))
new_S = S + sigma * S * dW # For simplicity, omitting drift mu*S*dt term
# Ensure price is non-negative
state['mid_price'] = max(1e-6, new_S)
state['mid_price_history'].append(state['mid_price'])
logging.debug(f"Time: {state['current_time']:.2f}, New Mid Price: {state['mid_price']:.2f}")
return stateFunction Name: run_simulation_step
This function executes a single time step of the Avellaneda-Stoikov market making simulation. It updates the mid-price, calculates optimal quotes, simulates order arrivals, and executes trades if any.
Parameters:
state(dict): The current market state dictionary.
Returns:
- (dict): The updated state dictionary after one simulation step.
def run_simulation_step(state: dict) -> dict:
"""
Executes a single time step of the Avellaneda-Stoikov market making simulation.
Parameters
----------
state : dict
Current market state dictionary.
Returns
-------
dict
Updated state dictionary after one simulation step.
"""
state['current_time'] += state['time_step']
state['time_history'].append(state['current_time'])
# 1. Update mid-price
state = update_mid_price(state)
# 2. Calculate reservation price
state = calculate_reservation_price(state)
# Ensure reservation_price history is updated for every step
state['reservation_price_history'].append(state['reservation_price'])
# 3. Calculate optimal spread
state = calculate_optimal_spread(state)
# Ensure spread history is updated for every step
state['spread_history'].append(state['optimal_spread'])
# 4. Get optimal quotes
state = get_optimal_quotes(state)
# Ensure bid/ask quote histories are updated for every step
state['bid_quote_history'].append(state['optimal_bid'])
state['ask_quote_history'].append(state['optimal_ask'])
# 5. Simulate order arrival
trade_type, trade_price = simulate_order_arrival(state)
# 6. Execute trade if any
if trade_type != 'none':
state = execute_trade(state, trade_type, trade_price)
# Ensure inventory history is updated for every step even if no trade occurred
if len(state['inventory_history']) < len(state['time_history']):
state['inventory_history'].append(state['inventory'])
logging.debug(f"--- End of Step {state['current_time'] / state['time_step']:.0f} --- ")
return stateFunction Name: calculate_pnl
This function calculates the market maker's Profit and Loss (PnL) at the end of the simulation. PnL is composed of the accumulated cash from trades and the liquidation value of the final inventory.
Parameters:
state(dict): The final market state dictionary.
Returns:
- (float): The calculated total PnL.
def calculate_pnl(state: dict) -> float:
"""
Calculates the market maker's total Profit and Loss (PnL).
PnL = Final Cash + Final Inventory Value (at current mid-price).
Parameters
----------
state : dict
The final market state dictionary.
Returns
-------
float
The calculated total PnL.
"""
final_cash = state['cash']
final_inventory = state['inventory']
final_mid_price = state['mid_price']
final_pnl = final_cash + final_inventory * final_mid_price
logging.info(f"Final PnL: {final_pnl:.2f}")
return final_pnlDemonstration/Visualization
This section demonstrates the Avellaneda-Stoikov market making model in action through a simulation. We will initialize a market state, run the simulation for a period, and then visualize the key metrics such as mid-price, inventory, reservation price, optimal quotes, and optimal spread. Finally, we will calculate and display the market maker's Profit and Loss (PnL).
Simulation Setup and Execution
We will set up the initial parameters for our market maker, including their risk aversion, initial inventory, and market characteristics. Then, we will run the run_simulation_step function iteratively to simulate the passage of time and market making activity.
import logging
import numpy as np
import pandas as pd
# Reset logging level to INFO for cleaner output during demonstration
logging.getLogger().setLevel(logging.INFO)
# Initialize market state
initial_state = create_market_state(
S0=100.0,
q0=0,
T=1.0,
dt=0.01,
gamma=0.1, # Risk aversion
kappa=1.5, # Order book liquidity parameter
sigma=0.2, # Volatility
lambda_0=1.0 # Baseline order arrival rate
)
market_state = initial_state.copy()
# Calculate and append initial (t=0) values for history lists
# This ensures all history lists start with a consistent value at t=0
market_state = calculate_reservation_price(market_state)
market_state['reservation_price_history'].append(market_state['reservation_price'])
market_state = calculate_optimal_spread(market_state)
market_state['spread_history'].append(market_state['optimal_spread'])
market_state = get_optimal_quotes(market_state)
market_state['bid_quote_history'].append(market_state['optimal_bid'])
market_state['ask_quote_history'].append(market_state['optimal_ask'])
# Run simulation
num_steps = int(market_state['total_time'] / market_state['time_step'])
print(f"Running simulation for {num_steps} steps...")
for _ in range(num_steps):
market_state = run_simulation_step(market_state)
print("Simulation finished.")
# Calculate final PnL
final_pnl = calculate_pnl(market_state)
print(f"Market Maker's Final PnL: {final_pnl:.2f}")
# Convert history lists to a DataFrame for easier plotting and analysis
simulation_df = pd.DataFrame({
'Time': market_state['time_history'],
'Mid_Price': market_state['mid_price_history'],
'Inventory': market_state['inventory_history'],
'Reservation_Price': market_state['reservation_price_history'],
'Bid_Quote': market_state['bid_quote_history'],
'Ask_Quote': market_state['ask_quote_history'],
'Optimal_Spread': market_state['spread_history']
})INFO:root:Market state initialized: S0=100.0, q0=0, T=1.0, dt=0.01 INFO:root:Time: 0.02, Sold 1 unit at 100.03. New Inventory: -1, New Cash: 100.03 INFO:root:Time: 0.28, Bought 1 unit at 74.12. New Inventory: 0, New Cash: 25.91 INFO:root:Final PnL: 25.91
Running simulation for 100 steps... Simulation finished. Market Maker's Final PnL: 25.91
Visualization of Key Metrics
We will now plot the evolution of the mid-price, inventory, reservation price, optimal quotes, and optimal spread over the simulation period to understand the market maker's strategy and its impact.
import matplotlib.pyplot as plt
# Set a larger figure size for better readability
plt.figure(figsize=(15, 10))
# Plot 1: Mid-Price, Reservation Price, Bid and Ask Quotes
plt.subplot(3, 1, 1)
plt.plot(simulation_df['Time'], simulation_df['Mid_Price'], label='Mid Price', color='blue')
plt.plot(simulation_df['Time'], simulation_df['Reservation_Price'], label='Reservation Price', linestyle='--', color='purple')
plt.plot(simulation_df['Time'], simulation_df['Bid_Quote'], label='Optimal Bid Quote', color='green')
plt.plot(simulation_df['Time'], simulation_df['Ask_Quote'], label='Optimal Ask Quote', color='red')
plt.title('Mid Price, Reservation Price, and Optimal Quotes Over Time')
plt.xlabel('Time')
plt.ylabel('Price')
plt.legend()
plt.grid(True)
# Plot 2: Inventory Level
plt.subplot(3, 1, 2)
plt.plot(simulation_df['Time'], simulation_df['Inventory'], label='Inventory Level', color='orange')
plt.title('Market Maker Inventory Over Time')
plt.xlabel('Time')
plt.ylabel('Inventory')
plt.legend()
plt.grid(True)
# Plot 3: Optimal Spread
plt.subplot(3, 1, 3)
plt.plot(simulation_df['Time'], simulation_df['Optimal_Spread'], label='Optimal Spread', color='brown')
plt.title('Optimal Spread Over Time')
plt.ylabel('Spread')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()Summary Statistics of Simulation
To provide a quantitative overview of the simulation, we'll display key summary statistics for the inventory, mid-price, and spread using a Pandas DataFrame.
summary_stats = simulation_df[['Mid_Price', 'Inventory', 'Optimal_Spread', 'Reservation_Price']].describe()
print("\nSummary Statistics of Simulation:\n")
print(summary_stats)
Summary Statistics of Simulation:
Mid_Price Inventory Optimal_Spread Reservation_Price
count 101.000000 101.000000 101.000000 101.000000
mean 73.682538 -0.257426 3.698785 73.683408
std 11.547441 0.439397 0.001172 11.548682
min 61.038337 -1.000000 3.696785 61.038337
25% 65.614686 -1.000000 3.697785 65.614686
50% 68.143690 0.000000 3.698785 68.143690
75% 80.336340 0.000000 3.699785 80.339380
max 100.715400 0.000000 3.700785 100.719200
Trade Activity Overview
Let's look at the actual trades executed by the market maker during the simulation.
import pandas as pd
trades_df = pd.DataFrame(market_state['trades'])
if not trades_df.empty:
print("\nTrade Activity:\n")
print(trades_df.head())
print(f"\nTotal Trades: {len(trades_df)}")
buy_trades = trades_df[trades_df['type'] == 'sell'] # Market Maker buys
sell_trades = trades_df[trades_df['type'] == 'buy'] # Market Maker sells
print(f"Market Maker Bought: {len(buy_trades)} times")
print(f"Market Maker Sold: {len(sell_trades)} times")
else:
print("No trades occurred during the simulation.")Trade Activity: time type price inventory_after cash_after 0 0.02 buy 100.027747 -1 100.027747 1 0.28 sell 74.118360 0 25.909387 Total Trades: 2 Market Maker Bought: 1 times Market Maker Sold: 1 times
Production Considerations
Deploying an Avellaneda-Stoikov market making strategy in a real-world production environment requires careful consideration of several practical aspects beyond the theoretical model. These include latency, order management, capital management, and robust error handling. Below is a table outlining key best practices for production deployment.
| Aspect | Best Practice | Description |
|---|---|---|
| Low Latency Infrastructure | Collocate servers with exchange matching engines | Minimizes network delays, crucial for reacting quickly to market events and maintaining competitive quotes. |
| Robust Order Management | Implement idempotent order placement/cancellation | Ensures that duplicate messages don't lead to erroneous orders, and allows for safe retry mechanisms for order actions. Use FOK/IOC orders. |
| Capital and Risk Limits | Set strict capital allocation and inventory limits | Prevents overexposure to market risk and ensures the strategy operates within acceptable financial boundaries. |
| Monitoring & Alerting | Real-time dashboards and automated alerts | Track PnL, inventory, quote activity, system health, and unexpected market conditions. Trigger alerts for deviations or system failures. |
| Parameter Optimization | Adaptive parameter tuning / Machine Learning | Dynamically adjust $\gamma$, $\kappa$, and $\lambda_0$ based on real-time market microstructure, rather than static values. |
| Market Data Feed | Redundant, normalized, and high-fidelity data | Use multiple data sources and ensure data quality, consistency, and low-latency processing. |
| Error Handling & Retries | Implement try/except with exponential backoff | For API calls, database operations, or any external dependencies, use robust error handling and intelligent retry logic to prevent failures. |
| Simulation & Backtesting | Continuous out-of-sample testing | Regularly test the strategy against historical and simulated market data, including various stress scenarios, to validate robustness. |
| Liquidation Strategy | Define clear inventory liquidation rules | Have a plan to unwind significant inventory imbalances, especially towards the end of the trading day or in volatile conditions. |
| Regulatory Compliance | Adhere to exchange rules and financial regulations | Ensure the market making activity complies with all relevant legal and exchange requirements. |
Conclusion
This notebook has provided a comprehensive implementation and simulation of the Avellaneda-Stoikov market making model. We began by establishing the theoretical foundation, followed by a modular breakdown of core functions for initializing the market state, calculating the reservation price and optimal spread, determining bid/ask quotes, simulating order arrivals, and executing trades. The simulation vividly demonstrated how a market maker adjusts their quotes and manages inventory in a dynamic environment.
Key takeaways include:
- The model's ability to dynamically adjust quoting strategies based on inventory levels and time to maturity.
- The trade-off between maximizing spread capture and minimizing inventory risk, governed by the risk aversion parameter $\gamma$.
- The importance of market microstructure parameters like volatility ($\sigma$) and order book liquidity ($\kappa$) in determining optimal quotes.
While this simulation provides a solid theoretical understanding, practical deployment necessitates advanced considerations such as latency optimization, robust error handling, adaptive parameter tuning, and comprehensive monitoring, as outlined in the production considerations section. The Avellaneda-Stoikov model remains a cornerstone for developing more sophisticated market making strategies in modern financial markets.