← Retour aux articles
May 17, 2025
5 min de lecture

Construire un algorithme de market making pour des paires crypto avec le modèle Avellaneda-Stoikov

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

Bonjour, amis ! Aujourd'hui, je vais vous montrer comment construire un algorithme de market making pour les paires crypto USD+/wETH et USD+/cbbtc. Nous utiliserons le modèle Avellaneda-Stoikov (A-S) et l'améliorerons avec un algorithme de Reinforcement Learning (PPO) pour l'optimisation dynamique du spread. Cela semble compliqué ? Ne vous inquiétez pas, je vais tout décomposer en étapes claires afin qu'un développeur débutant puisse suivre.

Market Making Visualization Visualiser l'essence du market making : équilibrer en permanence les ordres d'achat et de vente autour d'un prix juste afin de synthétiser une liquidité de marché optimale.

Qu'est-ce que le market making et pourquoi en avons-nous besoin ?

Le market making est une stratégie dans laquelle un trader place simultanément des ordres d'achat et de vente pour un actif, gagnant sur le spread (la différence entre les prix). Dans l'espace DeFi, les market makers jouent un rôle clé en fournissant de la liquidité et en réduisant le slippage pour les autres participants du marché.

Imaginez que vous êtes un vendeur sur un marché, toujours prêt à acheter un produit légèrement en dessous du prix du marché et à le vendre légèrement au-dessus. Votre profit est la différence entre votre prix d'achat et votre prix de vente. Mais il y a un piège : si le prix évolue soudainement dans une direction, vous risquez d'accumuler trop d'inventaire ou, à l'inverse, de vous retrouver sans rien à vendre.

Le modèle Avellaneda-Stoikov : les mathématiques au service du trading

Le modèle A-S est une approche mathématique permettant de déterminer les prix optimaux pour le market making. Son principal avantage est qu'il prend en compte non seulement le prix de marché actuel, mais aussi la taille de votre position (inventaire), la volatilité du marché et l'appétit pour le risque.

Les formules principales du modèle :

δ_a = S_t + (1/γ) * ln(1 + γ/k) + q_t * σ² * T
δ_b = S_t - (1/γ) * ln(1 + γ/k) - q_t * σ² * T

où :

  • δ_a et δ_b sont les prix ask et bid
  • S_t est le prix de marché actuel
  • γ est le paramètre de risque (plus il est élevé, plus le spread est large)
  • k est le taux d'arrivée des ordres
  • q_t est l'inventaire actuel
  • σ est la volatilité
  • T est l'horizon temporel

Particularités du trading onchain

Lorsque nous déplaçons l'algorithme onchain, des défis supplémentaires apparaissent :

  1. Latence – les transactions sur la blockchain ne sont pas instantanées, et le prix peut changer avant que l'ordre ne soit exécuté
  2. Coûts de gas – chaque transaction nécessite des frais de réseau
  3. Particularités AMM/PMM – la mécanique des pools de liquidité diffère de celle des exchanges traditionnelles

Voyons comment prendre en compte ces facteurs dans notre algorithme.

Étape 1 : Configurer l'environnement et collecter les données

Tout d'abord, nous devons configurer un environnement pour obtenir des données de marché. Nous utiliserons l'API Binance pour obtenir les prix actuels et la profondeur du carnet d'ordres.

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};
}

Nous aurons également besoin de métriques onchain telles que le coût du gas et la latence réseau :

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};
}

Étape 2 : Implémenter le modèle A-S de base

Implémentons maintenant le calcul du spread à l'aide du modèle 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};
}

Notez le inventory_term. Si vous avez un inventaire positif (beaucoup de l'actif), le prix ask diminue, et le prix bid diminue encore davantage pour encourager la vente et limiter l'achat. Et inversement pour un inventaire négatif.

Étape 3 : Adapter le modèle au trading onchain

Nous devons maintenant tenir compte des particularités de la blockchain. Commençons par la latence :

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;
}

Ici, nous utilisons un modèle de marche aléatoire (random walk) : plus la volatilité est élevée et plus la latence est longue, plus le prix peut varier avant que l'ordre ne soit exécuté.

Tenons maintenant compte du coût du 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
}

Enfin, adaptons les spreads aux particularités des 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};
}

Étape 4 : Gestion de l'inventaire

Pour suivre et gérer l'inventaire, créons une classe 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_;
};

Inventory management and risk visualization Visualisation du risque d'inventaire : surveillance de la taille des positions afin d'éviter une exposition excessive (longue ou courte) aux mouvements de marché unidirectionnels.

Étape 5 : Combiner le tout en un seul algorithme

Combinons maintenant tous les composants en un seul algorithme 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_;
};

L'état de notre environnement est un vecteur composé du prix actuel, de l'inventaire, de la volatilité, de la latence réseau et de la profondeur du pool. L'action est un vecteur de spreads et de tailles d'achat/vente.

Implémentons maintenant la fonction de récompense :

double reward = profit_term - inventory_risk - gas_cost;

Où :

  • profit_term est le profit issu des trades
  • inventory_risk est une pénalité pour un inventaire important (risque)
  • gas_cost est le gas dépensé

Enfin, entraînons l'agent 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);
    }
}

PPO Reinforcement Learning agent for spread optimization Le Reinforcement Learning en action : l'agent PPO traite des états de marché complexes pour optimiser dynamiquement les spreads d'achat/vente afin de maximiser la récompense attendue.

Étape 7 : Tests et visualisation

Pour tester notre algorithme, créons une simulation 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;
}

Et ensuite ?

Notre algorithme de market making est prêt, mais il existe de nombreuses façons de l'améliorer :

  1. Se connecter à de vraies API : remplacer les stubs par de vraies requêtes vers l'API Binance et un nœud Ethereum
  2. Améliorer le modèle de volatilité : utiliser GARCH ou d'autres modèles avancés
  3. Étendre le PPO : ajouter davantage de paramètres à l'état et à l'action
  4. Optimiser le gas : des stratégies pour minimiser les coûts de gas
  5. Stratégie multi-actifs : s'étendre à plusieurs paires à la fois

Conclusion

Nous avons construit un algorithme de market making qui prend en compte les particularités du trading onchain et qui utilise à la fois le modèle A-S classique et des méthodes de RL modernes. Cette approche permet de s'adapter aux conditions de marché changeantes et de maximiser le profit tout en contrôlant le risque.

Bien sûr, dans le trading réel, il existe de nombreux autres facteurs à prendre en compte, mais notre algorithme fournit une base solide pour un développement ultérieur. Rappelez-vous : dans le trading algorithmique, seules les mathématiques ne comptent pas, les tests approfondis, le monitoring et l'optimisation constante sont tout aussi importants.

J'espère que cet article vous a aidé à mieux comprendre les principes du market making et vous a inspiré à créer vos propres algorithmes. Bonne chance dans vos trades !

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.}
}
blog.disclaimer

Authors

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

Gardez une longueur d'avance sur le marché

Abonnez-vous à notre newsletter pour des insights exclusifs sur le trading IA, des analyses de marché et des mises à jour de la plateforme.

Nous respectons votre vie privée. Désabonnement possible à tout moment.