Ik heb al een maand van mijn leven verbrand aan het bouwen van mt5-httpapi, een complete Windows-VM die opstart onder QEMU/KVM binnen in Docker, alleen maar om te kunnen praten met een broker die uitsluitend een Windows-Python-wheel uitbrengt. Het werkt. Het is ook compleet geschift: een hardwarevirtualisatie van Windows 11, tot op het bot gestript, die MetaTrader 5 in portable modus draait, zodat ik een REST-endpoint kan aanroepen in plaats van MQL5 te schrijven. Ik zou het zo weer doen. Maar toen ik Interactive Brokers ging aansluiten, zat ik tien lange minuten gespannen te wachten op dezelfde nachtmerrie: een of andere IBKR-binary met een Windows-nasmaak, nog een VM, nog een ISO van 4GB om te downloaden, nog een ronde «waarom blijft mijn cursor vastzitten in het noVNC-venster».
Kwam niet. IB Gateway, de headless trading-terminal die IBKR uitbrengt voor API-toegang, draait native op Linux. Geen .exe. Geen wine. Geen wheel dat alleen bouwt voor win_amd64. Gewoon een JVM-proces dat je rechtstreeks in een container kan kieperen. Dus ibkr-httpapi is het zusterproject dat mt5-httpapi altijd verdiend heeft: dezelfde vorm, curl erin, JSON eruit, één bearer token, één URL-schema per assetklasse, alleen bestaat de hele stack nu uit Linux-containers. Geen KVM. Geen Windows-ISO. Geen noVNC naar een compleet bureaublad alleen maar om je eens per decennium door een Windows-installer te klikken.
Praten Met Een Broker Is Ellende
De geschiedenis van de Interactive Brokers-API is zo’n ding waardoor je begrijpt waarom elke retail trading-bot op GitHub of verlaten is of drie jaar achterloopt:
- De TWS-API bestaat alleen als er iets met een grafische interface draait, of TWS zelf, of zijn headless broer IB Gateway. Er is geen «roep een REST-endpoint aan en klaar», er is een socketprotocol dat alleen praat met een levende desktopsessie.
- Die sessie inloggen betekent door een loginscherm klikken, voorwaarden accepteren, soms een 2FA-push op je telefoon bevestigen, elke dag, want IBKR verbreekt de gateway elke 24 uur geforceerd, of je dat nou leuk vindt of niet.
ib_async(de onderhouden fork van het opgegevenib_insync) geeft je een fatsoenlijke asyncio-wrapper over die socket, maar reikt je rauweContract-objecten aan, dus je bouwt nog steeds met de handStockversusOptionversusFutureversusBagvoor elke assetklasse, elk met andere verplichte velden.- IBKR knipt je API-toegang af bij herhaalde pacing-overtredingen. Er is geen zacht waarschuwingssysteem, je ramt te hard op het endpoint voor historische data en je krijgt een schorsing. De meeste zelfgebouwde scripts hebben nul besef van rate limits tot het ze één keer bijt.
- Elke marktdata-aanroep die je doet wordt of weggegooid zodra je hem gelezen hebt, of je schrijft voor de vierde keer dit jaar je eigen CSV/SQLite-persistentielaag vanaf nul.
- Wil je RSI of MACD bovenop de candles die je net ophaalde? Dat is nog een dependency, nog een indicatorbibliotheek, nog een set randgevallen rond NaN-opwarmperiodes.
Niets daarvan is echt IBKR’s schuld, het is een brokerage-API op profniveau, geen speeltje, maar het betekent wel dat elk project dat «geef me AAPL’s OHLC over HTTP» wil uiteindelijk dezelfde zes dingen slecht opnieuw uitvindt. Ik was het zat om ze opnieuw uit te vinden, dus heb ik het ding één keer goed gebouwd en er een spec voor gezet.
De Stack
De stack is een FastAPI-service (ibkrapi/) voor een IB Gateway-container, die ermee praat over de TWS-API-socket via ib_async. Niets exotisch, één gedeelde IB-instantie, bewaakt door een asyncio.Lock zodat gelijktijdige requests niet over elkaar heen struikelen in de connect-aanroep, met exponentiële backoff (start op de geconfigureerde reconnect_backoff, verdubbelt tot reconnect_max_backoff) als de socket ooit wegvalt. Elke router trekt de verbinding via één functie, get_ib(), de eerste aanroeper wacht op de handshake, alle anderen hergebruiken hem. Wanneer IBKR de gateway geforceerd verbreekt voor zijn dagelijkse herstart, maakt het volgende binnenkomende request on demand opnieuw verbinding in plaats van de hele API om te leggen.
De gateway-container is gebouwd op gnzsnz/ib-gateway-docker, met IBC (IB Controller) erin gebakken om de headless loginstroom af te handelen, gebruikersnaam en wachtwoord komen uit een gitignorede .env.ibkr, en IBC bestuurt de login-interface namens IB Gateway zodat niemand een muis hoeft te bewegen. Eén juridisch detail dat het waard is om te weten: IBKR’s installerlicentie verbiedt het herdistribueren van voorgebouwde images die hun binary bevatten, dus bouw je het gateway-image lokaal in plaats van de Docker Hub-tag van een willekeurige vreemde met de IBKR-installer erin gebakken binnen te halen. docker-compose.yml.example schrijft dat uit en levert standaard een muteerbare :stable-tag die je geacht wordt vast te pinnen op een digest zodra je je eigen image hebt gebouwd.
Zes Assetklassen, Eén URL-Schema
Elk markttype krijgt zijn eigen prefix, in plaats van één overbelast «symbol»-endpoint dat stilletjes raadt wat je bedoelde:
/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 defaultAandelen, opties, futures, CFD’s, forex, crypto, zes assetklassen, één Contract-factory voor elk in ibkrapi/contracts.py, elk vraagt precies de disambiguatieparameters die die klasse echt nodig heeft (opties willen expiry/strike/right, futures willen expiry/exchange, aandelen willen vrijwel niets) en vult de rest in vanuit config.yaml:contract_defaults.<class> zodat je niet bij elke aanroep «SMART/USD» herhaalt. Waar het zinnig is voor die assetklasse krijg je dezelfde vier werkwoorden: contractdetails, een live tick-snapshot (met de grieken eraan vast voor opties), historische candles en rauwe historische ticks. Boven op de zes marktdataklassen zit cross-asset orderinvoer, een positielijst, het accountoverzicht en een historie van executies en afgeronde orders, allemaal geserveerd onder hetzelfde /v1-prefix.
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"Eerst De Spec, Niet Op Gevoel
Geen van die routers hierboven is een handgeschreven FastAPI-decorator die verspreid over bestanden ligt te hopen dat ze synchroon blijven. api/v1.yaml is de echte bron van waarheid, een OpenAPI 3.1.0-document met 42 operaties en 34 schema’s, en make generate is het overkoepelende target dat er drie verschillende generators mee aandrijft: fastapi-codegen (met eigen Jinja-templates) spuugt ibkrapi/api/_generated/{models.py, routers/*.py} uit als async stub-handlers die direct delegeren naar het met de hand geschreven ibkrapi/api/impl.py; oapi-codegen levert een volledig getypeerde Go-client in pkg/clients/go/client.gen.go; en openapi-python-client genereert een op zichzelf staand, installeerbaar Python-clientpakket. Negen routers worden in server.py op de app gemonteerd, system, stocks, options, futures, cfd, forex, crypto, orders, history, allemaal onder een vast /v1-prefix. Niemand bewerkt met de hand iets onder _generated/ of pkg/clients/*; je wijzigt de spec, draait de generator opnieuw, en de driftcheck in CI sloopt de build als je het vergeet.
MCP, Zodat Je Een Agent Geen curl-Handleiding Hoeft Te Geven
Diezelfde FastAPI-app monteert ook een MCP-server op /mcp, streamable-HTTP, in de wortel van de app, niet onder het /v1-prefix. Hij begon zijn leven met drie generieke tools (ping, endpoints, request) waarmee een agent elke REST-route kon aanroepen via methode en pad. Werkt technisch, maar het betekent dat het model tijdens runtime een OpenAPI-catalogus moet binnenhalen en zich vervolgens een weg moet raden door een padtemplate, wat precies de «lees eerst de documentatie»-belasting is die de spec juist moest afschaffen. Dus dat ging eruit: /mcp biedt nu 24 toegewijde, getypeerde tools, elk met getypeerde parameters en een beschrijving die de agent leest. Het schema is de documentatie.
Het interessante deel is wat er met de zes assetklassen gebeurde. Over REST zijn dat zes URL-prefixen. Over MCP vouwen ze samen achter één asset_class-enum, stock, option, future, cfd, forex, crypto, zodat get_contract, get_quote, get_rates en get_rates_ta het werk doen van wat anders 24 vrijwel identieke tools per klasse zouden zijn, die de context van de agent opblazen voor nul extra betekenis. Daaromheen zitten de speciale gevallen die alleen voor één klasse zinnig zijn (get_option_chain, place_option_combo, exercise_option, get_future_continuous, list_future_contracts, get_stock_ticks), de orderfamilie (list_orders, get_order, place_order, cancel_order, cancel_all_orders), account en posities (get_account, get_account_values, list_accounts, list_positions), historie (get_executions, get_completed_orders), plus ping en het oorspronkelijke endpoints/request-paar dat blijft als noodluik voor alles zonder toegewijd tool.
Niets hiervan is een parallelle implementatie naast de REST-API die erop wacht ervan weg te drijven. Elk tool dispatcht in-process door dezelfde routers, dezelfde validatie, dezelfde bearer-auth en dezelfde rate limiter als een HTTP-aanroep, dus een agent die get_rates staat te rammen verbrandt hetzelfde historische pacing-budget van 10 minuten als curl, en krijgt dezelfde 429 RATE_LIMIT_NEAR-envelop wanneer hij over de streep gaat. Laat api_token leeg en beide oppervlakken zijn exact even ongeauthenticeerd.
Tools die iets veranderen dragen hun waarschuwing in het schema zelf, de enige plek waar het model gegarandeerd kijkt. De beschrijving van place_order eindigt met «Dit plaatst een echte, onomkeerbare order op een live brokerage-account, roep het alleen aan wanneer de gebruiker er expliciet om heeft gevraagd, en bevestig de parameters vooraf.» Dat is geen voetnoot in een README die de agent nooit zal openen; het staat in de tooldefinitie, meegeleverd bij elke aanroep.
Er zit ook een stuk MCP-clientrealiteit in gebakken. Starlette’s Mount("/mcp", ...) serveert op /mcp/* en stuurt een 307 op de kale vorm /mcp, en een flink deel van de MCP-clients volgt die redirect bij een POST simpelweg niet. In plaats van het halve ecosysteem te laten breken op een spec-technicaliteit is er middleware die kaal /mcp herschrijft naar /mcp/ voor het routeren en gewoon doorgaat.
Om het aan een client te hangen is er een @psyb0t/ibkr-httpapi-plugin, een dunne brug van stdio naar HTTP, via IBKR_HTTPAPI_URL op jouw instantie gericht (de serverwortel; een aangeplakte /v1 wordt afgeknipt, want iedereen doet dat precies één keer verkeerd) plus IBKR_HTTPAPI_TOKEN wanneer auth aanstaat. In Claude Code zijn dat twee regels:
claude plugin marketplace add psyb0t/agents
claude plugin install ibkr-httpapi@psyb0tWat de waarschuwing verderop op deze pagina een stuk scherper maakt: een agent met place_order en cancel_all_orders in zijn toollijst zit één slechte inferentie af van iets duurs doen. Het schema zegt hem eerst te bevestigen. Zorg dat iets anders hem dat ook zegt.
Auth, Want Dit Is Geen Speeltje
Bearer token, gecontroleerd met hmac.compare_digest in plaats van een kale ==, want een naïeve stringvergelijking lekt timinginformatie, met genoeg requests kan je een token karakter voor karakter bisecteren. Zet api_token in config.yaml (of API_TOKEN via env) en elk request heeft Authorization: Bearer <token> nodig of krijgt een 401 met de standaard foutenvelop: {code, message, details}. Laat het token leeg en de API ligt wagenwijd open voor alles wat de socket bereikt, wat prima is bij een loopback-only deployment achter nginx en overal elders een beroerd idee, en de documentatie van het project is daar niet mis te verstaan over.
Pacing + De Goudmijn
Elke aanroep richting IBKR gaat door een preventieve rate limiter voordat hij de socket ook maar aanraakt, want IBKR knipt de API-toegang af bij herhaalde pacing-overtredingen en «sorry, mijn bot wist dat niet» is geen verdediging die werkt. Drie niveaus, elk met een eigen sliding-window-teller plus een asyncio.Lock per contract plus een globale concurrency-semafoor: historical-data-aanroepen zijn afgetopt op soft-50/hard-55 per venster van 10 minuten (IBKR’s harde limiet ligt op 60), market_data-aanroepen blijven onder het plafond van ~50 berichten per seconde van de TWS-socket, en orders worden het hardst geknepen, 5/sec, 3 gelijktijdig, want een stortvloed aan orderaanroepen is vrijwel nooit opzettelijk. Ga over de zachte grens en je krijgt een waarschuwing in de logs; ga over de harde en de aanroeper krijgt een 429 RATE_LIMIT_NEAR-envelop met de precieze regel, het gebruik, de limiet en de retry-after ingebakken in details.
Achter diezelfde deur wordt alles wat cachebaar is bij elke aanroep naar schijf geschreven onder data/history/, candles en ticks gaan naar CSV-bestanden per (assetklasse, symbool, timeframe) in precies de vorm die wickworks binnenslurpt, contractdetails en metadata van optieketens krijgen een JSON-cache met lange TTL, en elke tick- of ketensnapshot wordt aan een historisch record geplakt. Er wordt nooit iets verwijderd; het is expliciet bedoeld als een append-only «goudmijn», je mount ./data, maakt er een back-up van, en elke aanroep die je doet stapelt zich stilletjes op tot een langetermijndataset in plaats van weggegooid te worden nadat je het antwoord één keer hebt gelezen. Heb je gegarandeerd verse data nodig in plaats van de cache? Elk cachebaar endpoint accepteert ?refresh=true, wat de cache-read overslaat maar het verse resultaat alsnog terugschrijft zodat de volgende aanroeper er profijt van heeft.
Technische Analyse Zonder Een Indicatorbibliotheek Te Schrijven
Het /rates-endpoint van elke assetklasse heeft een broer, POST /<class>/<symbol>/rates/ta, die dezelfde candles ophaalt en ze doorgeeft aan wickworks, dezelfde TA-sidecar die mt5-httpapi al gebruikt. RSI, MACD, bollingerbanden, ADX, ATR, VWAP, Ichimoku, order blocks, fair value gaps, BOS/CHoCH-structuurbreuken, swingstructuur, steun- en weerstandsniveaus, liquiditeitszones, sessie-ankers, server-side berekend, in één aanroep, op candles die je toch al hebt. Sinds de laatste update is dit slimmer geworden: het TA-pad bouwt nu op dezelfde candle-cache die /rates gebruikt in plaats van zijn eigen aparte fetch te doen, dus een herhaalde TA-aanvraag tegen gecachete candles kost nul IBKR-pacing-budget en komt alsnog terug met verse indicatorberekeningen. wickworks blijft met opzet strikt primitief, rauwe reeksen en structurele feiten, nooit «koop» of «verkoop», dus als je meningen wil bouw je die in je eigen consumer, niet in de sidecar.
Je richt het op je eigen instantie met wickworks.url in de config; laat het leeg en /rates/ta geeft gewoon netjes een 503 terug in plaats van te doen alsof het werkt. De uitgaande aanroep zelf is beperkt op schema, alleen http:// en https://, precies zodat een verkeerd geconfigureerde URL niet verbogen kan worden tot een SSRF tegen iets als file://.
Waarom In Godsnaam Een Headless Gateway Een VNC-Oppervlak Nodig Heeft
Terechte vraag, want IB Gateway is niet echt headless in de klassieke zin, het is een Java Swing GUI-applicatie die draait onder een virtuele framebuffer (Xvfb) binnen de container. IBC bestuurt die GUI programmatisch om in te loggen en zich door de dialogen van de dagelijkse herstart te klikken, en redt zich in de overgrote meerderheid van de gevallen zonder mens in de buurt. Maar IBKR gooit af en toe een stok tussen de spaken: een wekelijkse 2FA-push die verloopt, een onverwachte dialoog die IBC’s automatisering niet herkent, een «nieuw apparaat»-bevestiging de eerste keer dat je een vers gebouwde gateway opstart. Als dat gebeurt moet je het bureaublad achter Xvfb daadwerkelijk *zien*, en daar is Dockerfile.novnc voor, een kleine websockify-proxy die de VNC-poort van de gateway (:5900) naar voren brengt over HTTP/WebSocket zodat je het IB Gateway-bureaublad kan bekijken (en aanklikken) vanuit een gewone browsertab, zonder native VNC-client. Het is geen volledige virtuele machine zoals de dockurr/windows-opstelling die mt5-httpapi nodig heeft, er is geen besturingssysteem om te booten, het is een python:3.12-slim-image van 57 regels, vastgepind op digest, dat websockify draait en waarvan de entrypoint alleen je VNC-wachtwoord in index.html zet zodat hij zichzelf direct met de sessie verbindt. Je richt er een browser op wanneer er iets vastloopt, repareert die dialoog, sluit de tab, en vergeet dat het bestaat tot de volgende wekelijkse 2FA-hik.
Container-Hardening, Want Dit Raakt Geld Aan
De API- en wickworks-containers draaien met cap_drop: [ALL], read_only: true-rootbestandssystemen met noexec,nosuid-tmpfs-mounts voor de delen die moeten schrijven, no-new-privileges:true, en geheugen-, CPU- en pid-limieten per service. Het netwerk valt uiteen in drie geïsoleerde Docker-netwerken: front (nginx die met de API praat), backend (de API die met de gateway praat, die uitgaand naar IBKR’s cloud moet), en een internal: true-netwerk voor het verkeer van API naar wickworks dat helemaal geen weg naar buiten heeft, wickworks kan fysiek niet naar huis bellen, zelfs al zou je dat willen. De gateway-container is de ene uitzondering die niet volledig dichtgetimmerd kan draaien (Xvfb plus een JVM plus IBC dat kriskras over het bestandssysteem schrijft verdraagt geen read-only root), dus die krijgt no-new-privileges als ondergrens. Alle publieke base-images zijn vastgepind op SHA-digest, Python-dependencies zijn hash-vergrendeld met uv pip compile --generate-hashes en geïnstalleerd met --require-hashes, er is een rollende leeftijdsdrempel van 7 dagen op nieuwe dependency-versies zodat een vers gepubliceerd, in de toeleveringsketen vergiftigd pakket niet dezelfde dag kan landen, en make audit / make audit-go / make audit-compose draaien respectievelijk pip-audit, govulncheck en een grep-gebaseerde compose-scanner (verboden instellingen zoals privileged, pid:host, Docker-socketmounts, ongepinde tags, publiek geëxposeerde poorten).
Het Deel Waarin Dit Ding Een Verdomd Echt Brokerage-Account Bedient
Ik ga dit niet in een voetnoot begraven. Dit is geen marktdata-speeltje, POST /orders plaatst een echte order tegen een echt IBKR-account, en het verplaatst echt geld op het moment dat die geaccepteerd wordt. Er is bewust geen endpoint om orders te wijzigen: om een rustende order te veranderen annuleer je hem (DELETE /orders/{orderId}) en plaats je er opzettelijk een nieuwe, in plaats van een levende order onderweg te muteren. DELETE /orders zonder ID annuleert *alle* openstaande orders van het account in één klap. POST /options/exercise oefent echte contracten uit of laat ze verlopen. Geen van deze heeft een ongedaan-maken-knop. Als je een agent of een script op dit ding richt, laat het dan bij een mens het opgeloste symbool, de richting, de hoeveelheid en de prijs bevestigen voordat er iets wijzigends afgaat, en laat het nooit automatisch een geweigerde order opnieuw proberen, een weigering is een stopsignaal, geen bug om omheen te werken.
Twee dingen dempen de explosieradius als je dat wil: TRADING_MODE=paper in .env.ibkr verbindt met IBKR’s paper trading-gateway (poort 4002) in plaats van de live gateway (4001), papieren accountnummers beginnen met DU, live nummers met U, en /accounts vertelt je met welke je daadwerkelijk praat. En IBC ondersteunt READ_ONLY_API=yes, wat de hele trading-API op gatewayniveau dichtzet als je van dit ding alleen marktdata en accountinzicht wil. Laat api_token leeg en dit hele oppervlak, marktdata, posities en het plaatsen van orders evengoed, is ongeauthenticeerd voor alles wat de poort bereikt. Zet het token. Luister op loopback. Wees niet de reden dat het script van iemand anders een order op jouw account plaatst.
De Slotsom
Als mt5-httpapi «zorg dat MetaTrader 5 HTTP praat, zelfs met Windows ertussen» was, dan is ibkr-httpapi de versie waarin Windows er nooit tussen zat, IB Gateway draait gewoon op Linux als een normaal stuk serversoftware, dus de hele stack bestaat uit containers, een spec-first API met gegenereerde clients in twee talen, 24 getypeerde MCP-tools die door diezelfde handlers lopen wanneer een agent aan het stuur zit, pacing die voorkomt dat IBKR je bant, een schijfcache die elke aanroep in permanente data verandert in plaats van weggegooide JSON, en dezelfde wickworks TA-sidecar die het indicatorrekenwerk server-side doet. Zes assetklassen, één bearer token, nul Windows-VM’s. Pak het op github.com/psyb0t/ibkr-httpapi, lees de licentienotities voordat je het gateway-image bouwt, en zet een API-token voordat je dit aan iets anders dan localhost blootstelt. Het staat onder WTFPL, doe ermee wat je verdomme wil, geef mij alleen niet de schuld wanneer je bot 500 puts op de verkeerde ticker koopt.