Construyendo un algoritmo de market making para pares cripto usando el modelo Avellaneda-Stoikov
¡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.
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:
δ_ayδ_bson los precios de venta (ask) y compra (bid)S_tes el precio de mercado actualγes el parámetro de riesgo (cuanto más alto, más amplio el spread)kes la tasa de llegada de órdenesq_tes el inventario actualσes la volatilidadTes el horizonte temporal
Particularidades del trading onchain
Cuando trasladamos el algoritmo onchain, surgen desafíos adicionales:
- Latencia – las transacciones en la blockchain no son instantáneas, y el precio puede cambiar antes de que se ejecute la orden
- Costes de gas – cada transacción requiere una comisión de red
- 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_;
};
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_termes el beneficio de las operacionesinventory_riskes una penalización por un inventario elevado (riesgo)gas_costes 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);
}
}
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:
- Conectar con APIs reales: sustituir los stubs por peticiones reales a la API de Binance y a un nodo de Ethereum
- Mejorar el modelo de volatilidad: usar GARCH u otros modelos avanzados
- Ampliar PPO: añadir más parámetros al estado y a la acción
- Optimizar el gas: estrategias para minimizar los costes de gas
- 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.}
}
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.