← Tous les projets

Market-making crypto · Architecture logicielle

Architecture d'un moteur de market-making

Fortuna est un framework de composants qui permet de décrire un bot de trading de manière déclarative. Un nouveau mandat client devient alors un fichier de configuration, plutôt qu'une nouvelle base de code. Un seul moteur pour plus de 100 places de marché et plus de 30 clients, avec une exploitation continue 24/7.

Période
2024-aujourd'hui
Rôle
Co-fondateur & CTO
Stack
Python 3.13, asyncio, CCXT, Web3 / DEX, structlog

Contexte

En fondant une société de market making, nous avons dû construire toute la plateforme à partir de zéro : moteur de trading, algorithmes, infrastructure d'exécution et supervision. En tant que co-fondateur et CTO, j'ai conçu et écrit l'ensemble du système, qui tourne aujourd'hui sur plus de trente clients en production.

La plateforme couvre plusieurs briques techniques et opérationnelles. Le schéma ci-dessous présente l'ensemble du système et les équipes qui l'exploitent ; cette étude de cas se concentre sur une seule partie, le moteur de trading central (la brique Fortuna surlignée en bleu ci-dessous). L'observabilité et le copilote IA ont leurs propres études de cas.

trade métriques requête contrôle opère CEX Binance · OKX · … DEX Uniswap · … Services Fortuna · VMs Service Fortuna Service Fortuna Service Fortuna ★ central, détaillé sur cette page Base time-series Grafana dashboards · alertes Agent IA Warren Plateforme admin configurer · déployer Équipe trading opère le desk
La plateforme complète et ses acteurs. Cette page se concentre sur la stack centrale surlignée : le moteur Fortuna.

Le problème

Le market making impose des contraintes fortes : chaque client arrive avec un mandat précis, exigeant et souvent spécifique.

  • La liquidité doit être fournie 24/7 sur de nombreuses plateformes, avec du capital constamment exposé au marché.
  • Chaque mandat varie selon le spread cible, la profondeur demandée, les actifs, les plateformes et les contraintes spécifiques du client.
  • Les exigences sont pointues et changent souvent ; le système doit s'adapter rapidement.
  • Dupliquer la base de code pour chaque client ou chaque stratégie aurait rendu le système impossible à exploiter, à maintenir et à fiabiliser.

Objectifs

  • Fournir une liquidité continue, 24/7, à spread maîtrisé sur de nombreuses places.
  • Mettre en production un nouveau mandat ou une nouvelle stratégie rapidement, sans écrire de code.
  • Atteindre un large éventail d'exchanges, centralisés et décentralisés, derrière une seule interface.
  • Garantir une précision adaptée aux contraintes financières, avec une exploitation sûre, observable et contrôlable.

Mon approche

J'ai construit Fortuna comme un framework central dans lequel chaque stratégie est assemblée à partir de composants simples, testés indépendamment. Au lieu de développer un bot spécifique pour chaque client, un opérateur compose une stratégie à partir d'un catalogue de plus de 110 briques, directement dans un fichier de configuration. L'architecture est en couches : un runtime central léger, un catalogue de modules enfichables, et une couche unifiée d'accès aux plateformes de trading.

Déclare
Configuration de service (YAML)
un fichier par stratégievariablescomposants & câblagevaleurs décimales exactes
▼  le conteneur d'injection résout & câble les composants
Runtime central
conteneur DI · cycle de vie des composants · services async
injection de dépendancesdécouverte de pluginsgestion du cycle de vieservices async planifiésarrêt gracieux
▼  compose
Modules composables (composants enfichables)
Un catalogue de 110+ briques de trading
market-makingarbitrageexécution : twap / pov / peggeranti-botprixlistenerssinksstatskill-switches
▼  trade via
Abstraction des exchanges
Une interface, places centralisées et décentralisées
CEX via CCXT (100+ places)adaptateurs réglés : Binance · Bybit · OKX · KuCoin · Gate.io · HTX · MEXC …DEX : Uniswap v2 …
Architecture en couches : la configuration en haut, un cœur DI léger, un catalogue de modules enfichables, et une couche d'exchanges unique en dessous.

La solution technique

Chaque brique, de l'adaptateur d'exchange à la stratégie complète, est un composant avec un cycle de vie asynchrone : chargement, exécution, arrêt. Au cœur du framework, un conteneur léger d'injection de dépendances résout chaque composant à partir de son nom. Les implémentations sont découvertes via le système standard d'entry points de Python, ce qui permet d'ajouter de nouveaux modules sans modifier le cœur du moteur. Le conteneur instancie ensuite les composants et les câble entre eux.

Toute cette composition est décrite de manière déclarative. Un service complet est décrit dans un seul fichier YAML : des variables partagées, puis la liste des composants, chacun avec son type et ses arguments. Tout argument qui pointe vers un autre composant (ou vers une variable) est résolu récursivement, et c'est exactement ainsi que les dépendances sont injectées. Un type décimal dédié conserve l'exactitude des valeurs monétaires dès l'analyse du fichier, sans passer par des nombres flottants.

# un bot d'arbitrage inter-exchange, décrit comme de la donnée
service: "@arbitrage"

vars:
  profit_threshold: !decimal "0.0015"
  trade_amount: !decimal "0.05"

