Example: Interactive Brokers
This is a worked example, not a shipped adapter. It shows how one broker's API
maps onto MarketDataFeed and TradingBrokerAdapter, and which of that broker's
constraints have to be designed around rather than discovered in production.
IBKR is a useful example because it supplies both market data and execution through the same account, so a single desk can get a working chart from one relationship. Verify current details against IBKR's own documentation before building — ports, limits, and entitlements change.
Choose an authorization path
This is the first decision, and it determines your deployment more than your code.
| Path | Requires a running desktop session | Aimed at | Trade-off |
|---|---|---|---|
| TWS API via Trader Workstation | Yes — the full desktop app, logged in | Individuals, developers | Easiest to start; a GUI app becomes production infrastructure |
| TWS API via IB Gateway | Yes — a lightweight session, still logged in | Small funds, automated desks | Far lower resource use than TWS, same API surface |
| Web API | No | Teams avoiding a desktop dependency | REST and WebSocket over standard auth; no GUI process to babysit |
| FIX CTCI | No | Institutional clients | Lowest latency, co-located; see FIX protocol |
The bridge service is the piece people underestimate. The TWS API is an asynchronous, callback-driven socket protocol with request ids — not a request/response HTTP API. Something has to hold that connection, correlate callbacks back to requests, and expose the result as the HTTP and WebSocket surface your gateway serves.
Authorizing through the desktop app
The TWS API is an interface to a running TWS or IB Gateway instance. There is no API without a logged-in session — the desktop app is the authorization.
Enable API access
In TWS or IB Gateway, open Edit → Global Configuration → API → Settings and:
- Enable "ActiveX and Socket Clients". Nothing connects until this is on.
- Turn off "Read-Only API" when the chart must place orders. It is enabled by default as a safety measure, and leaving it on is the correct choice for a chart that only displays data.
- Add the bridge service's address to trusted IPs when it does not run on the same host.
- Set a Master Client ID if several clients connect and all of them need to see order status updates, not just the client that submitted the order.
Ports
| Application | Live | Paper |
|---|---|---|
| Trader Workstation | 7496 | 7497 |
| IB Gateway | 4001 | 4002 |
Point paper and live at different ports and treat them as different
environments end to end. The broker adapter should declare which one it is
through executionEnvironment, so the SDK never has to infer a live account
from an account name.
Keep the session alive
A logged-in desktop session is now production infrastructure, and it has a schedule:
- Both TWS and IB Gateway require a daily restart. With auto-restart configured under
Global Configuration → Lock and Exit, the session restarts through the week and needs a manual restart and re-authentication once a week, on Sunday. - Select "Never lock Trader Workstation" for API hosts, or the session locks and stops serving.
- Expect a connectivity loss during IBKR's daily server maintenance. The session reconnects on its own; your bridge must survive that gap and tell the chart, rather than silently serving stale data.
- Two-factor authentication through IBKR Mobile applies to the login. Plan who performs the weekly re-authentication before you depend on it.
- One username cannot be logged into two trading applications at once. Running separate gateways — paper and live, or one per strategy — means creating additional usernames on the account.
Because the session can be down, the adapter must report status honestly:
import type { TradingConnectionStatus, TradingEvent } from '@tradescript/pro/sdk';
export function publishSessionStatus(
status: TradingConnectionStatus,
emit: (event: TradingEvent) => void,
): void {
emit({
type: 'connection-status',
status,
info: { status, connectionType: 'hybrid', message: 'IB Gateway session' },
});
}
A chart that shows disconnected during the Sunday restart window is correct. A
chart that keeps showing the last known positions as if they were live is not.
Mapping the API
| IBKR call or callback | SDK method |
|---|---|
reqHistoricalData | loadBars |
reqRealTimeBars (5-second bars) | subscribeRealTimeBars |
reqMktData | getQuotes, subscribeQuotes |
reqMktDepth | getDepth, subscribeDepth |
reqTickByTickData | getTimeAndSales, subscribeTimeAndSales |
reqMatchingSymbols | searchSymbols |
reqContractDetails | resolveSymbol, getInstrumentDetails |
placeOrder | placeOrder |
cancelOrder | cancelOrder |
openOrder, orderStatus | orders events |
reqPositions, position | positions events |
reqExecutions, execDetails | executions events |
reqAccountSummary | accounts in TradingState |
IBKR's real-time bars are 5-second bars. For a 1-minute chart, aggregate them in
the bridge and emit one bar per minute boundary — the chart replaces a bar whose
time matches the current one, so emitting the developing minute repeatedly is
correct and cheap.
Pacing and limits
These are the constraints that make a server-side cache mandatory rather than optional. They are counted against the IBKR session, so every browser tab shares one budget.
| Limit | Value |
|---|---|
| Simultaneous open historical data requests | 50 |
| Identical historical requests | Not within 15 seconds |
| Historical requests for the same contract, exchange, and tick type | Fewer than 6 within 2 seconds |
| Historical requests overall | No more than 60 in any 10-minute period |
BID_ASK historical requests | Count double |
| Bars of 30 seconds or less | Unavailable older than six months |
| Concurrent streaming market data lines | 100 by default |
| Client-to-TWS message rate | 50 messages per second |
Sixty historical requests per ten minutes is roughly one request every ten seconds. A user scrolling back through a chart can exceed that alone. The gateway must therefore:
- Serve closed bars from cache and never re-request a range it already holds.
- Queue and rate-limit history requests centrally, with one queue per session.
- Deduplicate identical in-flight requests instead of issuing them twice.
- Track the 100-line streaming budget and release lines when charts unsubscribe.
Surface pacing rejections as typed errors. A pacing violation that returns an empty bar array teaches the chart that no data exists for that range, and it will stop paging.
The broker adapter
Order placement is the same shape as any other broker: the chart submits an intent, your server revalidates, IBKR decides.
import type {
TradingBrokerAdapter,
TradingEvent,
TradingState,
} from '@tradescript/pro/sdk';
const listeners = new Set<(event: TradingEvent) => void>();
export const broker: TradingBrokerAdapter = {
executionEnvironment: 'paper',
async getState(): Promise<TradingState> {
const response = await fetch('/api/ibkr/state');
if (!response.ok) throw new Error(`State unavailable: ${response.status}`);
return response.json() as Promise<TradingState>;
},
subscribe(callback) {
listeners.add(callback);
return () => listeners.delete(callback);
},
async placeOrder(draft, context) {
const response = await fetch('/api/ibkr/orders', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ draft, accountId: context.accountId }),
});
if (!response.ok) {
return { accepted: false, status: 'rejected', message: await response.text() };
}
return { accepted: true, status: 'submitted' };
},
};
Map orderStatus and execDetails callbacks into orders and executions
events through the subscribers, exactly as in Broker
integration. The chart never invents a fill; it shows what
the broker reported.
Checklist
- Session host runs unattended, with auto-restart and lock disabled, and someone owns the weekly re-authentication.
- Paper and live use different ports, different usernames, and a declared
executionEnvironment. - Read-only API stays on for data-only deployments.
- History is cached server-side; the pacing budget is enforced by a central queue.
- Streaming lines are counted and released on unsubscribe.
- Disconnects are published as
connection-status, not hidden behind stale state. - Market data subscriptions are entitled on the IBKR account for every instrument the chart offers.
Next steps
- Build the data gateway — caching, multiplexing, and resync.
- Broker integration — the complete production path.
- FIX protocol — the CTCI path for latency-sensitive desks.