기사 목록으로
May 17, 2025
5분 소요

Building a Market Making Algorithm for Crypto Pairs Using the Avellaneda-Stoikov Model

market making
cryptocurrency
Avellaneda-Stoikov
algorithmic trading
reinforcement learning
PPO
DeFi

Hello, friends! Today I'll show you how to build a market making algorithm for the USD+/wETH and USD+/cbbtc crypto pairs. We'll use the Avellaneda-Stoikov (A-S) model and enhance it with a Reinforcement Learning (PPO) algorithm for dynamic spread optimization. Sounds complicated? Don't worry, I'll break everything down into clear steps so even a beginner developer can follow along.

What is Market Making and Why Do We Need It?

Market making is a strategy where a trader simultaneously places buy and sell orders for an asset, earning on the spread (the difference between prices). In the DeFi space, market makers play a key role by providing liquidity and reducing slippage for other market participants.

Imagine you're a vendor at a market, always ready to buy a product slightly below the market price and sell it slightly above. Your profit is the difference between your buy and sell prices. But there's a catch: if the price suddenly moves in one direction, you might accumulate too much inventory or, conversely, be left with nothing to sell.

The Avellaneda-Stoikov Model: Math in the Service of Trading

The A-S model is a mathematical approach to determining optimal prices for market making. Its main advantage is that it takes into account not only the current market price, but also your position size (inventory), market volatility, and risk appetite.

The main formulas of the model:

δ_a = S_t + (1/γ) * ln(1 + γ/k) + q_t * σ² * T
δ_b = S_t - (1/γ) * ln(1 + γ/k) - q_t * σ² * T

where:

  • δ_a and δ_b are the ask and bid prices
  • S_t is the current market price
  • γ is the risk parameter (the higher it is, the wider the spread)
  • k is the order arrival rate
  • q_t is the current inventory
  • σ is the volatility
  • T is the time horizon

Onchain Trading Features

When we move the algorithm onchain, additional challenges arise:

  1. Latency – transactions on the blockchain are not instant, and the price may change before the order is executed
  2. Gas costs – every transaction requires a network fee
  3. AMM/PMM specifics – liquidity pool mechanics differ from traditional exchanges

Let's see how to account for these factors in our algorithm.

Step 1: Set Up the Environment and Collect Data

First, we need to set up an environment to get market data. We'll use the Binance API to get current prices and order book depth.

std::tuple MarketMaker::get_binance_data(const std::string& pair) {
    // In real code, this would be a request to the Binance API
    // Returns: mid_price, bid, ask, bid_volume, ask_volume
    double mid_price = 2000.0;
    double bid = mid_price - 1.0;
    double ask = mid_price + 1.0;
    double bid_volume = 10.0;
    double ask_volume = 8.0;
    return {mid_price, bid, ask, bid_volume, ask_volume};
}

We'll also need onchain metrics like gas cost and network latency:

std::pair MarketMaker::get_onchain_metrics() {
    // In real code, this would be a request to an Ethereum node
    // Returns: gas_price (wei), latency (seconds)
    return {50e9, 12.0};
}

Step 2: Implement the Basic A-S Model

Now let's implement spread calculation using the A-S model:

std::pair MarketMaker::calculate_spreads(double S_t, double sigma, double k, double q_t) {
    // Avellaneda-Stoikov formula
    double spread_term = (1.0 / gamma_) * log(1.0 + gamma_ / k);
    double inventory_term = q_t * sigma * sigma * T_;
    
    double delta_a = S_t + spread_term + inventory_term;  // Ask price
    double delta_b = S_t - spread_term - inventory_term;  // Bid price
    
    return {delta_a, delta_b};
}

Note the inventory_term. If you have a positive inventory (a lot of the asset), the ask price decreases, and the bid price decreases even more to encourage selling and limit buying. And vice versa for negative inventory.

Step 3: Adapt the Model for Onchain Trading

Now we need to account for blockchain specifics. Let's start with latency:

double MarketMaker::adjust_price_with_latency(double S_t, double sigma, double latency) {
    // Simulate random price change due to latency
    double latency_adjustment = utils::normal_dist(0.0, sigma * std::sqrt(latency));
    return S_t + latency_adjustment;
}

Here we use a random walk model: the higher the volatility and the longer the latency, the more the price can change before the order is executed.

Now let's account for gas cost:

double MarketMaker::calculate_gas_cost(double gas_price, double trade_size) {
    const double GAS_LIMIT_PER_ORDER = 100000;  // Approximate value per order
    return (gas_price * GAS_LIMIT_PER_ORDER * trade_size) / 1e18;  // Convert wei to ETH
}

Finally, let's adapt the spreads for PMM pool specifics:

std::pair MarketMaker::adjust_spreads_for_pmm(double S_t, double delta_a, double delta_b, double pool_depth) {
    // Simplified PMM model: adjust spreads based on pool depth
    const double MIN_POOL_DEPTH = 10.0;
    double depth_factor = std::max(pool_depth, MIN_POOL_DEPTH) / MIN_POOL_DEPTH;
    
    // Reduce spreads with greater pool depth
    double spread_reduction = 1.0 / std::sqrt(depth_factor);
    double mid_price = (delta_a + delta_b) / 2;
    double new_delta_a = mid_price + (delta_a - mid_price) * spread_reduction;
    double new_delta_b = mid_price - (mid_price - delta_b) * spread_reduction;
    
    return {new_delta_a, new_delta_b};
}

Step 4: Inventory Management

To track and manage inventory, let's create a simple class:

