Ein Market-Making-Algorithmus für Krypto-Paare mit dem Avellaneda-Stoikov-Modell
Hallo, Freunde! Heute zeige ich euch, wie man einen Market-Making-Algorithmus für die Krypto-Paare USD+/wETH und USD+/cbbtc baut. Wir verwenden das Avellaneda-Stoikov-Modell (A-S) und erweitern es um einen Reinforcement-Learning-Algorithmus (PPO) zur dynamischen Spread-Optimierung. Klingt kompliziert? Keine Sorge, ich zerlege alles in klare Schritte, sodass auch ein Anfänger-Entwickler mitkommt.
Die Essenz des Market Making veranschaulicht: kontinuierliches Ausbalancieren von Kauf- und Verkaufsaufträgen um einen fairen Preis, um optimale Marktliquidität zu erzeugen.
Was ist Market Making und wozu brauchen wir es?
Market Making ist eine Strategie, bei der ein Trader gleichzeitig Kauf- und Verkaufsaufträge für einen Vermögenswert platziert und am Spread (der Preisdifferenz) verdient. Im DeFi-Bereich spielen Market Maker eine Schlüsselrolle, indem sie Liquidität bereitstellen und Slippage für andere Marktteilnehmer reduzieren.
Stellt euch vor, ihr seid Händler auf einem Markt, immer bereit, ein Produkt etwas unter dem Marktpreis zu kaufen und etwas darüber zu verkaufen. Euer Gewinn ist die Differenz zwischen Kauf- und Verkaufspreis. Aber es gibt einen Haken: Wenn sich der Preis plötzlich in eine Richtung bewegt, könnt ihr zu viel Bestand ansammeln oder umgekehrt nichts mehr zu verkaufen haben.
Das Avellaneda-Stoikov-Modell: Mathematik im Dienst des Handels
Das A-S-Modell ist ein mathematischer Ansatz zur Bestimmung optimaler Preise für Market Making. Sein größter Vorteil ist, dass es nicht nur den aktuellen Marktpreis berücksichtigt, sondern auch eure Positionsgröße (Bestand), die Marktvolatilität und die Risikobereitschaft.
Die Hauptformeln des Modells:
δ_a = S_t + (1/γ) * ln(1 + γ/k) + q_t * σ² * T
δ_b = S_t - (1/γ) * ln(1 + γ/k) - q_t * σ² * T
wobei:
δ_aundδ_bdie Ask- und Bid-Preise sindS_tder aktuelle Marktpreis istγder Risikoparameter ist (je höher, desto breiter der Spread)kdie Order-Ankunftsrate istq_tder aktuelle Bestand istσdie Volatilität istTder Zeithorizont ist
Besonderheiten des Onchain-Handels
Wenn wir den Algorithmus onchain verlagern, entstehen zusätzliche Herausforderungen:
- Latenz – Transaktionen auf der Blockchain sind nicht sofort, und der Preis kann sich ändern, bevor der Auftrag ausgeführt wird
- Gaskosten – jede Transaktion erfordert eine Netzwerkgebühr
- AMM/PMM-Besonderheiten – die Mechanik von Liquiditätspools unterscheidet sich von traditionellen Börsen
Schauen wir uns an, wie wir diese Faktoren in unserem Algorithmus berücksichtigen.
Schritt 1: Umgebung einrichten und Daten sammeln
Zunächst müssen wir eine Umgebung einrichten, um Marktdaten zu erhalten. Wir verwenden die Binance-API, um aktuelle Preise und die Tiefe des Orderbuchs abzurufen.
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};
}
Wir benötigen auch Onchain-Metriken wie Gaskosten und Netzwerklatenz:
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};
}
Schritt 2: Das grundlegende A-S-Modell implementieren
Jetzt implementieren wir die Spread-Berechnung mit dem A-S-Modell:
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};
}
Beachtet den inventory_term. Bei positivem Bestand (viel des Assets) sinkt der Ask-Preis, und der Bid-Preis sinkt noch stärker, um Verkäufe zu fördern und Käufe zu begrenzen. Und umgekehrt bei negativem Bestand.
Schritt 3: Das Modell für den Onchain-Handel anpassen
Nun müssen wir die Besonderheiten der Blockchain berücksichtigen. Beginnen wir mit der Latenz:
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 verwenden wir ein Random-Walk-Modell: Je höher die Volatilität und je länger die Latenz, desto mehr kann sich der Preis ändern, bevor der Auftrag ausgeführt wird.
Nun berücksichtigen wir die 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
}
Zuletzt passen wir die Spreads an die Besonderheiten des PMM-Pools an:
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};
}
Schritt 4: Bestandsmanagement
Um den Bestand zu verfolgen und zu verwalten, erstellen wir eine einfache 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_;
};
Visualisierung des Bestandsrisikos: Überwachung der Positionsgrößen, um ein übermäßiges Engagement (long oder short) bei einseitigen Marktbewegungen zu vermeiden.
Schritt 5: Alles zu einem einzigen Algorithmus kombinieren
Jetzt kombinieren wir alle Komponenten zu einem einzigen Market-Making-Algorithmus:
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_;
};
Unser Umgebungszustand ist ein Vektor aus aktuellem Preis, Bestand, Volatilität, Netzwerklatenz und Pool-Tiefe. Die Aktion ist ein Vektor aus Spreads und Kauf-/Verkaufsgrößen.
Jetzt implementieren wir die Reward-Funktion:
double reward = profit_term - inventory_risk - gas_cost;
Dabei ist:
profit_termder Gewinn aus Tradesinventory_riskeine Strafe für großen Bestand (Risiko)gas_costdie aufgewendeten Gaskosten
Zum Schluss trainieren wir den PPO-Agenten:
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 Aktion: Der PPO-Agent verarbeitet komplexe Marktzustände, um Kauf-/Verkaufsspreads dynamisch für maximale erwartete Belohnung zu optimieren.
Schritt 7: Testen und Visualisieren
Um unseren Algorithmus zu testen, erstellen wir eine einfache 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;
}
Wie geht es weiter?
Unser Market-Making-Algorithmus ist fertig, aber es gibt viele Möglichkeiten, ihn zu verbessern:
- Mit echten APIs verbinden: Stubs durch echte Anfragen an die Binance-API und einen Ethereum-Node ersetzen
- Volatilitätsmodell verbessern: GARCH oder andere fortgeschrittene Modelle verwenden
- PPO erweitern: mehr Parameter zu Zustand und Aktion hinzufügen
- Gas optimieren: Strategien zur Minimierung der Gaskosten
- Multi-Asset-Strategie: Erweiterung auf mehrere Paare gleichzeitig
Fazit
Wir haben einen Market-Making-Algorithmus gebaut, der die Besonderheiten des Onchain-Handels berücksichtigt und sowohl das klassische A-S-Modell als auch moderne RL-Methoden nutzt. Dieser Ansatz ermöglicht es, sich an veränderte Marktbedingungen anzupassen und den Gewinn zu maximieren, während das Risiko kontrolliert wird.
Natürlich gibt es im realen Handel viele zusätzliche Faktoren zu berücksichtigen, aber unser Algorithmus bietet eine solide Grundlage für die weitere Entwicklung. Denkt daran: Im algorithmischen Handel ist nicht nur die Mathematik wichtig, sondern auch gründliches Testen, Monitoring und ständige Optimierung.
Ich hoffe, dieser Artikel hat euch geholfen, die Prinzipien des Market Making besser zu verstehen, und euch inspiriert, eure eigenen Algorithmen zu entwickeln. Viel Erfolg beim Trading!
Zitierung
@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.