Aller au contenu

01 · Architecture du bot

Un bot de trading est un système temps réel avec de l'argent en jeu. Les propriétés qui comptent ne sont pas celles d'une application web classique.

Les cinq propriétés non négociables

Propriété Ce qu'elle signifie
Déterminisme Les mêmes données produisent les mêmes décisions
Idempotence Rejouer un message ne crée pas un second ordre
Réconciliation Après reconnexion, l'état local correspond à celui du courtier
Observabilité Toute décision est reconstituable a posteriori
Arrêt sûr Il existe un moyen d'arrêter et de tout fermer en une action

L'ordre de priorité en cas de doute

Sécurité > correction > performance. Un bot lent qui refuse d'agir quand il doute perd de l'argent lentement. Un bot rapide qui agit sur un état incertain le perd d'un coup. En cas d'ambiguïté sur l'état, ne rien faire est la bonne réponse par défaut.

L'architecture recommandée

┌──────────────────────────────────────────────────────┐
│                    superviseur                        │
│         (santé, arrêt d'urgence, redémarrage)         │
└──────────────────────────────────────────────────────┘
        │                                    │
┌───────▼────────┐                  ┌────────▼─────────┐
│  adaptateur    │   événements     │   journal        │
│  de données    │─────────────────▶│   (append-only)  │
│  (Rithmic…)    │                  └──────────────────┘
└───────┬────────┘
        │ MarketEvent
┌───────▼────────────────────────────────┐
│           agrégation                    │
│  footprint · OFI · barres · contexte   │
└───────┬────────────────────────────────┘
        │ État de marché
┌───────▼────────────────────────────────┐
│         moteur de signaux               │
│  (pur, sans effet de bord, testable)   │
└───────┬────────────────────────────────┘
        │ Intent
┌───────▼────────────────────────────────┐
│      risque et conformité               │
│  plafonds · consistance · drawdown      │
└───────┬────────────────────────────────┘
        │ Order
┌───────▼────────────────────────────────┐
│      adaptateur d'exécution             │
│   (Rithmic / ProjectX / simulateur)     │
└─────────────────────────────────────────┘

Le principe central

Le moteur de signaux est une fonction pure

def signaux(etat_marche, contexte, parametres) -> list[Intent]:
    ...

Pas d'entrées/sorties, pas d'horloge interne, pas d'appel réseau, pas d'aléatoire. Conséquences directes :

  • il est testable avec de simples assertions ;
  • il est backtestable sans réécriture — le backtest et le live appellent exactement le même code ;
  • il est déterministe : le même flux d'événements produit la même séquence de décisions, ce qui rend les bugs reproductibles.

C'est le seul choix d'architecture de ce rapport qui mérite d'être imposé avant d'écrire la première ligne. Tout le reste peut être ajouté plus tard ; celui-ci ne peut pas l'être.

Les types de données

from dataclasses import dataclass
from enum import Enum

class Side(Enum):
    BUY = 1
    SELL = -1

@dataclass(frozen=True)
class Trade:
    ts_exchange: int      # nanosecondes, horloge bourse
    ts_local: int         # nanosecondes, horloge locale
    symbol: str
    price: float
    size: int
    aggressor: int        # +1, -1 ou 0

@dataclass(frozen=True)
class Quote:
    ts_exchange: int
    ts_local: int
    symbol: str
    bid: float
    bid_size: int
    ask: float
    ask_size: int

@dataclass(frozen=True)
class Intent:
    """Ce que le moteur de signaux veut faire. Ne connaît aucun courtier."""
    symbol: str
    side: Side
    size: int
    stop_price: float
    target_price: float
    reason: str           # pour le journal
    max_hold_seconds: int

Pourquoi frozen=True

Un événement de marché immuable ne peut pas être modifié par erreur en aval. Le coût est nul, le bénéfice est une classe entière de bugs éliminée. C'est de la paresse bien placée : une ligne évite un débogage de trois heures.

Les deux horodatages ne sont pas redondants

ts_exchange ordonne les événements ; ts_local détermine ce que le bot pouvait savoir. Un backtest qui n'utilise que ts_exchange fait voyager le bot dans le temps : il réagit à un événement avant de l'avoir reçu. Conservez les deux, dès la collecte — on ne peut pas les reconstruire après coup.

La boucle principale

async def boucle(adaptateur, agregat, moteur, risque, executeur, journal):
    async for event in adaptateur.stream():
        journal.write(event)              # 1. journaliser d'abord

        etat = agregat.update(event)      # 2. agréger
        if etat is None:
            continue

        intents = moteur.signaux(etat)    # 3. décider (pur)

        for intent in intents:            # 4. filtrer
            verdict = risque.evaluer(intent, etat)
            journal.write(verdict)
            if verdict.autorise:
                await executeur.envoyer(intent)   # 5. exécuter

L'ordre importe : journaliser avant de décider. Si le bot plante à l'étape 3, le journal contient l'événement qui l'a provoqué. C'est la différence entre un bug reproductible et un mystère.

Le journal

Un fichier append-only, une ligne JSON par événement, jamais réécrit.

import json, time

class Journal:
    def __init__(self, path):
        self.f = open(path, "a", buffering=1)   # ligne par ligne

    def write(self, obj):
        self.f.write(json.dumps({
            "ts": time.time_ns(),
            "type": type(obj).__name__,
            "data": obj.__dict__ if hasattr(obj, "__dict__") else obj,
        }) + "\n")

Le journal est le seul outil de débogage qui fonctionne en production

Un bot de trading ne se débogue pas au pas à pas : le marché n'attend pas. Le journal permet de rejouer une séance exactement, de comprendre pourquoi une décision a été prise, et de prouver ce qui s'est passé en cas de litige avec une prop firm.

Coût : trente lignes de code et quelques mégaoctets par jour. Aucun autre investissement du rapport n'a un rapport valeur/effort comparable.

Écrivez-le avant la première stratégie.

La gestion des connexions

Rithmic sépare ses services en plants indépendants (voir Panorama). Chacun peut tomber séparément.

État Comportement du bot
Ticker plant tombé Arrêt des nouvelles entrées, positions gérées par les ordres serveur
Order plant tombé Arrêt complet, alerte, aucune tentative de contournement
Les deux tombés Arrêt, alerte, intervention humaine
Reconnexion Réconciliation obligatoire avant toute nouvelle décision

async_rithmic fournit la reconnexion automatique avec backoff configurable, mais ses auteurs précisent explicitement que la bibliothèque ne traite pas la gestion des pannes ni la correction sous charge — c'est votre travail.

La réconciliation

async def reconcilier(executeur, etat_local):
    """À exécuter après CHAQUE reconnexion, avant toute nouvelle décision."""
    positions = await executeur.get_positions()
    ordres = await executeur.get_ordres_actifs()

    if positions != etat_local.positions:
        journal.write({"alerte": "divergence_position",
                       "local": etat_local.positions,
                       "courtier": positions})
        etat_local.positions = positions      # le courtier fait autorité
        etat_local.pause()                    # supervision humaine requise

    etat_local.ordres = ordres

Le courtier fait toujours autorité

En cas de divergence entre votre état local et celui du courtier, le courtier a raison. Toujours. Un bot qui « corrige » l'état du courtier pour le faire correspondre au sien envoie des ordres sur la base d'une fiction, et transforme un désaccord d'affichage en position réelle non voulue.

Les protections côté serveur

R|API+ propose des stops suiveurs, brackets et OCO qui restent actifs même si la connexion du client tombe (Ironbeam).

À utiliser systématiquement

Une coupure internet domestique de trois minutes avec une position ouverte et un stop géré côté client est un scénario de perte totale. Le stop côté serveur transforme ce scénario en perte bornée et connue.

Si votre voie d'accès ne permet pas de stop côté serveur, réduisez la taille de vos positions en conséquence. La contrainte est réelle et se paie quelque part.

L'arrêt d'urgence

Trois niveaux, du plus doux au plus radical :

Niveau Action Déclencheur
Pause Plus de nouvelles entrées, positions gérées Plafond atteint, doute
Flatten Fermeture de toutes les positions au marché Perte journalière, anomalie
Kill Flatten + arrêt du processus Bug détecté, panne réseau

Le déclenchement doit être possible de l'extérieur du bot : un fichier sentinelle suffit et ne dépend d'aucune infrastructure.

import os

def verifier_arret():
    if os.path.exists("/tmp/bot.kill"):
        return "kill"
    if os.path.exists("/tmp/bot.pause"):
        return "pause"
    return None

Un touch doit suffire

touch /tmp/bot.kill depuis un téléphone en SSH arrête le bot. Pas d'API d'administration, pas d'interface web, pas de service de messagerie qui peut tomber. Un fichier. C'est laid, et c'est ce qui fonctionne quand tout le reste est cassé.

Les langages

Langage Quand
Python Par défaut. Suffisant pour tout signal d'horizon > 1 s
C# Si vous développez dans NinjaTrader ou Quantower
C++ Si vous développez dans Sierra Chart (ACSIL), ou < 1 ms
Rust Si vous aimez Rust ; rithmic-rs existe

Le débat sur la performance de Python est un faux débat

Avec 50 ms de latence réseau, les 2 ms de traitement Python représentent 4 % du budget total. Réécrire en C++ pour gagner 1,9 ms est un mauvais investissement tant que la latence réseau domine.

Python devient un problème dans un seul cas : si votre boucle prend plus de temps que l'intervalle entre deux messages, vous accumulez du retard sans borne. C'est un problème d'architecture (traitement synchrone dans la boucle d'événements), pas de langage — et il se règle en déplaçant le travail lourd hors du chemin critique.

Résumé

  • Cinq propriétés : déterminisme, idempotence, réconciliation, observabilité, arrêt sûr.
  • Le moteur de signaux est une fonction pure : testable, backtestable, déterministe.
  • Journaliser avant de décider ; le journal est le seul débogueur qui marche en production.
  • Le courtier fait autorité sur l'état ; réconcilier après chaque reconnexion.
  • Utiliser les ordres de protection côté serveur.
  • Arrêt d'urgence par fichier sentinelle.
  • Python suffit tant que la latence réseau domine.

Chapitre suivant : Connexion Rithmic en Python