Architecture of IbkrApi
View SourceThis document explains the architecture and design principles of the IbkrApi library, providing context for developers who want to understand how the library works internally.
Architecture Overview
The IbkrApi library is designed to provide a clean, idiomatic Elixir interface to Interactive Brokers' Client Portal Web API. The library follows Elixir conventions and leverages the language's strengths in concurrent programming and fault tolerance.
Overview
IbkrApi is designed as a clean, idiomatic Elixir wrapper around Interactive Brokers' Client Portal API. The library follows these key design principles:
- Simplicity: Providing a straightforward interface to the IBKR API
- Consistency: Maintaining a consistent API design across all modules
- Error Handling: Comprehensive error handling with meaningful messages
- Type Safety: Using structs and typespecs for better code quality
System Architecture
The IbkrApi library provides two distinct communication channels with the IBKR Client Portal Gateway:
Dual-Channel Architecture
flowchart TD
Client["🏢 Client Code"]
%% REST API Channel
Client --> RestAPI["📡 REST API Calls"]
RestAPI --> ClientPortal["IbkrApi.ClientPortal"]
ClientPortal --> Auth["🔐 Auth"]
ClientPortal --> Portfolio["💼 Portfolio"]
ClientPortal --> Order["📋 Order"]
ClientPortal --> Contract["📄 Contract"]
ClientPortal --> Trade["💱 Trade"]
ClientPortal --> MarketData["📊 MarketData"]
Auth --> HTTP["IbkrApi.HTTP<br/>(Rate Limited)"]
Portfolio --> HTTP
Order --> HTTP
Contract --> HTTP
Trade --> HTTP
MarketData --> HTTP
HTTP --> SharedHTTP["IbkrApi.SharedUtils.HTTP"]
SharedHTTP --> Finch["🌐 Finch<br/>(HTTP Client)"]
%% WebSocket Channel
Client --> RealTime["⚡ Real-time Data"]
RealTime --> WebSocket["IbkrApi.Websocket"]
WebSocket --> WebSockex["🔌 WebSockex<br/>(Behavior)"]
WebSocket --> WSMarketData["📈 Market Data"]
WebSocket --> WSOrderUpdates["📋 Order Updates"]
WebSocket --> WSPnL["💰 P&L Streaming"]
%% Gateway
Finch --> Gateway["🏛️ IBKR Client Portal Gateway"]
WebSockex --> Gateway
Gateway --> RestEndpoint["🌐 REST API<br/>(Port 5000)"]
Gateway --> WSEndpoint["🔌 WebSocket API<br/>(wss://localhost:5000/v1/api/ws)"]
%% Styling
classDef clientCode fill:#e1f5fe
classDef restApi fill:#f3e5f5
classDef websocket fill:#e8f5e8
classDef http fill:#fff3e0
classDef gateway fill:#fce4ec
class Client clientCode
class RestAPI,ClientPortal,Auth,Portfolio,Order,Contract,Trade,MarketData restApi
class RealTime,WebSocket,WebSockex,WSMarketData,WSOrderUpdates,WSPnL websocket
class HTTP,SharedHTTP,Finch http
class Gateway,RestEndpoint,WSEndpoint gateway
Communication Channels
The library provides two independent communication channels:
1. REST API Channel (Request/Response)
Purpose: Synchronous operations like authentication, account queries, order placement, and historical data retrieval.
Flow: ClientPortal Modules → IbkrApi.HTTP → SharedUtils.HTTP → Finch → IBKR Gateway REST API
Key Modules:
IbkrApi.ClientPortal.Auth
- Authentication and session managementIbkrApi.ClientPortal.Portfolio
- Account information, positions, P&LIbkrApi.ClientPortal.Order
- Order placement and managementIbkrApi.ClientPortal.Contract
- Contract search and informationIbkrApi.ClientPortal.MarketData
- Historical market dataIbkrApi.ClientPortal.Trade
- Trade execution history
HTTP Stack:
IbkrApi.HTTP
- Rate-limited HTTP client wrapperIbkrApi.SharedUtils.HTTP
- Core HTTP functionality with FinchFinch
- Elixir HTTP client with connection pooling
2. WebSocket Channel (Real-time Streaming)
Purpose: Real-time data streaming for market data, order updates, and portfolio P&L.
Flow: User Module → IbkrApi.Websocket → WebSockex → IBKR Gateway WebSocket API
Key Features:
IbkrApi.Websocket
- WebSocket client using WebSockex behavior- Real-time market data streaming (Level I quotes)
- Live order status updates and fills
- Portfolio P&L streaming
- Automatic heartbeat management
- Topic-based event dispatch to user callbacks
Core Components
- IbkrApi: The main module that serves as the entry point to the library
- IbkrApi.Application: Manages the application lifecycle and dependencies
- IbkrApi.Config: Handles configuration for the library
- IbkrApi.HTTP: Low-level HTTP client for making requests to the API
Domain-Specific Modules
The IbkrApi.ClientPortal
namespace contains domain-specific modules that correspond to different areas of the IBKR API:
- IbkrApi.ClientPortal.Auth: Authentication and session management
- IbkrApi.ClientPortal.Account: Account information and operations
- IbkrApi.ClientPortal.Contract: Financial instruments and contracts
- IbkrApi.ClientPortal.Order: Order placement and monitoring
- IbkrApi.ClientPortal.Profile: User profile information
- IbkrApi.ClientPortal.Trade: Trade executions and history
Data Flow
A typical request through the library follows this flow:
- Client code calls a function in one of the domain-specific modules
- The domain module constructs the appropriate request parameters
- The request is passed to
IbkrApi.HTTP
which handles the HTTP communication IbkrApi.HTTP
uses Finch to make the actual HTTP request to the IBKR Gateway- The response is received and parsed (typically from JSON to Elixir structs)
- The domain module returns the structured response to the client code
Error Handling
The library uses the ErrorMessage
module for consistent error handling:
defmodule ErrorMessage do
@type t :: %__MODULE__{
code: String.t() | nil,
message: String.t() | nil,
description: String.t() | nil
}
@type t_res :: {:ok, any()} | {:error, t()}
defstruct [:code, :message, :description]
end
All API functions return either {:ok, result}
on success or {:error, %ErrorMessage{}}
on failure, allowing for consistent error handling throughout the application.
Data Structures
The library uses Elixir structs to represent API responses. Each domain module defines its own set of structs that match the structure of the API responses. For example:
defmodule IbkrApi.ClientPortal.Auth.CheckAuthStatusResponse do
defstruct authenticated: nil, competing: nil, connected: nil,
fail: nil, hardware_info: nil, mac: nil, message: nil,
server_info: %IbkrApi.ClientPortal.Auth.ServerInfo{}
end
This approach provides type safety and makes it clear what data is available from each API call.
Configuration
The library is configured through the standard Elixir configuration system:
config :ibkr_api,
base_url: "http://localhost:5000/v1/api",
timeout: 30_000
The IbkrApi.Config
module provides functions to access this configuration:
defmodule IbkrApi.Config do
def base_url, do: Application.get_env(:ibkr_api, :base_url, "http://localhost:5000/v1/api")
def timeout, do: Application.get_env(:ibkr_api, :timeout, 30_000)
end
HTTP Client
The IbkrApi.HTTP
module handles all HTTP communication with the IBKR API. It uses Finch for making HTTP requests and Jason for JSON encoding/decoding:
defmodule IbkrApi.HTTP do
@spec get(String.t()) :: ErrorMessage.t_res()
def get(url) do
# Implementation...
end
@spec post(String.t(), map()) :: ErrorMessage.t_res()
def post(url, body) do
# Implementation...
end
end
Design Decisions
Why Finch?
Finch was chosen as the HTTP client because:
- It's a modern, fast HTTP client for Elixir
- It provides connection pooling for better performance
- It's well-maintained and widely used in the Elixir community
Why Structs for API Responses?
Structs were chosen to represent API responses because:
- They provide a clear structure for the data
- They enable type checking with dialyzer
- They make it clear what data is available from each API call
- They allow for pattern matching in client code
Why the ClientPortal Namespace?
The IbkrApi.ClientPortal
namespace was created to:
- Separate the Client Portal API from potential future APIs
- Group related functionality together
- Allow for easy discovery of available modules
Testing Strategy
The library is designed to be testable at multiple levels:
- Unit Tests: Test individual functions in isolation
- Integration Tests: Test interaction between modules
- API Tests: Test actual communication with the IBKR API (requires a running gateway)
Mock modules can be used to simulate API responses for testing without a live connection to the IBKR gateway.
WebSocket Architecture
The library includes a comprehensive WebSocket implementation for real-time data streaming:
IbkrApi.Websocket Module
The WebSocket implementation uses the WebSockex behavior and provides:
- Connection Management: Automatic SSL configuration for localhost connections
- Message Parsing: Handles IBKR's
<verb>+<arg>+<JSON>
message format - Event Dispatch: Topic-based routing to user-defined
handle_event/2
callbacks - Heartbeat Management: Automatic heartbeat messages every 10 seconds
- Subscription Management: Helper functions for market data, orders, and P&L
WebSocket Message Flow
IBKR Gateway ──WebSocket──> IbkrApi.Websocket ──Events──> User Module
│ │ │
│ │ │
└──── Heartbeat ←────────────┴─── Subscriptions ←──────┘
Supported Data Streams
- Market Data (
smd
): Level I top-of-book data with configurable fields - Order Updates (
sor
): Real-time order status changes and fills - P&L Updates (
spl
): Portfolio profit/loss streaming - System Messages: Connection status and system notifications
Usage Pattern
Users implement the __using__
macro to create WebSocket clients:
defmodule MyTradingBot do
use IbkrApi.Websocket
def handle_event({:market_data, data}, state) do
# Process market data
{:ok, state}
end
end
Future Considerations
Potential future enhancements to the architecture include:
- Rate Limiting: Implementing intelligent rate limiting to avoid API throttling
- Caching: Adding a caching layer for frequently accessed data
- Retry Logic: Implementing automatic retries for transient failures
- Async Operations: Supporting asynchronous operations for long-running requests
- Enhanced WebSocket Features: Multi-account streaming, advanced reconnection strategies