J’ai déjà cramé un mois de ma vie à construire mt5-httpapi, une VM Windows complète démarrée sous QEMU/KVM à l’intérieur de Docker, juste pour pouvoir parler à un broker qui ne livre qu’une wheel Python réservée à Windows. Ça marche. C’est aussi complètement taré: une install Windows 11 virtualisée en matériel, débloatée jusqu’à l’os, qui fait tourner MetaTrader 5 en mode portable, pour que je puisse taper un endpoint REST au lieu d’écrire du MQL5. Je le referais. Mais quand je me suis attaqué à Interactive Brokers, je suis resté dix bonnes minutes crispé, à attendre le même cauchemar: un binaire IBKR sauce Windows, encore une VM, encore un ISO de 4GB à télécharger, encore une tournée de « pourquoi le curseur reste coincé dans la fenêtre noVNC ».
Ça n’est pas arrivé. IB Gateway, le terminal de trading sans interface qu’IBKR livre pour l’accès API, tourne nativement sous Linux. Pas de .exe. Pas de wine. Pas de wheel qui ne se compile que sur win_amd64. Juste un processus JVM que tu peux poser directement dans un container. Du coup ibkr-httpapi est le projet frère que mt5-httpapi a toujours mérité: même forme, curl en entrée, JSON en sortie, un bearer token, un schéma d’URL par classe d’actifs, sauf que toute la stack est en containers Linux. Pas de KVM. Pas d’ISO Windows. Pas de noVNC vers un bureau complet juste pour cliquer dans un assistant d’installation Windows une fois par décennie.
Parler à un Broker, C’est Une Misère
L’histoire de l’API d’Interactive Brokers, c’est le genre de truc qui te fait comprendre pourquoi tous les bots de trading retail sur GitHub sont soit abandonnés, soit vieux de trois ans:
- L’API TWS n’existe que si quelque chose avec une interface graphique tourne, TWS lui-même, ou son frère sans interface IB Gateway. Il n’y a pas de « tu appelles juste un endpoint REST », il y a un protocole sur socket qui ne parle qu’à une session de bureau vivante.
- Pour connecter cette session, il faut cliquer dans un écran de login, accepter les conditions, parfois taper une notif 2FA sur ton téléphone, tous les jours, parce qu’IBKR déconnecte de force la gateway toutes les 24 heures, que ça te plaise ou non.
ib_async(le fork maintenu de feuib_insync) te donne un bon wrapper asyncio par-dessus ce socket, mais il te tend des objetsContractbruts, c’est encore toi qui construis à la mainStockcontreOptioncontreFuturecontreBagpour chaque classe d’actifs, avec des champs obligatoires différents à chaque fois.- IBKR coupe l’accès API pour violations de pacing répétées. Il n’y a aucun système d’avertissement gentil de leur côté, tu tapes trop fort sur l’endpoint de données historiques, tu manges une suspension. La plupart des scripts maison ont zéro conscience des rate limits jusqu’au jour où ils se font mordre.
- Chaque appel de données de marché que tu fais est soit jeté une fois lu, soit tu te réécris une couche de persistance CSV/SQLite depuis zéro, encore une fois, pour le quatrième projet de l’année.
- Tu veux du RSI ou du MACD par-dessus les bougies que tu viens de tirer? C’est encore une dépendance, encore une bibliothèque d’indicateurs, encore une série de cas limites autour des périodes de chauffe avec des NaN.
Rien de tout ça n’est vraiment la faute d’IBKR, c’est une API de courtage de niveau professionnel, pas un jouet, mais ça veut dire que chaque projet qui veut « donne-moi l’OHLC d’AAPL en HTTP » finit par réinventer mal les mêmes six trucs. J’en ai eu marre de les réinventer, alors j’ai construit la chose une bonne fois, correctement, avec une spec devant.
La Stack
La stack, c’est un service FastAPI (ibkrapi/) posé devant un container IB Gateway, qui lui parle sur le socket de l’API TWS via ib_async. Rien d’exotique, une seule instance IB partagée, gardée par un asyncio.Lock pour que les requêtes concurrentes ne se piétinent pas sur l’appel de connexion, avec backoff exponentiel (ça part du reconnect_backoff configuré, ça double jusqu’à reconnect_max_backoff) si le socket tombe. Chaque routeur tire la connexion par une seule fonction, get_ib(), le premier appelant attend le handshake, tous les suivants la réutilisent. Quand IBKR déconnecte de force la gateway pour son redémarrage quotidien, la requête suivante se reconnecte à la demande au lieu de faire tomber toute l’API.
Le container de gateway lui-même est construit à partir de gnzsnz/ib-gateway-docker, avec IBC (IB Controller) cuit dedans pour gérer le flux de login sans interface, identifiant et mot de passe passent par un fichier .env.ibkr gitignoré, et IBC pilote l’interface de login à la place d’IB Gateway pour que personne n’ait à cliquer. Une subtilité juridique à connaître: la licence d’installation d’IBKR interdit de redistribuer des images pré-construites contenant leur binaire, donc tu construis l’image de gateway en local plutôt que de tirer le tag Docker Hub d’un inconnu avec l’installeur IBKR cuit dedans. docker-compose.yml.example le dit explicitement et met par défaut un tag mutable :stable que tu es censé épingler sur un digest une fois la tienne construite.
Six Classes d’Actifs, Un Seul Schéma d’URL
Chaque type de marché a son propre préfixe, plutôt qu’un endpoint « symbol » surchargé qui devine en silence ce que tu voulais dire:
/stocks/<symbol> Equities (STK)
/options/<symbol> Options (OPT) — ?expiry=YYYYMMDD&strike=N&right=C|P
/options/<symbol>/chain Full option chain — all strikes × expirations
/futures/<symbol> Futures (FUT) — ?expiry=YYYYMM&exchange=CME
/futures/<symbol>/continuous Continuous future — no expiry needed
/cfd/<symbol> CFDs
/forex/<pair> Currencies (CASH) — IDEALPRO default
/crypto/<symbol> Crypto (CRYPTO) — PAXOS defaultActions, options, futures, CFD, forex, crypto, six classes d’actifs, une fabrique de Contract pour chacune dans ibkrapi/contracts.py, chacune prenant exactement les paramètres de désambiguïsation dont cette classe a besoin (les options veulent expiry/strike/right, les futures veulent expiry/exchange, les actions ne veulent quasiment rien) et remplissant le reste depuis config.yaml:contract_defaults.<class> pour que tu ne répètes pas « SMART/USD » à chaque appel. Là où ça a du sens pour la classe, tu as les quatre mêmes verbes: détails du contrat, un snapshot de tick live (avec les grecques greffées pour les options), des bougies historiques, et des ticks historiques bruts. Par-dessus les six classes de données de marché, il y a la saisie d’ordres cross-asset, une liste de positions, le résumé de compte, et un historique d’exécutions et d’ordres terminés, le tout servi sous le même préfixe /v1.
curl -H "Authorization: Bearer $API_TOKEN"
"http://localhost:8889/v1/stocks/AAPL/rates?duration=30+D&barSize=1d"
curl -H "Authorization: Bearer $API_TOKEN"
"http://localhost:8889/v1/options/AAPL/tick?expiry=20260619&strike=200&right=C"
curl -H "Authorization: Bearer $API_TOKEN"
"http://localhost:8889/v1/futures/ES/continuous?exchange=CME"La Spec d’Abord, Pas au Feeling
Aucun des routeurs ci-dessus n’est un décorateur FastAPI écrit à la main, éparpillé dans des fichiers en espérant qu’ils restent synchronisés. api/v1.yaml est la vraie source de vérité, un document OpenAPI 3.1.0 avec 42 opérations et 34 schémas, et make generate est la cible parapluie qui pilote trois générateurs distincts à partir de lui: fastapi-codegen (avec des templates Jinja maison) crache ibkrapi/api/_generated/{models.py, routers/*.py} sous forme de handlers-souches async qui délèguent directement dans ibkrapi/api/impl.py, écrit à la main; oapi-codegen émet un client Go entièrement typé à pkg/clients/go/client.gen.go; et openapi-python-client génère un paquet client Python autonome et installable. Neuf routeurs sont montés sur l’app dans server.py, system, stocks, options, futures, cfd, forex, crypto, orders, history, tous sous un préfixe fixe /v1. Personne n’édite à la main quoi que ce soit sous _generated/ ou pkg/clients/*; tu changes la spec, tu relances le générateur, et le contrôle de dérive en CI fait échouer le build si tu oublies.
MCP, Pour Ne Pas Tendre un Manuel de curl à un Agent
La même app FastAPI monte aussi un serveur MCP à /mcp, en streamable-HTTP, à la racine de l’app, pas sous le préfixe /v1. Ça a commencé avec trois outils génériques (ping, endpoints, request) qui laissaient un agent appeler n’importe quelle route REST par méthode et chemin. Techniquement ça marche, mais ça veut dire que le modèle doit tirer un catalogue OpenAPI à l’exécution puis deviner son chemin dans un template d’URL, c’est-à-dire exactement la taxe « lis la doc d’abord » que la spec était censée tuer. Donc ça a sauté: /mcp expose maintenant 24 outils typés dédiés, chacun avec ses paramètres typés et une description que l’agent lit. Le schéma est la documentation.
La partie intéressante, c’est ce qui est arrivé aux six classes d’actifs. En REST ce sont six préfixes d’URL. En MCP elles se replient derrière un seul enum asset_class, stock, option, future, cfd, forex, crypto, si bien que get_contract, get_quote, get_rates et get_rates_ta remplacent ce qui serait sinon 24 outils par classe quasi identiques, à gonfler le contexte de l’agent pour zéro sens ajouté. Autour gravitent les spéciaux qui n’ont de sens que pour une seule classe (get_option_chain, place_option_combo, exercise_option, get_future_continuous, list_future_contracts, get_stock_ticks), la famille des ordres (list_orders, get_order, place_order, cancel_order, cancel_all_orders), le compte et les positions (get_account, get_account_values, list_accounts, list_positions), l’historique (get_executions, get_completed_orders), plus ping et la paire d’origine endpoints/request gardée en filet pour tout ce qui n’a pas d’outil dédié.
Rien de tout ça n’est une implémentation parallèle posée à côté de l’API REST en attendant de se désynchroniser. Chaque outil est dispatché en interne par les mêmes routeurs, la même validation, la même auth bearer et le même rate limiter qu’un appel HTTP, donc un agent qui martèle get_rates brûle le même budget de pacing historique de 10 minutes que curl, et mange la même enveloppe 429 RATE_LIMIT_NEAR quand il franchit la ligne. Laisse api_token vide et les deux surfaces sont non authentifiées exactement de la même façon.
Les outils qui modifient quelque chose portent leur avertissement dans le schéma lui-même, le seul endroit où le modèle est garanti de regarder. La description de place_order se termine par « Ceci place un ordre réel et irréversible sur un compte de courtage en direct, ne l’appelle que si l’utilisateur l’a explicitement demandé, et confirme d’abord les paramètres. » Ce n’est pas une note de bas de page dans un README que l’agent n’ouvrira jamais; c’est dans la définition de l’outil, tendue à chaque appel.
Il y a aussi un bout de réalité des clients MCP cuit dedans. Le Mount("/mcp", ...) de Starlette sert à /mcp/* et fait un 307 sur la forme nue /mcp, et une bonne partie des clients MCP ne suivent tout simplement pas cette redirection sur un POST. Plutôt que de laisser la moitié de l’écosystème se casser sur un détail technique de la spec, un middleware réécrit /mcp nu en /mcp/ avant le routage et passe à autre chose.
Pour le brancher à un client il y a un plugin @psyb0t/ibkr-httpapi, un pont fin de stdio vers HTTP pointé sur ton instance via IBKR_HTTPAPI_URL (la racine du serveur; un /v1 traînant est retiré, parce que tout le monde se plante là-dessus exactement une fois) plus IBKR_HTTPAPI_TOKEN quand l’auth est active. Dans Claude Code, ça fait deux lignes:
claude plugin marketplace add psyb0t/agents
claude plugin install ibkr-httpapi@psyb0tCe qui remet bien plus nettement en lumière l’avertissement plus bas sur cette page: un agent avec place_order et cancel_all_orders dans sa liste d’outils est à une mauvaise inférence de faire quelque chose de cher. Le schéma lui dit de confirmer d’abord. Assure-toi que quelque chose d’autre le lui dise aussi.
Auth, Parce Que Ce N’est Pas un Jouet
Bearer token, vérifié avec hmac.compare_digest plutôt qu’un == tout simple, parce qu’une comparaison de chaînes naïve fuit de l’information temporelle, assez de requêtes et tu bissectes un token caractère par caractère. Mets api_token dans config.yaml (ou API_TOKEN en env) et chaque requête exige Authorization: Bearer <token>, sinon elle prend un 401 avec l’enveloppe d’erreur standard: {code, message, details}. Laisse le token vide et l’API est grande ouverte à tout ce qui peut atteindre le socket, ce qui va bien sur un déploiement loopback derrière nginx, et est une très mauvaise idée partout ailleurs, et la doc du projet est franche là-dessus.
Pacing + La Mine d’Or
Chaque appel en direction d’IBKR passe par un rate limiter préventif avant même de toucher le socket, parce qu’IBKR coupe l’accès API pour violations de pacing répétées et « désolé, mon bot ne savait pas » n’est pas une défense qui tient. Trois paliers, chacun avec son propre compteur à fenêtre glissante, plus un asyncio.Lock par contrat, plus un sémaphore global de concurrence: les appels de données historical sont plafonnés à soft-50/hard-55 par fenêtre de 10 minutes (la limite dure d’IBKR est à 60), les appels market_data restent sous le plafond de ~50 msg/sec du socket TWS, et les orders sont bridés le plus durement de tous, 5/sec, 3 concurrents, parce qu’un déluge d’appels d’ordres n’est presque jamais intentionnel. Franchis le plafond souple et tu as un avertissement dans les logs; franchis le dur et l’appelant mange une enveloppe 429 RATE_LIMIT_NEAR avec la règle exacte, l’usage, la limite et le retry-after cuits dans details.
Derrière cette même porte, tout ce qui est cachable est écrit sur disque sous data/history/ à chaque appel, bougies et ticks partent dans des CSV par (classe d’actifs, symbole, timeframe) taillés pour l’ingestion wickworks, les détails de contrat et les métadonnées de chaînes d’options reçoivent un cache JSON à longue TTL, et chaque snapshot de tick ou de chaîne se greffe sur un registre d’historique. Rien n’est jamais supprimé; c’est pensé explicitement comme une « mine d’or » en ajout seul, tu montes ./data, tu sauvegardes, et chaque appel que tu fais s’accumule en silence en jeu de données long terme au lieu d’être jeté après une seule lecture. Tu veux une lecture garantie fraîche plutôt que le cache? Chaque endpoint cachable prend ?refresh=true, qui court-circuite la lecture du cache mais réécrit quand même le résultat frais pour que l’appelant suivant en profite.
De l’Analyse Technique Sans Écrire une Bibliothèque d’Indicateurs
L’endpoint /rates de chaque classe d’actifs a un frère, POST /<class>/<symbol>/rates/ta, qui récupère les mêmes bougies et les passe à wickworks, le même sidecar TA que mt5-httpapi utilise déjà. RSI, MACD, bandes de Bollinger, ADX, ATR, VWAP, Ichimoku, order blocks, fair value gaps, ruptures de structure BOS/CHoCH, structure de swing, niveaux de support et résistance, zones de liquidité, ancres de session, calculés côté serveur, en un appel, sur des bougies que tu as déjà. Depuis la dernière mise à jour, ça s’est affiné: le chemin TA se compose maintenant avec le même cache de bougies que /rates au lieu de faire sa propre récupération séparée, donc une requête TA répétée sur des bougies en cache coûte zéro budget de pacing IBKR et revient quand même avec des calculs d’indicateurs frais. wickworks reste strictement primitif par conception, séries brutes et faits structurels, jamais « achète » ou « vends », donc si tu veux des avis tu les construis dans ton propre consommateur, pas dans le sidecar.
Tu le pointes sur ta propre instance via wickworks.url dans la config; laisse-le vide et /rates/ta renvoie juste un 503 propre au lieu de faire semblant de marcher. L’appel sortant lui-même est restreint par schéma, uniquement http:// et https://, précisément pour qu’une URL mal configurée ne puisse pas être détournée en SSRF vers quelque chose comme file://.
Pourquoi Diable une Gateway Sans Interface a Besoin d’une Surface VNC
Question légitime, parce qu’IB Gateway n’est pas vraiment sans interface au sens classique, c’est une application Java Swing avec GUI qui tourne sous un framebuffer virtuel (Xvfb) à l’intérieur du container. IBC pilote cette interface par programme pour se connecter et cliquer au-delà des dialogues de redémarrage quotidien, et il gère l’écrasante majorité des cas sans aucun humain dans les parages. Mais IBKR met parfois des bâtons dans les roues: une notif 2FA hebdomadaire qui expire, un dialogue inattendu que l’automatisation d’IBC ne reconnaît pas, une confirmation « nouvel appareil » la première fois que tu montes une gateway neuve. Quand ça arrive, tu as besoin de *voir* réellement le bureau posé derrière Xvfb, et c’est à ça que sert Dockerfile.novnc, un petit proxy websockify qui expose le port VNC de la gateway (:5900) en HTTP/WebSocket pour que tu puisses regarder (et cliquer sur) le bureau d’IB Gateway depuis un simple onglet de navigateur, sans client VNC natif. Ce n’est pas une machine virtuelle complète comme le setup dockurr/windows dont mt5-httpapi a besoin, il n’y a pas d’OS à démarrer, c’est une image python:3.12-slim de 57 lignes épinglée sur digest qui fait tourner websockify, et dont l’entrypoint se contente d’injecter ton mot de passe VNC dans index.html pour qu’elle se connecte directement à la session. Tu pointes un navigateur dessus quand quelque chose est coincé, tu règles le dialogue, tu fermes l’onglet, et tu oublies que ça existe jusqu’au prochain hoquet 2FA hebdomadaire.
Durcissement des Containers, Parce Que Ça Touche à de l’Argent
Les containers API et wickworks tournent avec cap_drop: [ALL], des systèmes de fichiers racine read_only: true avec des montages tmpfs noexec,nosuid pour les bouts qui ont besoin d’écrire, no-new-privileges:true, et des limites de mémoire, CPU et pid par service. Le réseau est découpé en trois réseaux Docker isolés: front (nginx qui parle à l’API), backend (l’API qui parle à la gateway, qui a besoin de sortir vers le cloud IBKR), et un réseau internal: true pour le trafic API vers wickworks qui n’a aucune voie de sortie, wickworks ne peut physiquement pas téléphoner dehors même si tu le voulais. Le container de gateway est la seule exception qui ne peut pas tourner complètement verrouillée (Xvfb plus une JVM plus IBC qui écrit partout dans le système de fichiers ne tolère pas une racine en lecture seule), alors il reçoit no-new-privileges comme plancher. Toutes les images de base publiques sont épinglées par digest SHA, les dépendances Python sont verrouillées par hash via uv pip compile --generate-hashes et installées avec --require-hashes, il y a une barrière d’âge glissante de 7 jours sur les nouvelles versions de dépendances pour qu’un paquet tout juste publié et empoisonné en chaîne d’approvisionnement ne puisse pas atterrir le jour même, et make audit / make audit-go / make audit-compose lancent respectivement pip-audit, govulncheck, et un scanner de compose basé sur grep (réglages interdits comme privileged, pid:host, montages de socket Docker, tags non épinglés, ports exposés publiquement).
Le Moment Où Ce Truc Pilote un Vrai Putain de Compte de Courtage
Je ne vais pas enterrer ça dans une note de bas de page. Ce n’est pas un jouet de données de marché, POST /orders place un ordre réel sur un vrai compte IBKR, et il bouge de l’argent réel dès qu’il est accepté. Il n’y a pas d’endpoint de modification d’ordre, exprès: pour changer un ordre en attente tu l’annules (DELETE /orders/{orderId}) et tu en places un neuf, délibérément, plutôt que de muter un ordre vivant sur place. DELETE /orders sans ID annule *tous* les ordres ouverts du compte d’un coup. POST /options/exercise exerce ou abandonne de vrais contrats. Aucun de ces gestes n’a de bouton d’annulation. Si tu pointes un agent ou un script là-dessus, fais-lui confirmer à un humain le symbole résolu, le sens, la quantité et le prix avant de déclencher quoi que ce soit qui modifie, et ne le laisse jamais réessayer automatiquement un ordre rejeté, un rejet est un panneau stop, pas un bug à contourner.
Deux choses adoucissent le rayon de souffle si tu les veux: TRADING_MODE=paper dans .env.ibkr connecte à la gateway de paper trading d’IBKR (port 4002) plutôt qu’au live (4001), les numéros de compte paper commencent par DU, les live par U, et /accounts te dira auquel tu parles vraiment. Et IBC supporte READ_ONLY_API=yes, qui bloque entièrement l’API de trading au niveau de la gateway si tout ce que tu veux de ce truc ce sont des données de marché et de la visibilité sur le compte. Laisse api_token vide et toute cette surface, données de marché, positions et placement d’ordres compris, est non authentifiée pour tout ce qui atteint le port. Mets le token. Écoute sur loopback. Ne sois pas la raison pour laquelle le script de quelqu’un d’autre place un ordre sur ton compte.
En Résumé
Si mt5-httpapi c’était « faire parler HTTP à MetaTrader 5 alors que Windows est en travers du chemin », ibkr-httpapi est la version où Windows n’a jamais été en travers du chemin, IB Gateway tourne simplement sous Linux comme un logiciel serveur normal, donc toute la stack est en containers, une API spec-first avec des clients générés dans deux langages, 24 outils MCP typés qui passent par ces mêmes handlers pour quand c’est un agent qui conduit, un pacing qui empêche IBKR de te bannir, un cache disque qui transforme chaque appel en données permanentes au lieu de JSON jeté, et le même sidecar TA wickworks qui fait le calcul d’indicateurs côté serveur. Six classes d’actifs, un bearer token, zéro VM Windows. Récupère-le sur github.com/psyb0t/ibkr-httpapi, lis les notes de licence avant de construire l’image de gateway, et mets un token d’API avant d’exposer ça à autre chose que localhost. C’est sous licence WTFPL, fais-en ce que tu veux, mais ne viens pas te plaindre quand ton bot achète 500 puts sur le mauvais ticker.