Automate trading strategies with the powerful IB API

October 5, 2024
Facebook logo.
Twitter logo.
LinkedIn logo.
Get this code in Google Colab

Automate trading strategies with the powerful IB API

Trading in 2024 is overwhelming.

Especially when it comes to automating trading strategies.

Many traders rely on manual processes or basic automation, which often fall short by being too slow or not adaptable enough. I’ve been there myself, trying to juggle automation with using my own intuition.

The good news is you can solve this problem by using Python to automate your trading strategies with the Interactive Brokers API (IB API).

I’ve been using Interactive Brokers for more than 10 years. The IB API is infamous because of how complicated it can be to get up and running.

In today’s newsletter, I put it altogether so you can quickly use the IB API to automate your first trading strategy.

Let’s go!

Automate trading strategies with the powerful IB API

Automating trading strategies is a powerful tool for traders seeking efficiency. Interactive Brokers offers a Python API that automates trading strategies.

This API provides access to market data, account information, and order management.

Professionals leverage the API for algorithmic trading, using predefined rules and models for executing trades. The API's comprehensive functionality is great for both novice and experienced traders aiming for precise control over their trading strategies.

Let's see how it works with Python.

Imports and set up

You can download and install the Interactive Brokers API from GitHub. Here's a step-by-step guide. Once the libraries are installed, you can import them.

1import time
2import threading
3from datetime import datetime
4from typing import Dict, Optional
5import pandas as pd
6import warnings
7
8from ibapi.client import EClient
9from ibapi.wrapper import EWrapper
10from ibapi.contract import Contract
11from ibapi.order import Order
12from ibapi.common import BarData

Our strategy will look for breakouts using a popular technical indicator called Donchian Channels.

Here, we define a function to calculate Donchian Channels for given price data over a specified period. It calculates the upper and lower bands and the middle line.

1def donchian_channel(df: pd.DataFrame, period: int = 30) -> pd.DataFrame:
2
3    df["upper"] = df["high"].rolling(window=period).max()
4
5    df["lower"] = df["low"].rolling(window=period).min()
6
7    df["mid"] = (df["upper"] + df["lower"]) / 2
8
9    return df

This function takes a DataFrame containing price data and a period for the calculation. It computes the upper band as the highest high over the period and the lower band as the lowest low. The middle line is calculated as the average of the upper and lower bands.

Create a class to interact with Interactive Brokers API

This section defines a TradingApp class that interacts with the IB API. This class handles connections, data retrieval, and order placement. It’s our trading app.

1class TradingApp(EClient, EWrapper):
2
3    def __init__(self) -> None:
4
5        EClient.__init__(self, self)
6        self.data: Dict[int, pd.DataFrame] = {}
7        self.nextOrderId: Optional[int] = None
8
9    def error(self, reqId: int, errorCode: int, errorString: str, advanced: any) -> None:
10
11        print(f"Error: {reqId}, {errorCode}, {errorString}")
12
13    def nextValidId(self, orderId: int) -> None:
14
15        super().nextValidId(orderId)
16        self.nextOrderId = orderId
17
18    def get_historical_data(self, reqId: int, contract: Contract) -> pd.DataFrame:
19
20        self.data[reqId] = pd.DataFrame(columns=["time", "high", "low", "close"])
21        self.data[reqId].set_index("time", inplace=True)
22        self.reqHistoricalData(
23            reqId=reqId,
24            contract=contract,
25            endDateTime="",
26            durationStr="1 D",
27            barSizeSetting="1 min",
28            whatToShow="MIDPOINT",
29            useRTH=0,
30            formatDate=2,
31            keepUpToDate=False,
32            chartOptions=[],
33        )
34        time.sleep(5)
35        return self.data[reqId]
36
37    def historicalData(self, reqId: int, bar: BarData) -> None:
38
39        df = self.data[reqId]
40
41        df.loc[
42            pd.to_datetime(bar.date, unit="s"), 
43            ["high", "low", "close"]
44        ] = [bar.high, bar.low, bar.close]
45
46        df = df.astype(float)
47
48        self.data[reqId] = df
49
50    @staticmethod
51    def get_contract(symbol: str) -> Contract:
52
53        contract = Contract()
54        contract.symbol = symbol
55        contract.secType = "STK"
56        contract.exchange = "SMART"
57        contract.currency = "USD"
58        return contract
59
60    def place_order(self, contract: Contract, action: str, order_type: str, quantity: int) -> None:
61
62        order = Order()
63        order.action = action
64        order.orderType = order_type
65        order.totalQuantity = quantity
66
67        self.placeOrder(self.nextOrderId, contract, order)
68        self.nextOrderId += 1
69        print("Order placed")

