Construindo um algoritmo de market making para pares cripto usando o modelo Avellaneda-Stoikov
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.
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:
δ_aeδ_bsã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 ordensq_té o inventário atualσé a volatilidadeTé o horizonte temporal
Particularidades do trading onchain
Quando movemos o algoritmo para onchain, surgem desafios adicionais:
- 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
- Custos de gas – cada transação exige uma taxa de rede
- 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_;
};
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çõesinventory_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);
}
}
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:
- Conectar a APIs reais: substituir os stubs por requisições reais à API da Binance e a um nó Ethereum
- Melhorar o modelo de volatilidade: usar GARCH ou outros modelos avançados
- Expandir o PPO: adicionar mais parâmetros ao estado e à ação
- Otimizar o gas: estratégias para minimizar os custos de gas
- 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.}
}
Authors
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.