← Voltar aos artigos
May 17, 2025
5 min read

Construindo um algoritmo de market making para pares cripto usando o modelo Avellaneda-Stoikov

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

Olá, amigos! Hoje vou mostrar como construir um algoritmo de market making para os pares cripto USD+/wETH e USD+/cbbtc. Usaremos o modelo Avellaneda-Stoikov (A-S) e o aprimoraremos com um algoritmo de Reinforcement Learning (PPO) para otimização dinâmica de spread. Parece complicado? Não se preocupem, vou dividir tudo em passos claros para que até um desenvolvedor iniciante consiga acompanhar.

Market Making Visualization Visualizando a essência do market making: equilibrando continuamente ordens de compra e venda em torno de um preço justo para sintetizar a liquidez de mercado ideal.

O que é market making e por que precisamos dele?

Market making é uma estratégia em que um trader coloca simultaneamente ordens de compra e venda para um ativo, lucrando com o spread (a diferença entre os preços). No espaço DeFi, os market makers desempenham um papel fundamental ao fornecer liquidez e reduzir o slippage para outros participantes do mercado.

Imaginem que vocês são um vendedor em um mercado, sempre prontos para comprar um produto um pouco abaixo do preço de mercado e vendê-lo um pouco acima. O lucro de vocês é a diferença entre os preços de compra e venda. Mas há uma pegadinha: se o preço se mover repentinamente em uma direção, vocês podem acumular inventário demais ou, ao contrário, ficar sem nada para vender.

O modelo Avellaneda-Stoikov: matemática a serviço do trading

O modelo A-S é uma abordagem matemática para determinar preços ótimos para market making. Sua principal vantagem é que ele leva em conta não apenas o preço de mercado atual, mas também o tamanho da posição de vocês (inventário), a volatilidade do mercado e o apetite ao risco.

As principais fórmulas do modelo:

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

onde:

  • δ_a e δ_b são os preços de venda (ask) e compra (bid)
  • S_t é o preço de mercado atual
  • γ é o parâmetro de risco (quanto maior, mais amplo o spread)
  • k é a taxa de chegada de ordens
  • q_t é o inventário atual
  • σ é a volatilidade
  • T é o horizonte temporal

Particularidades do trading onchain

Quando movemos o algoritmo para onchain, surgem desafios adicionais:

  1. Latência – as transações na blockchain não são instantâneas, e o preço pode mudar antes que a ordem seja executada
  2. Custos de gas – cada transação exige uma taxa de rede
  3. Particularidades de AMM/PMM – a mecânica dos pools de liquidez difere das exchanges tradicionais

Vamos ver como considerar esses fatores em nosso algoritmo.

Passo 1: Configurar o ambiente e coletar dados

Primeiro, precisamos configurar um ambiente para obter dados de mercado. Usaremos a API da Binance para obter os preços atuais e a profundidade do livro de ordens.

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};
}

Também precisaremos de métricas onchain, como custo de gas e latência de rede:

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};
}

Passo 2: Implementar o modelo A-S básico

Agora vamos implementar o cálculo do spread usando o modelo A-S:

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};
}

Observem o inventory_term. Se vocês têm um inventário positivo (muito do ativo), o preço de venda diminui, e o preço de compra diminui ainda mais para incentivar a venda e limitar a compra. E vice-versa para inventário negativo.

Passo 3: Adaptar o modelo para o trading onchain

Agora precisamos considerar as particularidades da blockchain. Vamos começar pela latência:

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;
}

Aqui usamos um modelo de random walk: quanto maior a volatilidade e mais longa a latência, mais o preço pode mudar antes que a ordem seja executada.

Agora vamos considerar o custo de gas:

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
}

Por fim, vamos adaptar os spreads às particularidades dos pools PMM:

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};
}

Passo 4: Gestão de inventário

Para rastrear e gerenciar o inventário, vamos criar uma classe simples:

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_;
};

Inventory management and risk visualization Visualização do risco de inventário: monitoramento do tamanho das posições para evitar exposição excessiva (comprada ou vendida) a movimentos de mercado unidirecionais.

Passo 5: Combinar tudo em um único algoritmo

Agora vamos combinar todos os componentes em um único algoritmo de market making:

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_;
};

O estado do nosso ambiente é um vetor com o preço atual, o inventário, a volatilidade, a latência de rede e a profundidade do pool. A ação é um vetor de spreads e tamanhos de compra/venda.

Agora vamos implementar a função de recompensa:

double reward = profit_term - inventory_risk - gas_cost;

Onde:

  • profit_term é o lucro das operações
  • inventory_risk é uma penalidade por inventário elevado (risco)
  • gas_cost é o gas gasto

Por fim, vamos treinar o agente PPO:

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);
    }
}

PPO Reinforcement Learning agent for spread optimization Reinforcement Learning em ação: o agente PPO processa estados de mercado complexos para otimizar dinamicamente os spreads de compra/venda visando a máxima recompensa esperada.

Passo 7: Testes e visualização

Para testar nosso algoritmo, vamos criar uma simulação simples:

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;
}

O que vem a seguir?

Nosso algoritmo de market making está pronto, mas há muitas maneiras de melhorá-lo:

  1. Conectar a APIs reais: substituir os stubs por requisições reais à API da Binance e a um nó Ethereum
  2. Melhorar o modelo de volatilidade: usar GARCH ou outros modelos avançados
  3. Expandir o PPO: adicionar mais parâmetros ao estado e à ação
  4. Otimizar o gas: estratégias para minimizar os custos de gas
  5. Estratégia multiativos: expandir para vários pares ao mesmo tempo

Conclusão

Construímos um algoritmo de market making que leva em conta as particularidades do trading onchain e utiliza tanto o modelo A-S clássico quanto métodos modernos de RL. Essa abordagem permite adaptar-se a condições de mercado em mudança e maximizar o lucro enquanto controla o risco.

Claro, no trading real há muitos fatores adicionais a considerar, mas nosso algoritmo fornece uma base sólida para desenvolvimento futuro. Lembrem-se: no trading algorítmico, não é só a matemática que importa, mas também testes rigorosos, monitoramento e otimização constante.

Espero que este artigo tenha ajudado vocês a entender melhor os princípios do market making e os tenha inspirado a criar seus próprios algoritmos. Boa sorte no trading!

Citação

@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.}
}
blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Fique à frente do mercado

Assine nossa newsletter para insights exclusivos sobre trading com IA, análises de mercado e atualizações da plataforma.

Respeitamos sua privacidade. Cancele a inscrição a qualquer momento.