AlgoDaily

Market Making & Liquidity Provision

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

From Taking To Making

Why this lesson matters

In Core Execution Algorithms (TWAP, VWAP, POV), you learned how an algo can trade a target quantity while managing market impact. Those algos often consume liquidity.

Market making flips the role: you continuously post both a bid and an ask, trying to earn the spread while controlling inventory and adverse selection risk.

A simplified market maker loop:

rebuilt order book -> fair value -> quote prices/sizes -> risk checks -> OMS -> exchange

You will use the order book from Market Data Handling & Order Book Reconstruction, the order states from Order Management & FIX Integration, and the latency discipline from earlier lessons.

From Taking To Making

Quote Anatomy

The two-sided quote

A market maker usually posts:

bid = price where we buy
ask = price where we sell
spread = ask - bid
mid = (bid + ask) / 2

If the current top of book is:

best bid: 99.98
best ask: 100.02
mid:      100.00
spread:   0.04

A simple market maker might quote around a fair value estimate:

our bid = fair value - half spread - inventory skew
our ask = fair value + half spread - inventory skew

The key idea: market making is not just 'buy low, sell high.' It is continuously choosing prices that balance fill probability, spread capture, and risk.

Quote Anatomy

Liquidity Role Check

A market maker is primarily trying to do which of the following?

Tiny Quote Engine

Build the first quote function

Below is a tiny C++ quote engine. It takes:

  • a fair value estimate
  • a desired half-spread
  • current inventory
  • max inventory

Then it skews prices down when long and up when short. This encourages trades that move inventory back toward zero.

This is intentionally small, but the shape resembles what production systems do before sending quote updates through the OMS.

```cpp
#include <iostream>
#include <iomanip>
#include <vector>

struct Quote {
    double bid;
    double ask;
    int bid_size;
    int ask_size;
};

Quote make_quote(double fair, double half_spread, int inventory, int max_inventory) {
    // Positive inventory means we are long. Shift quotes lower:
    // - lower bid discourages buying more
    // - lower ask encourages selling inventory
    double inventory_ratio = static_cast<double>(inventory) / max_inventory;
    double skew = inventory_ratio * 0.05; // 5 cents max skew at full inventory

    Quote q;
    q.bid = fair - half_spread - skew;
    q.ask = fair + half_spread - skew;

    q.bid_size = (inventory > 0.7 * max_inventory) ? 10 : 100;
    q.ask_size = (inventory < -0.7 * max_inventory) ? 10 : 100;
    return q;
}

int main() {
    double fair = 100.00;
    double half_spread = 0.02;
    int max_inventory = 100;

    std::vector<int> inventories = {-80, 0, 80};

    std::cout << std::fixed << std::setprecision(2);
    for (int inv : inventories) {
        Quote q = make_quote(fair, half_spread, inv, max_inventory);
        std::cout << "inventory=" << inv
                  << " bid=" << q.bid << " x " << q.bid_size
                  << " ask=" << q.ask << " x " << q.ask_size
                  << '\n';
    }

    return 0;
}
```

Inventory Is Gravity

Inventory pulls on every decision

If you buy too much, you become long. If price falls, your inventory loses value. If you sell too much, you become short. If price rises, you lose.

So a maker normally changes quotes as inventory moves:

Too long  -> lower bid, lower ask, maybe smaller bid size
Too short -> higher bid, higher ask, maybe smaller ask size
Near flat -> quote more symmetrically

Think of inventory as gravity: the farther you drift from zero, the harder your quote engine should pull you back.

Common controls:

  • max position limit
  • per-symbol inventory limit
  • quote size reduction near limits
  • cancel all quotes if limits are breached
  • separate emergency kill switch

Inventory Is Gravity

Risk Statement

True or false: A market maker can ignore inventory risk if its average spread capture is positive.

Spread Versus Markout

A fill is not automatically profit

When your ask is lifted, you sell. When your bid is hit, you buy. The immediate spread looks attractive, but the next price move matters.

A common diagnostic is markout: after a fill, where did the mid price move?

  • Buy at bid, then mid rises: good
  • Buy at bid, then mid falls: bad
  • Sell at ask, then mid falls: good
  • Sell at ask, then mid rises: bad

