ibkr-httpapi: Șase Clase de Active, Fără VM de Windows, Numai Bani Adevărați

Deja mi-am ars o lună din viață construind mt5-httpapi, un VM Windows complet pornit sub QEMU/KVM înăuntrul lui Docker, doar ca să pot vorbi cu un broker care livrează un wheel de Python numai pentru Windows. Merge. Și e complet dus cu pluta: o instalare de Windows 11 virtualizată hardware, curățată până la sânge, care rulează MetaTrader 5 în mod portabil, ca să pot lovi un endpoint REST în loc să scriu MQL5. Aș face-o din nou. Dar când m-am apucat să leg Interactive Brokers, am stat vreo zece minute încordat, așteptând același coșmar: vreun binar IBKR pentru Windows, încă un VM, încă un ISO de 4GB de descărcat, încă o rundă de „de ce mi se blochează cursorul în fereastra noVNC”.
Nu s-a întâmplat. IB Gateway, terminalul de tranzacționare fără interfață pe care îl livrează IBKR pentru acces prin API, rulează nativ pe Linux. Fără .exe. Fără wine. Fără vreun wheel care se compilează doar pe win_amd64. Doar un proces JVM pe care îl poți arunca direct într-un container. Așa că ibkr-httpapi e proiectul frate pe care mt5-httpapi l-a meritat dintotdeauna: aceeași formă, curl la intrare, JSON la ieșire, un singur bearer token, o singură schemă de URL pe clase de active, doar că tot stack-ul e containere de Linux. Fără KVM. Fără ISO de Windows. Fără noVNC într-un desktop întreg doar ca să dai click printr-un wizard de instalare Windows o dată la un deceniu.

E O Mizerie Să Vorbești cu Un Broker

Povestea cu API-ul de la Interactive Brokers e genul de chestie care te face să înțelegi de ce fiecare bot de tranzacționare de retail de pe GitHub e ori abandonat, ori vechi de trei ani:

  • TWS API există doar dacă rulează ceva cu interfață grafică, fie TWS-ul propriu-zis, fie fratele lui fără interfață, IB Gateway. Nu există „apelezi pur și simplu un endpoint REST”, există un protocol pe socket care vorbește doar cu o sesiune de desktop activă.
  • Ca să loghezi sesiunea aia trebuie să dai click printr-un ecran de login, să accepți termenii, uneori să pleznești un push de 2FA pe telefon, în fiecare zi, pentru că IBKR îți deloghează forțat gateway-ul o dată la 24 de ore, fie că îți place, fie că nu.
  • ib_async (forkul întreținut al abandonatului ib_insync) îți dă un wrapper asyncio solid peste socketul ăla, dar îți întinde obiecte Contract brute, tot tu construiești de mână Stock față de Option față de Future față de Bag pentru fiecare clasă de active, cu câmpuri obligatorii diferite la fiecare.
  • IBKR îți taie accesul la API pentru încălcări repetate de pacing. Nu au niciun sistem blând de avertizare, lovești prea tare endpointul de date istorice și mănânci o suspendare. Majoritatea scripturilor făcute în casă au zero conștiință de rate limit până când sunt mușcate o dată.
  • Fiecare apel de date de piață pe care îl faci ori se aruncă după ce îl citești, ori îți scrii singur încă un strat de persistență în CSV/SQLite, de la zero, din nou, pentru al patrulea proiect din anul ăsta.
  • Vrei RSI sau MACD peste barele pe care tocmai le-ai tras? Asta e încă o dependință, încă o bibliotecă de indicatori, încă un set de cazuri limită legate de perioadele de încălzire cu NaN.

Nimic din toate astea nu e chiar vina IBKR, e un API de brokeraj de nivel profesionist, nu o jucărie, dar înseamnă că fiecare proiect care vrea „dă-mi OHLC pentru AAPL peste HTTP” ajunge să reinventeze prost aceleași șase lucruri. M-am săturat să le tot reinventez, așa că am construit chestia o dată, ca lumea, și i-am pus o specificație în față.

Stack-ul

