Een market-making-algoritme bouwen voor crypto-paren met het Avellaneda-Stoikov-model
Hallo, vrienden! Vandaag laat ik jullie zien hoe je een market-making-algoritme bouwt voor de crypto-paren USD+/wETH en USD+/cbbtc. We gebruiken het Avellaneda-Stoikov-model (A-S) en verbeteren het met een Reinforcement Learning-algoritme (PPO) voor dynamische spread-optimalisatie. Klinkt ingewikkeld? Geen zorgen, ik splits alles op in duidelijke stappen zodat zelfs een beginnende developer het kan volgen.
De essentie van market making gevisualiseerd: voortdurend koop- en verkooporders in balans houden rond een eerlijke prijs om optimale marktliquiditeit te synthetiseren.
Wat is market making en waarom hebben we het nodig?
Market making is een strategie waarbij een trader tegelijkertijd koop- en verkooporders voor een asset plaatst en verdient aan de spread (het verschil tussen de prijzen). In de DeFi-ruimte spelen market makers een sleutelrol door liquiditeit te bieden en slippage voor andere marktdeelnemers te verminderen.
Stel je voor dat je een verkoper bent op een markt, altijd bereid om een product iets onder de marktprijs te kopen en iets erboven te verkopen. Je winst is het verschil tussen je koop- en verkoopprijzen. Maar er is een addertje onder het gras: als de prijs plotseling in één richting beweegt, kun je te veel voorraad opbouwen of, andersom, met niets meer te verkopen achterblijven.
Het Avellaneda-Stoikov-model: wiskunde in dienst van trading
Het A-S-model is een wiskundige aanpak om optimale prijzen voor market making te bepalen. Het grootste voordeel is dat het niet alleen rekening houdt met de huidige marktprijs, maar ook met je positiegrootte (voorraad), marktvolatiliteit en risicobereidheid.
De belangrijkste formules van het model:
δ_a = S_t + (1/γ) * ln(1 + γ/k) + q_t * σ² * T
δ_b = S_t - (1/γ) * ln(1 + γ/k) - q_t * σ² * T
waarbij:
δ_aenδ_bde ask- en bid-prijzen zijnS_tde huidige marktprijs isγde risicoparameter is (hoe hoger, hoe breder de spread)kde order-aankomstsnelheid isq_tde huidige voorraad isσde volatiliteit isTde tijdshorizon is
Kenmerken van onchain trading
Wanneer we het algoritme onchain verplaatsen, ontstaan er extra uitdagingen:
- Latentie – transacties op de blockchain zijn niet direct, en de prijs kan veranderen voordat de order wordt uitgevoerd
- Gaskosten – elke transactie vereist een netwerkvergoeding
- AMM/PMM-kenmerken – de mechanica van liquiditeitspools verschilt van traditionele exchanges
Laten we eens kijken hoe we deze factoren in ons algoritme kunnen verwerken.
Stap 1: De omgeving opzetten en data verzamelen
Eerst moeten we een omgeving opzetten om marktdata te verkrijgen. We gebruiken de Binance API om de actuele prijzen en de diepte van het orderboek op te halen.
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 hebben ook onchain-metrics nodig zoals gaskosten en netwerklatentie:
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};
}
Stap 2: Het basis A-S-model implementeren
Laten we nu de spread-berekening implementeren met het 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};
}
Let op de inventory_term. Als je een positieve voorraad hebt (veel van de asset), daalt de ask-prijs, en de bid-prijs daalt nog meer om verkopen te stimuleren en kopen te beperken. En andersom voor een negatieve voorraad.
Stap 3: Het model aanpassen voor onchain trading
Nu moeten we rekening houden met de kenmerken van de blockchain. Laten we beginnen met latentie:
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;
}
Hier gebruiken we een random-walk-model: hoe hoger de volatiliteit en hoe langer de latentie, hoe meer de prijs kan veranderen voordat de order wordt uitgevoerd.
Laten we nu rekening houden met gaskosten:
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
}
Tot slot passen we de spreads aan de kenmerken van de PMM-pool aan:
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};
}
Stap 4: Voorraadbeheer
Om de voorraad bij te houden en te beheren, maken we een eenvoudige klasse:
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_;
};
Visualisatie van voorraadrisico: het monitoren van positiegroottes om buitensporige blootstelling (long of short) aan eenrichtingsmarktbewegingen te voorkomen.
Stap 5: Alles combineren tot één algoritme
Laten we nu alle onderdelen combineren tot één market-making-algoritme:
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_;
};
De staat van onze omgeving is een vector van de huidige prijs, voorraad, volatiliteit, netwerklatentie en pool-diepte. De actie is een vector van spreads en koop-/verkoopgroottes.
Laten we nu de reward-functie implementeren:
double reward = profit_term - inventory_risk - gas_cost;
Waarbij:
profit_termde winst uit trades isinventory_riskeen boete is voor een grote voorraad (risico)gas_costde bestede gas is
Tot slot trainen we de 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);
}
}
Reinforcement Learning in actie: de PPO-agent verwerkt complexe marktstaten om koop-/verkoopspreads dynamisch te optimaliseren voor maximale verwachte beloning.
Stap 7: Testen en visualiseren
Om ons algoritme te testen, maken we een eenvoudige simulatie:
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;
}
Wat nu?
Ons market-making-algoritme is klaar, maar er zijn nog veel manieren om het te verbeteren:
- Verbinden met echte API's: stubs vervangen door echte verzoeken aan de Binance API en een Ethereum-node
- Volatiliteitsmodel verbeteren: GARCH of andere geavanceerde modellen gebruiken
- PPO uitbreiden: meer parameters toevoegen aan staat en actie
- Gas optimaliseren: strategieën om gaskosten te minimaliseren
- Multi-asset-strategie: uitbreiden naar meerdere paren tegelijk
Conclusie
We hebben een market-making-algoritme gebouwd dat rekening houdt met de kenmerken van onchain trading en zowel het klassieke A-S-model als moderne RL-methoden gebruikt. Deze aanpak maakt het mogelijk om je aan te passen aan veranderende marktomstandigheden en de winst te maximaliseren terwijl je het risico beheerst.
Natuurlijk zijn er in echte trading nog veel meer factoren om rekening mee te houden, maar ons algoritme biedt een solide basis voor verdere ontwikkeling. Onthoud: in algoritmische trading is niet alleen wiskunde belangrijk, maar ook grondig testen, monitoring en constante optimalisatie.
Ik hoop dat dit artikel je heeft geholpen de principes van market making beter te begrijpen en je heeft geïnspireerd om je eigen algoritmes te maken. Veel succes met traden!
Citatie
@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.}
}
Auteurs
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.