High-Frequency Trading and Latency Arbitrage › Market Making at High Frequency

Michael BrenndoerferJanuary 1, 20263 min read
Quantitative FinanceAll chapters
Part 12 of 15Market Making and High-Frequency Trading
Chapter 66High-Frequency Trading and Latency Arbitrage › Market Making at High Frequency
Reading section 11 of 18 · Market Making at High Frequency · 3 minSection 11 of 18 · 3 min

Book contents

Section 11 of 18 · 3 min read

High-Frequency Trading and Latency Arbitrage › Market Making at High FrequencyOne section · Sections in this chapter11 / 18 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Many HFT firms operate as electronic market makers, building on the principles we covered in the previous chapter but implementing them at much higher speeds and with more sophisticated risk management.

Quote Management

High-frequency market makers continuously update their quotes to reflect changing market conditions. A key challenge is managing the risk of being "picked off," which occurs when your stale quotes are executed against by faster traders who have already seen a price change.

The quote update process involves these steps:

  1. Receiving market data (prices, order flow, news)
  2. Updating fair value estimate
  3. Calculating new bid and ask quotes
  4. Sending cancel messages for old quotes
  5. Sending new quote messages

The entire cycle must complete faster than competitors can trade against your stale quotes.

In[23]:
Code
def simulate_hft_market_maker(
    n_periods: int = 1000,
    true_price_vol: float = 0.10,  # Price volatility per period (percentage)
    spread_bps: float = 2.0,
    inventory_limit: int = 1000,
    fill_probability: float = 0.3,
    adverse_selection_bps: float = 0.5,
):
    """
    Simulate a high-frequency market maker.

    Parameters:
    -----------
    n_periods : Number of time periods
    true_price_vol : True price volatility per period (%)
    spread_bps : Bid-ask spread in basis points
    inventory_limit : Maximum inventory position
    fill_probability : Probability of quote being filled each period
    adverse_selection_bps : Loss per fill due to adverse selection
    """
    np.random.seed(42)

    # Initialize
    inventory = 0
    cash = 0
    pnl_history = []
    inventory_history = []

    # Starting price
    price = 100.0

    for t in range(n_periods):
        # True price evolution (random walk)
        price_change = np.random.normal(0, true_price_vol)
        price = price * (1 + price_change / 100)

        # Market maker quotes around estimated fair value
        # (slight lag means some adverse selection)
        estimated_price = price * (
            1 - np.random.normal(0, adverse_selection_bps / 10000)
        )

        half_spread = spread_bps / 10000 / 2 * estimated_price
        bid = estimated_price - half_spread
        ask = estimated_price + half_spread

        # Adjust for inventory (lean against position)
        # Inventory skewing formula: shift = (inventory / limit) * spread * factor
        inventory_skew = (inventory / inventory_limit) * half_spread * 0.5
        bid -= inventory_skew
        ask -= inventory_skew

        # Random fills
        bid_fill = (
            np.random.random() < fill_probability
            and inventory < inventory_limit
        )
        ask_fill = (
            np.random.random() < fill_probability
            and inventory > -inventory_limit
        )

        # Process fills
        if bid_fill:
            inventory += 100
            cash -= bid * 100
        if ask_fill:
            inventory -= 100
            cash += ask * 100

        # Mark to market
        mtm_value = cash + inventory * price
        pnl_history.append(mtm_value)
        inventory_history.append(inventory)

    pnl_history = np.array(pnl_history)
    inventory_history = np.array(inventory_history)

    # Calculate returns
    returns = np.diff(pnl_history)

    return {
        "final_pnl": pnl_history[-1],
        "pnl_history": pnl_history,
        "inventory_history": inventory_history,
        "sharpe": returns.mean() / returns.std() * np.sqrt(252 * n_periods)
        if returns.std() > 0
        else 0,
        "max_drawdown": np.min(
            pnl_history - np.maximum.accumulate(pnl_history)
        ),
        "avg_inventory": np.abs(inventory_history).mean(),
    }


mm_results = simulate_hft_market_maker()
Out[24]:
Console
HFT Market Maker Simulation
========================================
Final P&L: $1,848.75
Sharpe Ratio: 15.19
Max Drawdown: $-1,114.19
Average Absolute Inventory: 552 shares

The simulation finishes with a profit, but its path is volatile rather than steadily increasing. Mark-to-market P&L peaks around the middle of the run and then gives back part of those gains. Inventory also spends extended periods at the 1,000-share limit before later crossing below zero. These outputs show that the current quote-skewing rule does not consistently maintain neutral inventory and that the final profit should be interpreted alongside substantial inventory and drawdown risk.