The code below shows how inventory and mark-to-market PnL can change after fills and a mid-price move.

```cpp
#include <iostream>
#include <iomanip>
#include <vector>
#include <string>

struct Event {
    std::string type;
    double price;
};

int main() {
    double mid = 100.00;
    double bid = 99.99;
    double ask = 100.01;
    int inventory = 0;
    double cash = 0.0;

    std::vector<Event> events = {
        {"seller_hits_our_bid", bid},   // we buy 1
        {"mid_moves_down", 99.95},
        {"buyer_lifts_our_ask", ask},   // we sell 1
        {"seller_hits_our_bid", bid}    // we buy 1
    };

    std::cout << std::fixed << std::setprecision(2);

    for (const auto& e : events) {
        if (e.type == "seller_hits_our_bid") {
            inventory += 1;
            cash -= e.price;
            std::cout << "Bought 1 at bid " << e.price << '\n';
        } else if (e.type == "buyer_lifts_our_ask") {
            inventory -= 1;
            cash += e.price;
            std::cout << "Sold 1 at ask " << e.price << '\n';
        } else if (e.type == "mid_moves_down") {
            mid = e.price;
            std::cout << "Mid moved to " << mid << '\n';
        }

        double mtm_pnl = cash + inventory * mid;
        std::cout << "  inventory=" << inventory
                  << " cash=" << cash
                  << " mark_to_market_pnl=" << mtm_pnl << "\n\n";
    }

    return 0;
}
```

Adverse Selection

The danger of being picked off

Adverse selection happens when your resting quote is filled because someone else knows, or reacts to, a price change before you do.

Example:

1. You quote ask = 100.01
2. Market value jumps to 100.08
3. Your old ask is still live
4. Someone buys from you at 100.01
5. You sold too cheaply

This is why market makers care about:

  • feed freshness and sequence health
  • fast cancel/replace behavior
  • not quoting during crossed, locked, or unstable books
  • widening quotes during uncertainty
  • reducing size when latency or volatility rises

We will later test these ideas in replay and simulation, but today the focus is the live decision model.

Adverse Selection

Skew Direction

A market maker that is too long usually shifts both quotes ____ to discourage buying more and encourage selling inventory.

Guarded Quote Path

Never quote without guardrails

A production quote is not just a price. It must pass risk checks before the OMS sends or replaces orders.

Typical quote gate questions:

  • Is market data fresh?
  • Is the book valid and not crossed?
  • Is spread wide enough to justify quoting?
  • Are order sizes within limits?
  • Would a fill breach max position?

Here is a compact C++ risk gate that accepts or rejects proposed quotes.

```cpp
#include <iostream>
#include <string>

struct ProposedQuote {
    double bid;
    double ask;
    int bid_size;
    int ask_size;
    long quote_age_us;
};

struct RiskConfig {
    int max_order_qty;
    double min_spread;
    long max_quote_age_us;
};

bool validate(const ProposedQuote& q, const RiskConfig& cfg, std::string& reason) {
    if (q.bid >= q.ask) {
        reason = "bid must be below ask";
        return false;
    }
    if (q.ask - q.bid < cfg.min_spread) {
        reason = "spread too tight";
        return false;
    }
    if (q.bid_size > cfg.max_order_qty || q.ask_size > cfg.max_order_qty) {
        reason = "order size too large";
        return false;
    }
    if (q.quote_age_us > cfg.max_quote_age_us) {
        reason = "market data is stale";
        return false;
    }
    reason = "accepted";
    return true;
}

int main() {
    RiskConfig cfg{100, 0.01, 500};

    ProposedQuote good{99.98, 100.02, 50, 50, 120};
    ProposedQuote stale{99.99, 100.00, 50, 50, 900};

    for (const auto& q : {good, stale}) {
        std::string reason;
        bool ok = validate(q, cfg, reason);
        std::cout << (ok ? "ACCEPT " : "REJECT ") << reason
                  << " bid=" << q.bid << " ask=" << q.ask << '\n';
    }

    return 0;
}
```
Open interactive view →