TL;DR: Interactive Brokers provides one of the most capable APIs for retail algo traders. This guide walks through connecting Python to IBKR, placing your first paper trade, and building toward a complete automated strategy with ML signals. Every code example runs against the paper trading environment, so there is zero financial risk while you learn.

What You Need Before Starting

Before writing a single line of code, you need five things in place. Missing any one of them will stop you before you start, so work through this checklist first.

1. An Interactive Brokers account. You need either a live account or a paper trading account. If you do not have an IBKR account yet, sign up at interactivebrokers.com. Once your live account is created, IBKR automatically provisions a paper trading account alongside it. The paper account uses virtual money with real market data, which is exactly what you want for development and testing. You do not need to fund the account to use paper trading.

2. Trader Workstation (TWS) or IB Gateway installed and running. TWS is the full desktop trading platform. IB Gateway is a lightweight, headless version designed specifically for API connections. Either one works. For bot development, most people prefer IB Gateway because it uses fewer system resources and does not display a full trading UI. Download either application from your IBKR account management page. You must have TWS or IB Gateway running on your machine (or a server) whenever the bot operates, because it acts as the intermediary between your Python code and IBKR's servers.

3. Python 3.9 or higher. The ib_insync library requires Python 3.9 as a minimum. Check your version by running python --version in a terminal. If you are below 3.9, upgrade before proceeding. Using a virtual environment is strongly recommended to isolate your trading dependencies from other Python projects on your system.

4. The ib_insync library. This is the Python library that handles communication with the IBKR API. It wraps the official TWS API in a more Pythonic, asyncio-compatible interface that is significantly easier to work with than the raw API. Install it with pip:

pip install ib_insync

The ib_insync library depends on the official ibapi package internally, so you do not need to install the TWS API separately. The single pip command handles everything.

5. A stable internet connection. Your Python script communicates with TWS or IB Gateway over a local TCP socket (localhost), but TWS/Gateway itself maintains a persistent connection to IBKR's servers. If your internet drops, TWS disconnects from IBKR, and your bot loses its data feed and order routing. For development and paper trading, a typical home connection is fine. For live trading, consider a VPS or dedicated server with higher uptime guarantees.

Setting Up TWS for API Access

The IBKR API is disabled by default in TWS and IB Gateway. You need to enable it manually before any Python code can connect. This is a one-time configuration step.

Step 1: Open TWS and log in. Launch Trader Workstation and log in with your IBKR credentials. If you are using paper trading, log in with your paper trading username (typically your regular username with "DU" prefix or the paper trading credentials shown in your account management page).

Step 2: Open Global Configuration. In TWS, navigate to Edit > Global Configuration (on Windows/Linux) or TWS > Global Configuration (on macOS). This opens the settings panel.

Step 3: Navigate to API Settings. In the left sidebar of the configuration panel, expand the API section and click Settings. This is where all API-related configuration lives.

Step 4: Enable ActiveX and Socket Clients. Check the box labeled "Enable ActiveX and Socket Clients". This is the master switch that allows external programs (including your Python script) to connect to TWS via TCP socket. Without this checked, all connection attempts will be refused.

Step 5: Set the socket port. The port number tells your Python script where to connect. Use port 7497 for paper trading and port 7496 for live trading. For this tutorial, set it to 7497. Write down which port you chose because you will need it in your Python code.

Step 6: Uncheck Read-Only API. The "Read-Only API" checkbox, when enabled, prevents any external program from placing orders. For a trading bot, you need to uncheck this box. With Read-Only disabled, your Python script can both read market data and submit orders. If you are nervous about this during initial development, you can leave Read-Only enabled while you work on data fetching code, and disable it only when you are ready to test order placement.

Step 7: Add trusted IP addresses (optional but recommended). In the "Trusted IPs" field, add 127.0.0.1. This tells TWS to accept connections from your local machine without showing a confirmation dialog each time. If you skip this step, TWS will pop up a dialog asking you to confirm every time your script connects, which is impractical for automated trading.