The TradingApp class extends EClient and EWrapper to interact with the IB API.

It initializes the client and wrapper components and sets up data storage. The error method handles API errors, while nextValidId sets the next valid order ID. The get_historical_data method requests historical market data for a given contract, storing it in a DataFrame. The historicalData method processes and stores the received data.

The get_contract method creates a stock contract, and the place_order method places trades using the provided contract, action, order type, and quantity.

Connect the trading app and request data

Once we build our trading app, we will connect to it and look for signals.

1app = TradingApp()
2
3app.connect("127.0.0.1", 7497, clientId=5)
4
5threading.Thread(target=app.run, daemon=True).start()
6
7while True:
8    if isinstance(app.nextOrderId, int):
9        print("connected")
10        break
11    else:
12        print("waiting for connection")
13        time.sleep(1)
14
15
16nvda = TradingApp.get_contract("NVDA")

Once the app is connected, you can download historical data.

data = app.get_historical_data(99, nvda)
data.tail()

The code creates an instance of the TradingApp class and connects it to the IB API using the IP address, port, and client ID. Note the port is the default for the IB paper trading account.

It then starts the app on a separate thread to allow code execution to continue. A loop checks for a successful connection by verifying the nextOrderId.

Once connected, it defines a contract for the stock symbol NVDA and requests historical data for the last trading day using a specified request ID. The Donchian Channels are then calculated for the acquired data.

Implement trading logic based on Donchian Channels

Now, we check for breakouts and places buy or sell orders accordingly.

1period = 30
2
3while True:
4
5    print("Getting data for contract...")
6    data = app.get_historical_data(99, nvda)
7
8    if len(data) < period:
9        print(f"There are only {len(data)} bars of data, skipping...")
10        continue
11
12    print("Computing the Donchian Channel...")
13    donchian = donchian_channel(data, period=period)
14
15    last_price = data.iloc[-1].close
16
17    upper, lower = donchian[["upper", "lower"]].iloc[-1]
18
19    print(f"Check if last price {last_price} is outside the channels {upper} and {lower}")
20
21    if last_price >= upper:
22        print("Breakout detected, going long...")
23        app.place_order(nvda, "BUY", "MKT", 10)
24
25    elif last_price <= lower:
26        print("Breakout detected, going short...")
27        app.place_order(nvda, "SELL", "MKT", 10)

The code sets the period for the Donchian Channels to 30. It enters an infinite loop to request data and check for trading opportunities continuously.

It retrieves historical data for the NVDA contract and skips further processing if there is insufficient data. It then calculates the Donchian Channels and gets the last traded price.

The code compares the last price with the upper and lower channels to detect breakouts. If a breakout to the upside is detected, it places a buy market order for 10 shares. If a breakout to the downside is detected, it places a sell market order for 10 shares.

To exit this loop, stop the Python kernel. Once you’re done, disconnect the app.

1app.disconnect()

Your next steps

This is a simple example of downloading historical market data, looking for signals, and executing trades with Python. It lacks risk and position management, which you should implement based on your experience and risk tolerance. The biggest area for improvement is checking for existing positions before entering new trades.

Finally, make sure to run this code in your paper trading account. Trading it live will expose you to risk of loss.

Man with glasses and a wristwatch, wearing a white shirt, looking thoughtfully at a laptop with a data screen in the background.