components:
  binance:
    id: exchanges.ccxt.binance
  mexc:
    id: exchanges.ccxt.mexc

  exchange_a_adapter:
    id: arbitrage.adapters.cex
    args:
      exchange: "@binance"
      symbol: "BTC/USDT"

  exchange_b_adapter:
    id: arbitrage.adapters.cex
    args:
      exchange: "@mexc"
      symbol: "BTC/USDC"

  converter:
    id: arbitrage.converters.simple
    args:
      exchange: "@binance"

  arbitrage:
    id: arbitrage
    args:
      exchange_a: "@exchange_a_adapter"
      exchange_b: "@exchange_b_adapter"
      converter: "@converter"
      threshold: "@profit_threshold"
      trade_amount: "@trade_amount"

Le conteneur transforme ces références en un graphe de composants vivants câblés :

binance exchanges.ccxt.binance mexc exchanges.ccxt.mexc exchange_a_adapter arbitrage.adapters.cex converter arbitrage.converters.simple exchange_b_adapter arbitrage.adapters.cex arbitrage arbitrage · service
La même config, résolue : les composants se référencent par nom, et le conteneur DI construit le graphe d'objets vivants.

Le moteur est conçu pour faire tourner les stratégies 24/7, avec arrêt contrôlé, reprise après erreur et supervision des composants critiques. Au démarrage d'un service, chaque composant est chargé dans l'ordre ; à l'arrêt, ils sont déchargés en ordre inverse, pour fermer proprement sessions et connexions. Les stratégies sont exécutées à intervalle fixe par un service planifié, conçu pour pouvoir être interrompu proprement. Un signal d'arrêt (Ctrl+C ou SIGTERM) déclenche une extinction contrôlée plutôt qu'une coupure brutale.

La principale valeur opérationnelle vient ensuite du catalogue de composants. Le même modèle de composant sert à construire un large éventail de briques de trading : un opérateur compose un mandat en piochant dedans plutôt qu'en écrivant du code.

  • Market making : placement d'ordres bid/ask à intervalle régulier, avec stratégies interchangeables, exécuteurs modulaires et mode dry-run pour tester un comportement sans engager de capital.
  • Algorithmes d'exécution : TWAP, POV (participation au volume), et un module de pegging qui maintient un ordre limite aligné sur le meilleur bid ou ask, avec des variantes on-chain pour les DEX.
  • Arbitrage : compare deux places via un adaptateur normalisé unique et capture l'écart dès qu'il dépasse un seuil configurable.
  • Fournisseurs de prix : sources centralisées, pools on-chain, agrégateurs et fournisseurs composites combinant plusieurs sources selon différentes méthodes : fallback, moyenne pondérée, VWAP, taux croisé, min/max ou médiane glissante.
  • Listeners de données de marché : récupération périodique des soldes, carnets d'ordres, trades, tickers, bougies OHLCV, et ordres ouverts ou clôturés.
  • Kill switches : composants de sécurité à part entière, des seuils d'inventaire à un agrégateur qui stoppe le trading dès qu'une condition se déclenche.
  • Métriques et sorties de données : collecteurs qui persistent soldes, spreads, profondeur, et exécutions en PostgreSQL, alimentant la stack d'observabilité traitée dans son étude de cas dédiée.

Dans un système qui exécute des transactions financières, la précision et la fiabilité doivent être garanties dès la conception : les valeurs sont des décimaux exacts dès le premier parsing (jamais des flottants), le logging structuré les préserve, et l'I/O réseau utilise des sessions HTTP async poolées liées au cycle de vie de chaque composant. Chaque stratégie embarque ses propres garde-fous : limites d'inventaire, seuils de prix, contraintes d'exposition et règles d'arrêt.

C'est la couche d'exchanges qui rend les stratégies indépendantes de la plateforme sous-jacente. Les plateformes centralisées s'appuient sur CCXT, en arithmétique décimale exacte plutôt qu'en flottants ; une vingtaine d'entre elles ont un adaptateur sur mesure là où leur API s'écarte du standard, par exemple pour reconstituer la liste complète des ordres clôturés ou contourner une particularité propre à une plateforme. Les plateformes décentralisées sont traitées au même niveau : Uniswap v2 et v3 et PancakeSwap sur les chaînes EVM, QuickSwap sur Polygon, Jupiter sur Solana et xExchange sur MultiversX, chacune interrogeant ses prix ou l'état de ses pools directement on-chain. Pour l'arbitrage, une plateforme centralisée et une décentralisée sont réunies derrière un même adaptateur normalisé, qui expose les mêmes méthodes de cotation et d'exécution : la stratégie reste indépendante du type de marché sous-jacent. Les appels indépendants, qu'il s'agisse de cotations sur plusieurs plateformes ou d'écritures vers plusieurs sinks, s'exécutent en parallèle.

Résultats

  • Un seul moteur exécute plus de 30 mandats clients en production, 24/7.
  • Plus de 100 plateformes centralisées et décentralisées accessibles via une interface unique.
  • Une nouvelle stratégie client peut être mise en production par configuration, sans développement spécifique.
  • Justesse de niveau financier (décimaux partout) et exploitation sûre (cycle de vie, kill switches) par construction.

Ce que j'en retiens

En modélisant les stratégies comme des compositions déclaratives de composants testés, les demandes spécifiques des clients sont devenues configurables plutôt que développées au cas par cas. C'est cette seule décision qui a permis à une base de code unique de passer à de nombreux clients et places sans se fragmenter, tout en gardant le système juste et opérable au fil de sa croissance.

Études de cas liées

Un défi similaire ? Me contacter →