Step 8: Click Apply and OK. Save the settings. TWS may prompt you to restart. If it does, restart and log in again. Your API settings will persist across sessions.

If you are using IB Gateway instead of TWS, the API settings are presented during the login process. Select "IB API" as the connection type, set the port to 7497, and confirm. IB Gateway does not have a Read-Only toggle because it is designed exclusively for API access.

Your First Connection — Hello, IBKR

With TWS running and the API enabled, you can now connect from Python. Here is the simplest possible script that connects to IBKR, retrieves your account summary, and disconnects:

from ib_insync import *

# Create an IB instance
ib = IB()

# Connect to TWS/Gateway on localhost, port 7497 (paper), clientId=1
ib.connect('127.0.0.1', 7497, clientId=1)

# Request and print the account summary
print(ib.accountSummary())

# Disconnect cleanly
ib.disconnect()

Let us walk through each line.

from ib_insync import * imports all classes and functions from the library. In production code you would import only what you need, but for learning purposes the wildcard import keeps things readable.

ib = IB() creates the main IB object. This is your interface to everything: market data, orders, account information, and positions. All API calls go through this object.

ib.connect('127.0.0.1', 7497, clientId=1) opens a TCP connection to TWS on your local machine. The first argument is the IP address (127.0.0.1 means localhost). The second argument is the port number, which must match what you configured in TWS. The clientId is an integer that identifies this specific connection. If you run multiple scripts simultaneously, each one needs a unique clientId. For a single script, 1 is fine.

ib.accountSummary() requests a summary of your account from IBKR, including total account value, available funds, buying power, and margin requirements. This is a synchronous call that blocks until the data arrives.

ib.disconnect() closes the connection cleanly. Always disconnect when your script finishes to avoid leaving orphaned connections.

What to expect on success: You should see a list of AccountValue objects printed to the console, showing fields like NetLiquidation, TotalCashValue, BuyingPower, and GrossPositionValue. If you see this output, your connection is working and you are ready to proceed.

Common errors and fixes:

  • "ConnectionRefusedError: [Errno 111] Connection refused" — TWS or IB Gateway is not running, or the API is not enabled. Go back to the TWS setup section and verify that ActiveX and Socket Clients is checked.
  • "ClientId already in use" — Another script or a previous crashed instance is already connected with the same clientId. Either close the other connection or change clientId to a different number (e.g., 2).
  • "Connection dropped after a few seconds" — Check the port number. If you are logged into a paper account but connecting to port 7496 (live), or vice versa, TWS will reject the connection.
  • Timeout with no response — Your firewall may be blocking localhost connections on port 7497. Add an exception for the port or temporarily disable the firewall for testing.

Fetching Market Data

Now that you can connect, the next step is retrieving market data. The ib_insync library uses Contract objects to specify which instrument you want data for. Here is how to fetch historical OHLCV (Open, High, Low, Close, Volume) bars for AAPL:

from ib_insync import *
import pandas as pd

ib = IB()
ib.connect('127.0.0.1', 7497, clientId=1)

# Define the contract for Apple stock
contract = Stock('AAPL', 'SMART', 'USD')

# Request historical bars: 30 days of daily data
bars = ib.reqHistoricalData(
    contract,
    endDateTime='',          # Empty string = current time
    durationStr='30 D',      # 30 days of history
    barSizeSetting='1 day',  # Daily bars
    whatToShow='TRADES',     # Based on trade prices
    useRTH=True              # Regular trading hours only
)

# Convert to a pandas DataFrame
df = util.df(bars)
print(df.head(10))
print(f"\nTotal bars: {len(df)}")

ib.disconnect()

Understanding the Contract object. Every instrument in IBKR is identified by a Contract. The Stock('AAPL', 'SMART', 'USD') call creates a contract for Apple stock, routed through IBKR's SMART routing system, denominated in USD. For other asset types, you would use different contract classes: Forex('EURUSD') for currency pairs, Crypto('BTC', 'PAXOS', 'USD') for crypto, or Future('ES', '202603', 'CME') for futures. The contract tells IBKR exactly which instrument you want data for.

