← Volver a los artículos
May 17, 2025
5 min de lectura

Construyendo un algoritmo de market making para pares cripto usando el modelo Avellaneda-Stoikov

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

¡Hola, amigos! Hoy os mostraré cómo construir un algoritmo de market making para los pares cripto USD+/wETH y USD+/cbbtc. Usaremos el modelo Avellaneda-Stoikov (A-S) y lo mejoraremos con un algoritmo de Reinforcement Learning (PPO) para la optimización dinámica del spread. ¿Suena complicado? No os preocupéis, desglosaré todo en pasos claros para que hasta un desarrollador principiante pueda seguirlo.

Market Making Visualization Visualizando la esencia del market making: equilibrar continuamente órdenes de compra y venta en torno a un precio justo para sintetizar una liquidez de mercado óptima.

¿Qué es el market making y por qué lo necesitamos?

El market making es una estrategia en la que un trader coloca simultáneamente órdenes de compra y venta de un activo, ganando con el spread (la diferencia entre precios). En el espacio DeFi, los market makers desempeñan un papel clave al proporcionar liquidez y reducir el slippage para otros participantes del mercado.

Imaginad que sois un vendedor en un mercado, siempre dispuestos a comprar un producto ligeramente por debajo del precio de mercado y venderlo ligeramente por encima. Vuestro beneficio es la diferencia entre el precio de compra y el de venta. Pero hay una trampa: si el precio se mueve de repente en una dirección, podéis acumular demasiado inventario o, al contrario, quedaros sin nada que vender.

El modelo Avellaneda-Stoikov: matemáticas al servicio del trading

El modelo A-S es un enfoque matemático para determinar los precios óptimos en el market making. Su principal ventaja es que tiene en cuenta no solo el precio de mercado actual, sino también el tamaño de vuestra posición (inventario), la volatilidad del mercado y el apetito por el riesgo.

Las fórmulas principales del modelo:

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

donde:

  • δ_a y δ_b son los precios de venta (ask) y compra (bid)
  • S_t es el precio de mercado actual
  • γ es el parámetro de riesgo (cuanto más alto, más amplio el spread)
  • k es la tasa de llegada de órdenes
  • q_t es el inventario actual
  • σ es la volatilidad
  • T es el horizonte temporal

Particularidades del trading onchain

Cuando trasladamos el algoritmo onchain, surgen desafíos adicionales:

  1. Latencia – las transacciones en la blockchain no son instantáneas, y el precio puede cambiar antes de que se ejecute la orden
  2. Costes de gas – cada transacción requiere una comisión de red
  3. Particularidades de AMM/PMM – la mecánica de los pools de liquidez difiere de la de las exchanges tradicionales

Veamos cómo tener en cuenta estos factores en nuestro algoritmo.

Paso 1: Configurar el entorno y recopilar datos

Primero, necesitamos configurar un entorno para obtener datos de mercado. Usaremos la API de Binance para obtener los precios actuales y la profundidad del libro de órdenes.

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

También necesitaremos métricas onchain como el coste de gas y la latencia de red:

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

Paso 2: Implementar el modelo A-S básico

Ahora implementemos el cálculo del spread usando el 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};
}

Fijaos en el inventory_term. Si tenéis un inventario positivo (mucho del activo), el precio de venta disminuye, y el precio de compra disminuye aún más para fomentar la venta y limitar la compra. Y viceversa para el inventario negativo.

Paso 3: Adaptar el modelo para el trading onchain

Ahora necesitamos tener en cuenta las particularidades de la blockchain. Empecemos por la latencia:

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

Aquí usamos un modelo de random walk: cuanto mayor sea la volatilidad y más larga la latencia, más puede cambiar el precio antes de que se ejecute la orden.

Ahora tengamos en cuenta el coste 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
}

Finalmente, adaptemos los spreads a las particularidades de los 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};
}

Paso 4: Gestión de inventario

Para rastrear y gestionar el inventario, creemos una clase simple:

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 Visualización del riesgo de inventario: monitorización del tamaño de las posiciones para evitar una exposición excesiva (larga o corta) a movimientos de mercado unidireccionales.

Paso 5: Combinar todo en un único algoritmo

Ahora combinemos todos los componentes en un ú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_;
};

El estado de nuestro entorno es un vector con el precio actual, el inventario, la volatilidad, la latencia de red y la profundidad del pool. La acción es un vector de spreads y tamaños de compra/venta.

Ahora implementemos la función de recompensa:

double reward = profit_term - inventory_risk - gas_cost;

Donde:

  • profit_term es el beneficio de las operaciones
  • inventory_risk es una penalización por un inventario elevado (riesgo)
  • gas_cost es el gas gastado

Por último, entrenemos al 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 en acción: el agente PPO procesa estados de mercado complejos para optimizar dinámicamente los spreads de compra/venta y maximizar la recompensa esperada.

Paso 7: Pruebas y visualización

Para probar nuestro algoritmo, creemos una simulación simple:

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

¿Qué sigue?

Nuestro algoritmo de market making está listo, pero hay muchas formas de mejorarlo:

  1. Conectar con APIs reales: sustituir los stubs por peticiones reales a la API de Binance y a un nodo de Ethereum
  2. Mejorar el modelo de volatilidad: usar GARCH u otros modelos avanzados
  3. Ampliar PPO: añadir más parámetros al estado y a la acción
  4. Optimizar el gas: estrategias para minimizar los costes de gas
  5. Estrategia multiactivo: expandirse a varios pares a la vez

Conclusión

Hemos construido un algoritmo de market making que tiene en cuenta las particularidades del trading onchain y que utiliza tanto el modelo A-S clásico como métodos modernos de RL. Este enfoque permite adaptarse a las condiciones cambiantes del mercado y maximizar el beneficio controlando el riesgo.

Por supuesto, en el trading real hay muchos factores adicionales a considerar, pero nuestro algoritmo proporciona una base sólida para un desarrollo posterior. Recordad: en el trading algorítmico no solo importan las matemáticas, sino también unas pruebas exhaustivas, la monitorización y la optimización constante.

Espero que este artículo os haya ayudado a comprender mejor los principios del market making y os haya inspirado a crear vuestros propios algoritmos. ¡Buena suerte con el trading!

Cita

@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

Mantente a la vanguardia

Suscríbete a nuestro boletín para recibir información exclusiva sobre trading con IA, análisis de mercado y actualizaciones de la plataforma.

Respetamos tu privacidad. Puedes darte de baja en cualquier momento.