Building a Market Making Algorithm for Crypto Pairs Using the Avellaneda-Stoikov Model
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:
δ_aandδ_bare the ask and bid pricesS_tis the current market priceγis the risk parameter (the higher it is, the wider the spread)kis the order arrival rateq_tis the current inventoryσis the volatilityTis the time horizon
Onchain Trading Features
When we move the algorithm onchain, additional challenges arise:
- Latency – transactions on the blockchain are not instant, and the price may change before the order is executed
- Gas costs – every transaction requires a network fee
- 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_termis the profit from tradesinventory_riskis a penalty for large inventory (risk)gas_costis 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:
- Connect to real APIs: replace stubs with real requests to Binance API and Ethereum node
- Improve volatility model: use GARCH or other advanced models
- Expand PPO: add more parameters to state and action
- Optimize gas: strategies to minimize gas costs
- 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
퀀트 리서치 및 전략