Understanding the parameters. The durationStr parameter specifies how far back to look. Valid values include '30 D' (30 days), '1 W' (1 week), '6 M' (6 months), or '1 Y' (1 year). The barSizeSetting defines the granularity of each bar: '1 day', '1 hour', '5 mins', '1 min', and so on. The whatToShow parameter determines the price type: 'TRADES' for trade prices, 'MIDPOINT' for bid-ask midpoints, or 'BID'/'ASK' for individual sides. Setting useRTH=True restricts data to regular trading hours (9:30 AM to 4:00 PM Eastern for US stocks), filtering out pre-market and after-hours trades.

Working with the DataFrame. The util.df(bars) call converts the list of bar objects into a pandas DataFrame with columns for date, open, high, low, close, volume, average, and barCount. This gives you a standard pandas structure that you can use for analysis, indicator calculations, and visualization. Each row represents one bar (one trading day in this example).

# Example output:
#         date    open    high     low   close   volume  average  barCount
# 0 2026-01-15  237.50  239.80  236.10  238.45  48230150  238.12    312450
# 1 2026-01-16  238.90  241.20  238.00  240.75  52180300  240.10    328700
# ...

For intraday data, reduce the bar size and duration. For example, durationStr='5 D' with barSizeSetting='5 mins' gives you five days of 5-minute bars, which is useful for short-term strategy development.

Placing Your First Paper Trade

With market data flowing, the next milestone is placing an order. Since you are connected to the paper trading port (7497), all orders execute in the simulated environment with virtual money. Nothing here affects real capital.

from ib_insync import *

ib = IB()
ib.connect('127.0.0.1', 7497, clientId=1)

# Define the contract
contract = Stock('AAPL', 'SMART', 'USD')

# Create a market order to buy 10 shares
order = MarketOrder('BUY', 10)

# Submit the order
trade = ib.placeOrder(contract, order)

# Wait briefly for the fill
ib.sleep(2)

# Check the order status
print(f"Order status: {trade.orderStatus.status}")
print(f"Filled: {trade.orderStatus.filled}")
print(f"Average fill price: {trade.orderStatus.avgFillPrice}")

ib.disconnect()

Understanding order types. The MarketOrder('BUY', 10) creates an order to buy 10 shares at the current market price. Market orders fill immediately at the best available price, which makes them the simplest order type but also the least price-controlled. IBKR supports many order types through ib_insync:

# Market order - executes immediately at best available price
order = MarketOrder('BUY', 10)

# Limit order - only fills at $235.00 or better
order = LimitOrder('BUY', 10, 235.00)

# Stop order - becomes a market order when price hits $230.00
order = StopOrder('SELL', 10, 230.00)

# Stop-limit order - becomes a limit order at $229.50 when price hits $230.00
order = StopOrder('SELL', 10, stopPrice=230.00, lmtPrice=229.50)

For a trading bot, limit orders are generally preferred over market orders because they give you price control and avoid unexpected slippage. A market order on a thinly traded stock might fill at a price significantly different from what you expected. Limit orders guarantee your price or better, though they might not fill at all if the market moves away.

Checking order status. After submitting an order, the trade object tracks its lifecycle. The trade.orderStatus.status field shows the current state: Submitted, Filled, Cancelled, or Inactive. In the paper trading environment, market orders typically fill within a second. If you see Filled with a non-zero avgFillPrice, the order executed successfully.

Verifying in TWS. If you have TWS open alongside your script, you can see the order appear in the Orders panel in real time. After it fills, the position shows up in the Portfolio panel. This cross-verification is useful during development to confirm that your code is doing what you expect.

Adding Technical Indicators

Raw price data becomes useful when you derive signals from it. Technical indicators are mathematical transformations of price and volume data that highlight patterns like momentum, trend direction, and overbought or oversold conditions. Here is how to calculate two of the most common indicators — RSI and moving average crossover — using pandas:

import pandas as pd
import numpy as np

