Part of Broker Architecture
I've integrated four brokers into the same codebase. Three are still running. One is gone. This is the evaluation framework I wish I'd had before I started — the things that actually matter when your trading system needs to run unattended, and the things that don't matter until they ruin your week.
The four brokers
What matters is the API. Can your code authenticate reliably, receive market data in real time, submit orders without ambiguity, and recover from failures without manual intervention? Everything else — platform UIs, educational resources, marketing pages — is noise for a system that trades unattended.
Over the life of this system, I've integrated:
- Interactive Brokers (IBKR) — first broker, now removed. Full integration including a custom Python port for their encryption.
- TradeStation — current primary broker. Handles both market data and order execution.
- Tradovate (owned by NinjaTrader — they're the same company, but Tradovate is what offers the API) — secondary broker. I use it for execution only. More on why below.
- ThinkOrSwim (Schwab) — data source only. Their API doesn't support futures order execution.
TradeStation handles both market data and execution — the system could run on it alone. Tradovate is there for two unglamorous reasons: its commission structure can be cheaper if I pay the upfront membership fee, and I have capital already parked in the account that I haven't bothered to consolidate. ThinkOrSwim provides supplemental data. This isn't because TradeStation can't handle the job. The real lesson is that a broker's failure modes get discovered in production, not in the docs — and the IBKR Saga made that expensive enough to shape the architecture.
Authentication
Authentication isn't a one-time setup — it's a background process that runs continuously for the lifetime of your system.
TradeStation uses OAuth2 with per-user API credentials. Standard libraries handle the flow. Token refresh is automatic — a dedicated process polls every 10 seconds and refreshes before expiry. The system runs for days without thinking about auth. Paper and live trading use the same code path, different subdomain (sim-api vs api).
ThinkOrSwim also uses OAuth2, same shape as TradeStation. App-level credentials rather than per-user, but the same refresh pattern works.
Tradovate uses username/password authentication — no OAuth dance, just a direct POST with credentials, a device ID, and API keys. Request tokens too aggressively and Tradovate responds with a penalty-token flow (p-ticket) that enforces a mandatory wait and can't be cleanly automated. You discover this in production, not in the docs.
The coordination model is also inverted. TradeStation's sessions pull fresh tokens from the store on every call. Tradovate's token store has to push new tokens to live WebSocket sessions and trigger re-authentication. Same concept, completely different direction of data flow.
IBKR used a non-standard OAuth 1.0a with Diffie-Hellman key exchange. Elixir didn't have the crypto primitive. I wrote a Python port to handle it. That port became a dependency I owned, on a protocol I didn't design, for a broker that would eventually break my trust. It worked, but the operational surface area was enormous.
Symbol conventions
Every broker thinks their symbology is the standard. None of them agree with each other.
The same E-mini S&P 500 futures contract:
| Broker | Symbol | Convention |
|---|---|---|
| CME (canonical) | ES | Exchange root code |
| TradeStation | @ES | @ prefix for continuous contracts |
| Tradovate | ES | Canonical codes, server-side contract resolution |
| ThinkOrSwim | /ES | / prefix for all futures |
That's the easy case. Try the Euro FX contract:
| Broker | Symbol |
|---|---|
| CME (canonical) | 6E |
| TradeStation | @EC |
| Tradovate | 6E |
| ThinkOrSwim | /6E |
TradeStation doesn't just add a prefix — it uses a completely different root code for about 30 products. Euro FX becomes EC. Corn (ZC) becomes C. 30-Year Treasury (ZB) becomes US. Japanese Yen (6J) becomes JY. You won't find a pattern. You'll find a lookup table.
Continuous contracts add another dimension. TradeStation supports them for most products (prefix with @), but about 40 products — Eurex, micro grains, crypto, and others — don't support continuous contracts at all. For those, you need to build specific contract symbols with maturity codes: FDXSH25 for the March 2025 DAX future. Tradovate takes a different approach. It has no continuous contract syntax, but it resolves root symbols to the active front-month contract server-side. ES always refers to whatever's trading now. ThinkOrSwim's behavior here is similar in practice — you work with the root symbol and the broker handles the contract lookup.
Streaming protocols
Your system needs two kinds of real-time data: market data (bars, quotes) and account events (order fills, position changes).
Market data
Only two of my four brokers provide market data feeds.
TradeStation uses Server-Sent Events (SSE) over long-lived HTTP connections. Not WebSocket — HTTP streaming via Finch. Messages arrive as \r\n-delimited JSON chunks. The server sends GoAway messages to request graceful reconnection. Rate-limited to about 490 barchart requests per 5-minute sliding window.
ThinkOrSwim uses WebSocket with JSON commands and Schwab-specific headers in every message. Data arrives in numbered-field format — field "1" is time, "2" is open, and so on. You decode it yourself. The WebSocket URL itself comes from a user preferences API call, not from documentation.
Tradovate offers market data, but consuming it programmatically requires obtaining a market data license from CME directly and submitting it to Tradovate — on top of their own data charges. TradeStation's market data comes bundled with the brokerage account at no extra cost. That's why Tradovate is execution-only in my system — the economics don't justify a second paid market data feed when the primary one is already working.
Account events
Order fills and position updates also arrive via streaming — but on a completely different protocol than market data, even from the same broker.
TradeStation streams order and position updates over separate SSE connections, same pattern as market data but different endpoints.
Tradovate uses WebSocket with a custom text-based protocol — not JSON for requests. Commands look like "authorize\n{id}\n\n{token}". Request routing uses incrementing IDs matched against a routes map. On connect, it provides a complete state snapshot (accounts, orders, positions, balances), which is useful but means you need to handle a large initial payload. This is the only streaming connection to Tradovate — there's no separate market data feed.
Order execution
This is where brokers earn or lose your trust.
TradeStation supports bracket orders natively — an entry order with an attached stop loss and profit target, submitted as a single atomic request. If the entry fills, the protective orders are guaranteed to be working. No race condition between entry fill and stop placement. Errors are honest and unambiguous.
Tradovate handles execution reliably, but its rate limits are tight — 60 order operations per minute, 300 general API calls per minute. Your REST client needs exponential backoff with Retry-After support, or you'll hit 429s during any burst of activity. Less forgiving than TradeStation, but honest about where the edges are.
ThinkOrSwim cannot execute futures orders through their API. Data only.
IBKR could execute orders. Whether it would is a different question. Stop orders silently failed in production — the broker accepted them, acknowledged them, and then didn't execute them. I required a separate order reconciliation system, a growing error suppression list (40+ broker-specific error codes), and eventually, a complete replacement.
Operational concerns
Session limits
IBKR allows a single active session per account. Start a new connection, the old one dies. If your system has orphaned processes from a previous session, the new session invalidates them. Debugging sessions involving phantom WebSocket disconnections taught me more about OTP than any tutorial.
Environment parity
TradeStation's paper and live environments are the same code path with a different subdomain. Tradovate uses demo and live subdomains — same idea, different naming. IBKR required entirely separate credentials for paper trading — a different account, a different auth setup, effectively a second integration to maintain. If your paper and live code paths diverge, your paper testing doesn't tell you what you think it tells you.
Token storage
OAuth tokens and API credentials need to be encrypted at rest. This sounds obvious, but the implementation varies. Some tokens carry their own client credentials (TradeStation stores client_id and client_secret per token record), while others use app-level credentials from environment variables. Your credential storage model needs to handle both patterns.
Idle resource management
Sessions that stay open consume broker-side resources and sometimes rate-limit capacity. All three of my active broker sessions use the same pattern: the session process shuts itself down after 60 seconds of inactivity, and its consumers — account managers, data sources — send heartbeats every 15 seconds to keep it alive. If a consumer crashes, the session cleans up after itself instead of leaking. This pattern fell out of the IBKR experience, where leaked sessions caused cascading failures under the supervision tree.
The priority order
All of these concerns — session leaks, paper/live drift, token storage — live in the same place: below the API surface, where broker-specific behavior either meets you halfway or doesn't. After four integrations, this is the order I evaluate in.
- Authentication complexity
- Order execution reliability
- Symbol convention sanity
- Streaming protocol maturity
- Environment parity
- Error transparency
Commission rates, platform features, educational content — none of it matters if the API can't be trusted to execute a stop order at 2am.
The next post covers how I designed the abstraction layer that makes the remaining three coexist in the same system — three different APIs, three different protocols, one unified interface that the trading logic never looks past. Later, a paid post goes deeper into the GenServer state design sitting behind these sessions — what lives in memory, what gets persisted, and how the system reconciles with the broker after a restart.
IBKR is gone. Four integrations in, the system stopped caring which broker is at the other end of the wire.