← Terug naar artikelen
May 17, 2025
5 min leestijd

Een market-making-algoritme bouwen voor crypto-paren met het Avellaneda-Stoikov-model

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

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.

Market Making Visualization 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:

  • δ_a en δ_b de ask- en bid-prijzen zijn
  • S_t de huidige marktprijs is
  • γ de risicoparameter is (hoe hoger, hoe breder de spread)
  • k de order-aankomstsnelheid is
  • q_t de huidige voorraad is
  • σ de volatiliteit is
  • T de tijdshorizon is

Kenmerken van onchain trading

Wanneer we het algoritme onchain verplaatsen, ontstaan er extra uitdagingen:

  1. Latentie – transacties op de blockchain zijn niet direct, en de prijs kan veranderen voordat de order wordt uitgevoerd
  2. Gaskosten – elke transactie vereist een netwerkvergoeding
  3. 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_;
};

Inventory management and risk visualization 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_term de winst uit trades is
  • inventory_risk een boete is voor een grote voorraad (risico)
  • gas_cost de 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);
    }
}

PPO Reinforcement Learning agent for spread optimization 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:

  1. Verbinden met echte API's: stubs vervangen door echte verzoeken aan de Binance API en een Ethereum-node
  2. Volatiliteitsmodel verbeteren: GARCH of andere geavanceerde modellen gebruiken
  3. PPO uitbreiden: meer parameters toevoegen aan staat en actie
  4. Gas optimaliseren: strategieën om gaskosten te minimaliseren
  5. 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.}
}
Disclaimer: De informatie in dit artikel is uitsluitend bedoeld voor educatieve en informatieve doeleinden en vormt geen financieel, beleggings- of handelsadvies. Het handelen in cryptovaluta brengt een aanzienlijk risico op verlies met zich mee.

Auteurs

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

Blijf de markt voor

Abonneer je op onze nieuwsbrief voor exclusieve AI-handelsinzichten, marktanalyses en platformupdates.

We respecteren je privacy. Je kunt je op elk moment afmelden.