def calculate_rsi(df, period=14):
    """Calculate Relative Strength Index."""
    delta = df['close'].diff()
    gain = delta.where(delta > 0, 0.0)
    loss = -delta.where(delta < 0, 0.0)

    avg_gain = gain.rolling(window=period).mean()
    avg_loss = loss.rolling(window=period).mean()

    rs = avg_gain / avg_loss
    rsi = 100 - (100 / (1 + rs))
    return rsi

def calculate_moving_averages(df, fast=10, slow=30):
    """Calculate fast and slow simple moving averages."""
    df['sma_fast'] = df['close'].rolling(window=fast).mean()
    df['sma_slow'] = df['close'].rolling(window=slow).mean()
    return df

# After fetching data into df (from the previous section):
df['rsi'] = calculate_rsi(df)
df = calculate_moving_averages(df)

# Generate signals
df['signal'] = 'HOLD'
df.loc[(df['rsi'] < 30) & (df['sma_fast'] > df['sma_slow']), 'signal'] = 'BUY'
df.loc[(df['rsi'] > 70) & (df['sma_fast'] < df['sma_slow']), 'signal'] = 'SELL'

print(df[['date', 'close', 'rsi', 'sma_fast', 'sma_slow', 'signal']].tail(10))

RSI (Relative Strength Index) measures the speed and magnitude of recent price changes on a scale of 0 to 100. Values below 30 suggest the asset is oversold (potentially undervalued), while values above 70 suggest it is overbought (potentially overvalued). RSI alone is not a reliable trading signal, but combined with other indicators, it adds useful context about momentum.

Moving average crossover compares a fast-moving average (10-day SMA) to a slow-moving average (30-day SMA). When the fast average crosses above the slow average, it suggests upward momentum. When it crosses below, it suggests downward momentum. The crossover identifies trend changes, though with some lag because moving averages are inherently backward-looking.

The signal logic in the example above combines both indicators: a BUY signal fires only when RSI is below 30 (oversold) and the fast MA is above the slow MA (uptrend). A SELL signal fires when RSI is above 70 (overbought) and the fast MA is below the slow MA (downtrend). Requiring agreement between two indicators reduces false signals compared to using either one alone.

These indicators are a starting point. A production-grade trading bot would use dozens of features including MACD, Bollinger Bands, ATR, volume-weighted averages, and cross-asset correlations. The principle is the same: transform raw data into numeric signals, then combine multiple signals to generate higher-confidence trade decisions.

Adding ML Signals to Your Strategy

Technical indicators follow fixed rules: if RSI is below 30, consider buying. Machine learning takes a different approach. Instead of hardcoding rules, you give the model historical features and outcomes, and it learns the rules that best predict future price movements. This section introduces the concept and shows the basic structure.

Feature engineering is the process of turning raw market data into inputs for a model. Good features for trading include: RSI (14-day), MACD histogram, Bollinger Band width, 10-day and 30-day returns, volume relative to its 20-day average, ATR (Average True Range), day of the week, and time since the last earnings report. Each feature captures a different aspect of the market environment.

from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from xgboost import XGBClassifier
from sklearn.model_selection import TimeSeriesSplit
import numpy as np

# Assume df has columns: close, volume, rsi, sma_fast, sma_slow, etc.
# Create the target: 1 if price goes up tomorrow, 0 if it goes down
df['target'] = (df['close'].shift(-1) > df['close']).astype(int)

# Select features
feature_cols = ['rsi', 'sma_fast', 'sma_slow', 'volume']
X = df[feature_cols].dropna()
y = df['target'].loc[X.index]

# Walk-forward validation with TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)

for train_idx, test_idx in tscv.split(X):
    X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
    y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]

    # Train three models
    rf = RandomForestClassifier(n_estimators=100, random_state=42)
    gb = GradientBoostingClassifier(n_estimators=100, random_state=42)
    xgb = XGBClassifier(n_estimators=100, random_state=42, use_label_encoder=False)

    rf.fit(X_train, y_train)
    gb.fit(X_train, y_train)
    xgb.fit(X_train, y_train)

    # Ensemble: average the predicted probabilities
    rf_prob = rf.predict_proba(X_test)[:, 1]
    gb_prob = gb.predict_proba(X_test)[:, 1]
    xgb_prob = xgb.predict_proba(X_test)[:, 1]

    ensemble_confidence = (rf_prob + gb_prob + xgb_prob) / 3
    print(f"Fold avg confidence: {ensemble_confidence.mean():.3f}")