class InventoryManager {
public:
    InventoryManager() : inventory_(0.0) {}
    
    void update_inventory(double size, bool is_buy) {
        inventory_ += is_buy ? size : -size;
    }
    
    double get_inventory() const {
        return inventory_;
    }
    
private:
    double inventory_;
};

Step 5: Combine Everything into a Single Algorithm

Now let's combine all the components into a single market making algorithm:

void MarketMaker::step(double S_t, double sigma, double k, double latency, double gas_cost, double trade_size) {
    // Get current inventory
    double current_inventory = inventory_.get_inventory();
    
    // Calculate spreads based on current market conditions and inventory
    auto [delta_a, delta_b] = calculate_spreads(S_t, sigma, k, current_inventory);
    auto [adjusted_delta_a, adjusted_delta_b] = adjust_spreads_for_onchain(S_t, delta_a, delta_b, latency, sigma, gas_cost, trade_size);
    
    // Generate independent market price
    double market_price = S_t + utils::normal_dist(0.0, sigma);
    
    // Determine if trades should occur based on market price and spreads
    bool is_buy = (market_price = adjusted_delta_a);
    
    // Execute trades and update inventory
    if (is_buy) {
        inventory_.update_inventory(trade_size, true);
        std::cout  reset();
    
    // Take action and get new state, reward, and done flag
    std::tuple, double, bool> step(const std::array& action);
    
private:
    // Get current environment state
    std::vector get_state() const;
    
    MarketMaker& mm_;
    double current_inventory_;
    double current_profit_;
    int current_step_;
    int max_steps_;
    
    // Current market parameters
    double mid_price_;
    double sigma_;
    double latency_;
    double pool_depth_;
    
    std::mt19937 rng_;
};

Our environment state is a vector of the current price, inventory, volatility, network latency, and pool depth. The action is a vector of spreads and buy/sell sizes.

Now let's implement the reward function:

double reward = profit_term - inventory_risk - gas_cost;

Where:

  • profit_term is the profit from trades
  • inventory_risk is a penalty for large inventory (risk)
  • gas_cost is the gas spent

Finally, let's train the PPO agent:

void PPOTrainer::train(int episodes) {
    for (int ep = 0; ep  states;
        std::vector actions;
        std::vector rewards;
        
        while (true) {
            // Get action from policy
            auto action_probs = policy_net_->forward(torch::tensor(state));
            auto action = action_probs.multinomial(1);
            
            // Take a step in the environment
            auto [next_state, reward, done] = env_.step(action);
            
            // Save transition
            states.push_back(torch::tensor(state));
            actions.push_back(action);
            rewards.push_back(reward);
            
            if (done) break;
            state = next_state;
        }
        
        // Update PPO policy
        update_policy(states, actions, rewards);
    }
}

Step 7: Testing and Visualization

To test our algorithm, let's create a simple simulation:

int main() {
    // Use T = 300 seconds as specified in the task
    MarketMaker mm(0.1, 300.0);

    // Simulate historical data for volatility
    std::vector prices = {2000.0};
    double S_t = 2000.0;
    double trade_size = 1.0;
    double initial_sigma = 0.05;  // 5% volatility

    for (int i = 0; i < 300; ++i) {
        std::cout << "Step " << i + 1 << ": ";

        // Get data (stubs)
        auto [mid_price, bid_ask] = mm.get_binance_data("USD+/wETH");
        auto [gas_cost, latency] = mm.get_onchain_metrics();
        
        // Add random price movement to simulate a real market
        S_t = mid_price + utils::normal_dist(0.0, mid_price * 0.01);

        // Calculate volatility
        double sigma = mm.calculate_volatility(prices, 5);
        if (sigma < 0.01) sigma = initial_sigma;

        // Order arrival rate (stub)
        double k = 5.0;

        mm.step(S_t, sigma, k, latency, gas_cost, trade_size);

        // Update price for next step
        S_t += utils::normal_dist(0.0, S_t * 0.02);
        prices.push_back(S_t);
    }

    return 0;
}

What's Next?

Our market making algorithm is ready, but there are many ways to improve it:

  1. Connect to real APIs: replace stubs with real requests to Binance API and Ethereum node
  2. Improve volatility model: use GARCH or other advanced models
  3. Expand PPO: add more parameters to state and action
  4. Optimize gas: strategies to minimize gas costs
  5. Multi-asset strategy: expand to several pairs at once

Conclusion

We've built a market making algorithm that takes into account onchain trading features and uses both the classic A-S model and modern RL methods. This approach allows you to adapt to changing market conditions and maximize profit while controlling risk.

Of course, in real trading there are many additional factors to consider, but our algorithm provides a solid foundation for further development. Remember: in algorithmic trading, not only math is important, but also thorough testing, monitoring, and constant optimization.

I hope this article helped you better understand the principles of market making and inspired you to create your own algorithms. Good luck trading!

Citation

@software{soloviov2025marketmakingavellanedastoikov,
  author = {Soloviov, Eugen},
  title = {Building a Market Making Algorithm for Crypto Pairs Using the Avellaneda-Stoikov Model},
  year = {2025},
  url = {https://marketmaker.cc/en/blog/post/market-making-avellaneda-stoikov},
  version = {0.1.0},
  description = {A step-by-step guide to building a market making algorithm for USD+/wETH and USD+/cbbtc pairs using the Avellaneda-Stoikov model and PPO. Onchain trading features, inventory management, RL training.}
}

MarketMaker.cc Team

퀀트 리서치 및 전략

Telegram에서 토론하기