Stack-ul e un serviciu FastAPI (ibkrapi/) care stă în fața unui container de IB Gateway și vorbește cu el peste socketul TWS API prin ib_async. Nimic exotic, o singură instanță IB partajată, păzită de un asyncio.Lock ca cererile concurente să nu se calce în picioare la apelul de conectare, cu backoff exponențial (pornește de la reconnect_backoff-ul configurat, se dublează până la reconnect_max_backoff) dacă pică vreodată socketul. Fiecare router trage conexiunea printr-o singură funcție, get_ib(), primul apelant așteaptă handshake-ul, toți de după îl refolosesc. Când IBKR deloghează forțat gateway-ul pentru restartul lui zilnic, următoarea cerere care intră se reconectează la nevoie, în loc să pice tot API-ul.
Containerul de gateway e construit din gnzsnz/ib-gateway-docker, cu IBC (IB Controller) copt înăuntru ca să se ocupe de fluxul de login fără interfață, userul și parola intră printr-un fișier .env.ibkr ținut în gitignore, iar IBC conduce interfața de login în locul lui IB Gateway, ca să nu fie nevoie să dea nimeni click. O subtilitate legală de care merită să știi: licența de instalare de la IBKR interzice redistribuirea imaginilor pre-construite care conțin binarul lor, așa că îți construiești imaginea de gateway local în loc să tragi tagul de pe Docker Hub al cine știe cui, cu installerul IBKR copt înăuntru. docker-compose.yml.example spune asta explicit și are ca implicit un tag mutabil :stable, pe care se așteaptă să îl fixezi pe un digest odată ce ți-ai construit-o pe a ta.

Șase Clase de Active, O Singură Schemă de URL

Fiecare tip de piață primește propriul prefix, în loc de un singur endpoint „symbol” supraîncărcat care ghicește în tăcere ce ai vrut să spui:

/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 default

Acțiuni, opțiuni, futures, CFD-uri, forex, crypto, șase clase de active, câte o fabrică de Contract pentru fiecare în ibkrapi/contracts.py, fiecare luând exact parametrii de dezambiguizare de care are nevoie clasa aia (opțiunile vor expiry/strike/right, futures vor expiry/exchange, acțiunile nu vor mai nimic) și completând restul din config.yaml:contract_defaults.<class>, ca să nu repeți „SMART/USD” la fiecare apel. Unde are sens pentru clasa aia de active primești aceleași patru verbe: detalii de contract, un snapshot de tick live (cu grecii atașați pentru opțiuni), bare istorice și tick-uri istorice brute. Peste cele șase clase de date de piață mai e intrarea de ordine cross-asset, o listă de poziții, sumarul de cont și un istoric de execuții și ordine finalizate, toate servite sub același prefix /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"

Întâi Specificația, Nu Din Burtă