Walk-forward validation is critical for financial data. Unlike random cross-validation used in most ML applications, walk-forward validation respects the time ordering of data. The model is always trained on past data and tested on future data it has never seen. This prevents look-ahead bias, which is one of the most common reasons backtested strategies fail in live trading. The TimeSeriesSplit class from scikit-learn handles this correctly.

The ensemble approach combines predictions from multiple models. In the example above, three models (Random Forest, Gradient Boosting, XGBoost) each produce a probability estimate. Averaging their probabilities produces an ensemble confidence score. When all three models agree with high confidence, the signal is more likely to reflect a genuine pattern rather than noise that a single model latched onto.

For a deeper comparison of how XGBoost and Random Forest perform specifically for stock prediction, including benchmarks on accuracy, overfitting resistance, and training speed, see XGBoost vs Random Forest for Stock Prediction. For a broader look at how ML models fit into a complete automated trading pipeline, see How It Works.

Risk Controls Every Bot Should Have

A trading bot without risk controls is a liability. It does not matter how good your signals are if a single bad trade or a series of losses can blow up your account. At minimum, every trading bot should implement three things: position sizing, stop losses, and a daily loss limit.

Position sizing determines how much capital to allocate per trade. A common rule is to risk no more than 1-2% of your account on any single trade. If your account is $10,000 and your stop loss is 2%, the maximum position size that risks 2% of account value is $10,000:

def calculate_position_size(account_value, risk_per_trade, stop_loss_pct):
    """Calculate max position size based on risk parameters."""
    risk_amount = account_value * risk_per_trade  # e.g., $10,000 * 0.02 = $200
    position_size = risk_amount / stop_loss_pct    # e.g., $200 / 0.02 = $10,000
    return position_size

# Example: $10,000 account, risk 2% per trade, 2% stop loss
max_position = calculate_position_size(10000, 0.02, 0.02)
print(f"Max position size: ${max_position:,.0f}")  # $10,000

Stop losses are automatic exit orders that close a position when it loses a specified percentage. They should be placed immediately when the position is opened, not after the fact:

def place_trade_with_stop(ib, contract, quantity, stop_loss_pct):
    """Place a buy order with an attached stop loss."""
    # Place the entry order
    entry_order = MarketOrder('BUY', quantity)
    entry_trade = ib.placeOrder(contract, entry_order)
    ib.sleep(2)

    fill_price = entry_trade.orderStatus.avgFillPrice
    stop_price = round(fill_price * (1 - stop_loss_pct), 2)

    # Place the stop loss
    stop_order = StopOrder('SELL', quantity, stop_price)
    ib.placeOrder(contract, stop_order)

    print(f"Bought {quantity} at {fill_price}, stop loss at {stop_price}")
    return entry_trade

Daily loss limits track cumulative losses throughout the day and halt trading when a threshold is breached. This prevents a bad day from becoming a catastrophic day:

class DailyLossTracker:
    def __init__(self, max_daily_loss):
        self.max_daily_loss = max_daily_loss
        self.daily_loss = 0.0

    def record_loss(self, amount):
        self.daily_loss += amount

    def can_trade(self):
        return self.daily_loss < self.max_daily_loss

    def reset(self):
        self.daily_loss = 0.0

# Usage
tracker = DailyLossTracker(max_daily_loss=200)  # $200 daily limit
# After a losing trade:
tracker.record_loss(64.00)
# Before opening a new position:
if tracker.can_trade():
    # proceed with trade
    pass
else:
    print("Daily loss limit reached. No new trades today.")

These three controls are the minimum. A production bot should also implement circuit breakers (pause after consecutive losses), maximum position counts, and a kill switch for emergency shutdown. For the full configuration reference and a complete five-layer risk framework, see the risk controls documentation.