Niciunul din routerele de mai sus nu e un decorator FastAPI scris de mână, împrăștiat prin fișiere și sperând să rămână sincronizat. api/v1.yaml e sursa adevărată a adevărului, un document OpenAPI 3.1.0 cu 42 de operații și 34 de scheme, iar make generate e ținta-umbrelă care pune în mișcare trei generatoare separate pornind de la el: fastapi-codegen (cu șabloane Jinja proprii) scuipă ibkrapi/api/_generated/{models.py, routers/*.py} ca handlere-ciot async care deleagă direct în ibkrapi/api/impl.py, scris de mână; oapi-codegen emite un client Go complet tipat la pkg/clients/go/client.gen.go; iar openapi-python-client generează un pachet de client Python de sine stătător, instalabil. Nouă routere se montează pe aplicație în server.py, system, stocks, options, futures, cfd, forex, crypto, orders, history, toate sub un prefix fix /v1. Nimeni nu editează de mână nimic din _generated/ sau pkg/clients/*; schimbi specificația, rulezi din nou generatorul, iar verificarea de drift din CI pică build-ul dacă uiți.

MCP, Ca Să Nu Îi Dai Agentului un Manual de curl

Aceeași aplicație FastAPI montează și un server MCP la /mcp, streamable-HTTP, stând la rădăcina aplicației, nu sub prefixul /v1. A pornit în viață cu trei unelte generice (ping, endpoints, request) care lăsau un agent să cheme orice rută REST după metodă și cale. Tehnic funcționează, dar înseamnă că modelul trebuie să tragă un catalog OpenAPI la runtime și apoi să își ghicească drumul printr-un șablon de cale, adică exact taxa de „citește întâi documentația” pe care specificația trebuia să o omoare. Așa că treaba aia a fost aruncată: /mcp expune acum 24 de unelte tipate dedicate, fiecare cu parametri tipați și o descriere pe care o citește agentul. Schema e documentația.
Partea interesantă e ce s-a întâmplat cu cele șase clase de active. Peste REST sunt șase prefixe de URL. Peste MCP se strâng în spatele unui singur enum asset_class, stock, option, future, cfd, forex, crypto, așa că get_contract, get_quote, get_rates și get_rates_ta țin locul a ceea ce altfel ar fi 24 de unelte per clasă aproape identice, care ar umfla contextul agentului fără să adauge niciun sens. În jurul lor stau specialele care au sens doar pentru o singură clasă (get_option_chain, place_option_combo, exercise_option, get_future_continuous, list_future_contracts, get_stock_ticks), familia de ordine (list_orders, get_order, place_order, cancel_order, cancel_all_orders), contul și pozițiile (get_account, get_account_values, list_accounts, list_positions), istoricul (get_executions, get_completed_orders), plus ping și perechea originală endpoints/request, păstrate ca plasă de siguranță pentru orice nu are o unealtă dedicată.
Nimic din asta nu e o implementare paralelă care stă lângă API-ul REST așteptând să iasă din sincron cu el. Fiecare unealtă rulează în proces prin aceleași routere, aceeași validare, aceeași autentificare pe bearer și același rate limiter ca un apel HTTP, deci un agent care bate în get_rates arde același buget de pacing istoric de 10 minute ca și curl și mănâncă același plic 429 RATE_LIMIT_NEAR când trece linia. Lași api_token gol și ambele suprafețe sunt neautentificate exact în același fel.
Uneltele care modifică ceva își cară avertismentul chiar în schemă, care e singurul loc unde modelul se uită garantat. Descrierea lui place_order se termină cu „Asta plasează un ordin real, ireversibil, într-un cont de brokeraj real, cheamă-l doar când utilizatorul a cerut explicit asta și confirmă întâi parametrii.” Nu e o notă de subsol într-un README pe care agentul nu îl va deschide niciodată; e în definiția uneltei, întinsă la fiecare apel.
Mai e și o bucată de realitate a clienților MCP coaptă înăuntru. Mount("/mcp", ...) din Starlette servește la /mcp/* și face 307-redirect pentru forma goală /mcp, iar o bună parte din clienții MCP pur și simplu nu urmează redirectul ăla pe un POST. În loc să lase jumătate din ecosistem să pice pe un detaliu tehnic de specificație, există un middleware care rescrie /mcp gol în /mcp/ înainte de rutare și își vede de treabă.
Ca să îl legi la un client există un plugin @psyb0t/ibkr-httpapi, o punte subțire de la stdio la HTTP, îndreptată spre instanța ta prin IBKR_HTTPAPI_URL (rădăcina serverului; un /v1 la coadă se taie, pentru că toată lumea greșește asta exact o dată) plus IBKR_HTTPAPI_TOKEN când e pornită autentificarea. În Claude Code sunt două linii:

claude plugin marketplace add psyb0t/agents
claude plugin install ibkr-httpapi@psyb0t

Ceea ce aduce avertismentul de mai jos de pe pagina asta mult mai clar în focus: un agent care are place_order și cancel_all_orders în lista de unelte e la o inferență proastă distanță de a face ceva scump. Schema îi spune să confirme întâi. Ai grijă să îi mai spună și altceva.

Autentificare, Pentru Că Asta Nu E O Jucărie

Bearer token, verificat cu hmac.compare_digest în loc de un == simplu, pentru că o comparație naivă de stringuri scurge informație de timing, suficiente cereri și poți bisecta un token caracter cu caracter. Setezi api_token în config.yaml (sau API_TOKEN din env) și fiecare cerere are nevoie de Authorization: Bearer <token>, altfel primește 401 cu plicul standard de eroare: {code, message, details}. Lași tokenul gol și API-ul e larg deschis pentru orice ajunge la socket, ceea ce e în regulă pe o instalare doar pe loopback în spatele nginx și o idee foarte proastă oriunde altundeva, iar documentația proiectului e directă în privința asta.

Pacing + Mina de Aur

Fiecare apel care pleacă spre IBKR trece printr-un rate limiter preventiv înainte să atingă socketul, pentru că IBKR taie accesul la API pentru încălcări repetate de pacing, iar „scuze, botul meu nu știa” nu e o apărare care ține. Trei niveluri, fiecare cu propriul contor pe fereastră glisantă, plus un asyncio.Lock per contract, plus un semafor global de concurență: apelurile de date historical sunt plafonate la soft-50/hard-55 pe fereastră de 10 minute (limita dură de la IBKR stă la 60), apelurile de market_data rămân sub plafonul de ~50 de mesaje pe secundă al socketului TWS, iar orders sunt strânse cel mai tare dintre toate, 5/sec, 3 concurente, pentru că un potop de apeluri de ordine aproape niciodată nu e intenționat. Treci de plafonul soft și primești un avertisment în loguri; treci de cel hard și apelantul mănâncă un plic 429 RATE_LIMIT_NEAR cu regula exactă, consumul, limita și retry-after coapte în details.
În spatele aceleiași porți, orice se poate cacha se scrie pe disc sub data/history/ la fiecare apel, barele și tick-urile intră în fișiere CSV per (clasă de active, simbol, timeframe) modelate pentru ingestia în wickworks, detaliile de contract și metadatele lanțurilor de opțiuni primesc un cache JSON cu TTL lung, iar fiecare snapshot de tick sau lanț se agață de un registru de istoric. Nimic nu se șterge; e gândit explicit ca o „mină de aur” în care doar se adaugă, montezi ./data, faci backup, și fiecare apel pe care îl faci vreodată se adună în tăcere într-un set de date pe termen lung în loc să fie aruncat după ce citești răspunsul o dată. Ai nevoie de o citire garantat proaspătă în loc de cache? Fiecare endpoint cachabil ia ?refresh=true, care ocolește citirea din cache dar tot scrie rezultatul proaspăt înapoi, ca să profite și următorul apelant.

Analiză Tehnică Fără Să Scrii O Bibliotecă de Indicatori

Endpointul /rates al fiecărei clase de active are un frate, POST /<class>/<symbol>/rates/ta, care aduce aceleași bare și le pasează lui wickworks, același sidecar de TA pe care îl folosește deja mt5-httpapi. RSI, MACD, Bollinger Bands, ADX, ATR, VWAP, Ichimoku, order blocks, fair value gaps, rupturi de structură BOS/CHoCH, structură de swing, niveluri de suport și rezistență, zone de lichiditate, ancore de sesiune, toate calculate pe server, într-un singur apel, pe bare pe care deja le ai. De la ultima actualizare treaba s-a deșteptat: calea de TA se compune acum cu același cache de bare pe care îl folosește /rates în loc să facă propria ei cerere separată, deci o cerere de TA repetată pe bare din cache costă zero buget de pacing la IBKR și tot se întoarce cu calcule de indicatori proaspete. wickworks rămâne strict primitiv prin design, serii brute și fapte structurale, niciodată „cumpără” sau „vinde”, deci dacă vrei păreri ți le construiești în propriul consumator, nu în sidecar.
Îl îndrepți spre propria ta instanță prin wickworks.url din config; îl lași gol și /rates/ta întoarce pur și simplu un 503 curat în loc să se prefacă că merge. Apelul care iese e restricționat pe schemă, doar http:// și https://, exact ca un URL prost configurat să nu poată fi împins într-un SSRF către ceva de genul file://.

De Ce Naiba Are Nevoie Un Gateway Fără Interfață de O Suprafață VNC

Întrebare corectă, pentru că IB Gateway nu e de fapt fără interfață în sensul clasic, e o aplicație Java Swing cu interfață grafică care rulează sub un framebuffer virtual (Xvfb) înăuntrul containerului. IBC conduce interfața aia programatic ca să se logheze și să dea click peste dialogurile de restart zilnic, și se descurcă în marea majoritate a cazurilor fără vreun om prin apropiere. Dar IBKR mai aruncă din când în când un băț prin roți: un push săptămânal de 2FA care expiră, un dialog neașteptat pe care automatizarea IBC nu îl recunoaște, o confirmare de „dispozitiv nou” prima dată când ridici o instanță proaspătă de gateway. Când se întâmplă asta trebuie chiar să *vezi* desktopul care stă în spatele lui Xvfb, și exact pentru asta e Dockerfile.novnc, un proxy websockify mic care pune portul VNC al gateway-ului (:5900) în față peste HTTP/WebSocket, ca să te poți uita la (și să dai click pe) desktopul IB Gateway dintr-un tab de browser obișnuit, fără client VNC nativ. Nu e o mașină virtuală întreagă ca setup-ul dockurr/windows de care are nevoie mt5-httpapi, nu e niciun sistem de operare de pornit, e o imagine python:3.12-slim de 57 de linii, fixată pe digest, care rulează websockify, iar entrypointul ei doar îți bagă parola de VNC prin șablon în index.html, ca să se conecteze singur direct în sesiune. Îndrepți un browser spre el când se blochează ceva, rezolvi dialogul ăla, închizi tabul și uiți din nou că există până la următorul sughiț săptămânal de 2FA.

Securizarea Containerelor, Pentru Că Asta Umblă la Bani

Containerele de API și de wickworks rulează cu cap_drop: [ALL], filesystem root read_only: true cu monturi tmpfs noexec,nosuid pentru bucățile care trebuie să scrie, no-new-privileges:true și limite de memorie, CPU și PID per serviciu. Rețeaua e împărțită în trei rețele Docker izolate: front (nginx care vorbește cu API-ul), backend (API-ul care vorbește cu gateway-ul, care are nevoie de ieșire spre cloudul IBKR) și o rețea internal: true pentru traficul dintre API și wickworks, care nu are absolut nicio cale de ieșire, wickworks fizic nu poate suna acasă nici dacă ai vrea tu. Containerul de gateway e singura excepție care nu poate rula complet închis (Xvfb plus un JVM plus IBC care scrie peste tot prin filesystem nu tolerează un root read-only), așa că primește no-new-privileges ca podea. Toate imaginile de bază publice sunt fixate pe digest SHA, dependințele de Python sunt blocate pe hash prin uv pip compile --generate-hashes și instalate cu --require-hashes, există o poartă de vârstă rulantă de 7 zile pentru versiunile noi de dependințe, ca un pachet otrăvit în lanțul de aprovizionare abia publicat să nu poată ateriza în aceeași zi, iar make audit / make audit-go / make audit-compose rulează pip-audit, govulncheck și, respectiv, un scaner de compose bazat pe grep (setări interzise ca privileged, pid:host, monturi de socket Docker, taguri nefixate, porturi legate public).


Partea în Care Chestia Asta Conduce Un Cont de Brokeraj Adevărat

N-am de gând să îngrop asta într-o notă de subsol. Nu e o jucărie de date de piață, POST /orders plasează un ordin real într-un cont IBKR real și mișcă bani reali în clipa în care e acceptat. Nu există endpoint de modificare a ordinelor, intenționat: ca să schimbi un ordin care stă îl anulezi (DELETE /orders/{orderId}) și pui unul nou, deliberat, în loc să modifici pe loc un ordin viu. DELETE /orders fără ID anulează *toate* ordinele deschise din cont dintr-o singură lovitură. POST /options/exercise exercită sau lasă să expire contracte reale. Niciunul dintre astea nu are buton de undo. Dacă îndrepți un agent sau un script spre chestia asta, pune-l să confirme unui om simbolul rezolvat, direcția, cantitatea și prețul înainte să tragă ceva care modifică, și nu îl lăsa niciodată să reîncerce automat un ordin respins, o respingere e un semn de stop, nu un bug de ocolit.
Două lucruri îți moaie raza de explozie dacă le vrei: TRADING_MODE=paper în .env.ibkr se conectează la gateway-ul de paper trading al IBKR (portul 4002) în loc de cel live (4001), numerele de cont de paper încep cu DU, cele live cu U, iar /accounts îți spune cu care vorbești de fapt. Și IBC suportă READ_ONLY_API=yes, care blochează complet API-ul de tranzacționare la nivel de gateway, dacă tot ce vrei de la chestia asta sunt date de piață și vizibilitate pe cont. Lași api_token gol și toată suprafața asta, date de piață, poziții și plasare de ordine deopotrivă, e neautentificată pentru orice ajunge la port. Setează tokenul. Ascultă doar pe loopback. Nu fi tu motivul pentru care scriptul altcuiva plasează un ordin în contul tău.

Pe Scurt

Dacă mt5-httpapi a fost „fă-l pe MetaTrader 5 să vorbească HTTP chiar dacă Windows stă în cale”, ibkr-httpapi e versiunea în care Windows n-a stat niciodată în cale de la bun început, IB Gateway rulează pur și simplu pe Linux ca o bucată normală de software de server, deci tot stack-ul e containere, un API cu specificația întâi și clienți generați în două limbaje, 24 de unelte MCP tipate care trec prin aceleași handlere pentru când conduce un agent, pacing care îl împiedică pe IBKR să te baneze, un cache pe disc care transformă fiecare apel în date permanente în loc de JSON aruncat, și același sidecar de TA wickworks care face calculele de indicatori pe server. Șase clase de active, un bearer token, zero VM-uri de Windows. Ia-l de la github.com/psyb0t/ibkr-httpapi, citește notele de licențiere înainte să construiești imaginea de gateway, și setează un token de API înainte să expui asta la ceva care nu e localhost. E licențiat WTFPL, fă cu el ce vrei, doar nu da vina pe mine când botul tău cumpără 500 de puturi pe tickerul greșit.