From Script to Production Bot

The code examples in this article are individual scripts. A production trading bot needs to run continuously, handle errors gracefully, and recover from disconnections without human intervention. This is where a simple script becomes a genuine system, and the engineering complexity increases significantly.

Running 24/7. A trading bot needs to be running whenever markets are open. For US stocks, that means 6.5 hours per day. For forex and crypto, it means around the clock. This typically requires a VPS or dedicated server rather than your personal laptop. The bot should start automatically on boot and restart on crash.

Handling disconnections. IBKR's API connection can drop for many reasons: internet outages, TWS daily restarts (TWS restarts automatically at a configurable time each day), or server-side maintenance. A production bot must detect disconnections, wait, and reconnect automatically. It also needs to reconcile its internal state with IBKR's actual state after reconnecting, because orders might have filled or positions might have changed while it was disconnected.

Logging. Every decision the bot makes should be logged with a timestamp: signals generated, orders placed, fills received, risk checks passed or failed, errors encountered. Logs are your primary debugging tool when something goes wrong. They should be written to files (not just the console) and rotated so they do not consume unlimited disk space.

Monitoring. Beyond logging, you need a way to check on the bot's status without reading log files. A web dashboard showing open positions, daily P&L, recent signals, and system health is the standard approach. Alerting (email or push notification) for critical events like the kill switch triggering or a disconnection lasting more than a few minutes is also valuable.

Building all of this from scratch is a substantial engineering project. Connection management, state reconciliation, error recovery, logging infrastructure, and monitoring are each non-trivial problems that take weeks to implement and months to harden. This is exactly the problem that slmaj solves out of the box. It handles the connection lifecycle, reconnection logic, state management, logging, web dashboard, and risk controls, so you get a production-grade bot without building the infrastructure yourself. See Features for the full list of what is included, or Pricing to compare tiers.

Frequently Asked Questions

Can you use Python with Interactive Brokers?

Yes. Interactive Brokers provides an official API called the TWS API that supports Python natively. The most popular Python wrapper is ib_insync, which provides a clean, synchronous interface on top of the official async API. You can use it to retrieve market data, place orders, manage positions, and access account information programmatically. IBKR also supports Java, C++, C#, and several other languages, but Python is the most popular choice for algorithmic trading due to its ecosystem of data analysis libraries (pandas, numpy, scikit-learn).

What port does Interactive Brokers API use?

The default ports are 7496 for live trading and 7497 for paper trading. These ports are configurable in TWS under Edit > Global Configuration > API > Settings. If you change the port in TWS, you must use the same port number in your Python ib.connect() call. IB Gateway uses the same default ports. It is a common mistake to connect to the wrong port (live instead of paper or vice versa), which is why most tutorials explicitly state the port in each code example.

Is Interactive Brokers good for algorithmic trading?

Interactive Brokers is one of the best brokerages for algorithmic trading among retail and small institutional traders. Its strengths include a comprehensive API with broad language support, access to 150+ markets in 34 countries, competitive commissions (often $0 for US stocks on IBKR Lite, or $0.005/share on IBKR Pro), real-time market data, margin accounts with reasonable rates, and support for stocks, options, futures, forex, bonds, and crypto. The main downsides are the learning curve of the API (which libraries like ib_insync mitigate) and the requirement to run TWS or IB Gateway as a persistent application. For serious algo trading, IBKR is the default choice for a reason.

How much does it cost to use IBKR API?

The API itself is free to use. There is no separate charge for API access. You pay the same commissions and fees as any other IBKR customer: $0 per trade for US stocks on IBKR Lite, or $0.005 per share (minimum $1.00) on IBKR Pro. Market data subscriptions are an additional cost if you need real-time data beyond the free delayed quotes. US stocks Level 1 data costs approximately $1.50/month. Paper trading does not require any paid data subscriptions. There is no monthly platform fee, no minimum account balance requirement to use the API, and no per-request charges.

Skip the Setup — Start Trading Today

slmaj handles connection management, risk controls, ML signals, and monitoring out of the box. Paper trade in minutes, go live when ready.