ib_insync Guide – Interactive Brokers API

21 min read

Get 10-day Free Algo Trading Course

Loading

Last Updated on January 17, 2021

Table of Contents

  1. What is ib_insync?
  2. Why should I use ib_insync?
  3. Why shouldn’t I use ib_insync?
  4. What is the Interactive Brokers API?
  5. How to sign up for a demo account for Interactive Brokers and set up the trading platform?
  6. How do I get started with ib_insync?
  7. How to retrieve the current price of a stock using ib_insync?
  8. How to retrieve market data for cryptos, currencies and precious metals using ib_insync?
  9. How can I retrieve historical data from ib_insync?
  10. How can I fire an order through ib_insync?
  11. How to attach a take profit and stop loss to orders using ib_insync?
  12. How to fire an order for Mastercard when Visa hits a certain price using ib_insync?
  13. How to fire an order for Mastercard when Visa moves more than 5% in the last 5 minutes using ib_insync?
  14. How can I buy call and put options using ib_insync?
  15. How to deploy a full trading strategy using ib_insync?
  16. What is asynchronous programming and how does it differ from other programming methods
  17. Download the full codebase

ib_insync docs page

What is ib_insync?

ib_insync is a framework that simplifies the Interactive Brokers (IB) Native Python API that was developed by the IB team.

Why should I use ib_insync?

The following snippet, from the ib_insync documentation, sums it up nicely-

“The goal of the IB-insync library is to make working with the Trader Workstation API from Interactive Brokers as easy as possible.”

The Native Python API has a bit of a reputation for being complicated. It doesn’t follow the same style that most Python programmers have become accustomed to and the learning curve is steep.

ib-insync offers a more familiar environment for Python programmers. It also simplifies other aspects like installation and has an active support group. Further, it has extensive documentation and is well-maintained.

ib_insync simplifies the methods and syntax used in the Interactive Brokers Native Python API. Also, and this is an important one, ib_insync uses asynchronous execution.

We will discuss asynchronous programming in a bit more detail later on in this guide. But in short, this manages the communication received from the API, ensuring that we wait for data when it’s urgent and don’t sit idle when it’s not.

Why shouldn’t I use ib_insync?

The asynchronous methods in ib_insync in some cases can make it difficult to implement further speed optimization.

Since ib_insync already uses an event loop, adding additional multiprocessing or other external asynchronous methods can be a challenge.

For example, let’s say you want to use alternative data in your strategy, obtained from an external source. With the native IB API, you can simply launch a new thread and quickly make asynchronous calls to the external alternative data API.

With ib_insync, you’ll have to ensure that your additional custom functions don’t interfere with ib_insync’s event loop.

In other words, if your strategy is extremely time sensitive (we are talking milliseconds), then it might be easier to implement speed improvements using the native API compared to ib_insync. Unless you are an advanced user with an in-depth understanding of asynchronous frameworks.

Note that ib_insync was not created by the IB team.

What is the Interactive Brokers API?

ib_insync is built on top of the IB Native API.

The IB API is an interface that allows traders to trade algorithmically with Interactive Brokers. Almost all of the things that can be done in the client can be done through the API.

This means you can execute trades, get both live and historical data, and check on your account details, all programmatically.

One thing to note about the Interactive Brokers API is that it is not a REST API. That means you’re not directly connecting to IB’s server via your code. Rather, all of the interaction takes place via one of their client apps. 

How to sign up for a demo account for Interactive Brokers and set up the trading platform?

There are two ways to get a demo account with Interactive Brokers. If you already have a live account, a demo will have automatically been created for you. Simply use the same user name and password that was created for your live account.

The benefit of being an existing client is that you’ll have access to live data and any data packages that you’ve subscribed to.

If you’re not already a client, here is a demo account creation guide. The process is quick, you just need to download the software and use your email to sign in.

The experience of a demo account will be similar to that of a live environment. The difference is that data is delayed by 10 to 15 minutes.

That means you’ll have access to data that you would otherwise need to pay for on a live account, albeit delayed.

How do I get started with ib_insync?

The easiest way to install ib_insync is by using pip.

pip install ib_insync

You should also have a client installed. You can use either TWS or IB Gateway. Make sure that the API is enabled within the client before attempting to make a connection.

IB-insync works in both interactive environments and script mode. We will use Jupyter notebooks for the examples in this guide.

from ib_insync import *

util.startLoop()  # only use in interactive environments (i.e. Jupyter Notebooks)

We start by importing the library. The second line in the code snippet above is specific for interactive environments like Jupyter environments. It’s not needed otherwise and will throw an error if you try to use it in script mode.

ib = IB()
ib.connect()

Next, we will instantiate the IB class and establish a connection. When running the cell, we get a message showing we are connected.

In this case, we just use the connect() function without passing through any parameters as we are configured to use the defaults.

But if you need to change the port number or client id, you can do so by passing in the appropriate parameters in the connect function as follows:

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

How to retrieve the current price of a stock using ib_insync?

Whenever data is required for any asset, we start by creating a “contract”. This is to specify some details about the asset we want data for.

The ib_insync has helper functions and classes to simplify contract creation for most of the common asset types.

nflx_contract = Stock('NFLX', 'SMART', 'USD')

In the above code example, we use the Stock() helper Class to create a contract. We’ve passed through three values here:

  1. The ticker – NFLX in this case
  2. The exchange it trades on. By specifying “SMART” here, IB will automatically route this to the appropriate exchange.
  3. The currency the stock trades in which is US dollars.

There is still some further information IB will need. We can use the qualify contracts function to automatically fill in this additional information.

ib.qualifyContracts(nflx_contract)

You should get an output that looks like this.

As per the image, it has determined the conId, primaryExchange, and tradingClass automatically.

Next, we can request a tick data stream as follows.

data = ib.reqMktData(nflx_contract)

Now that a tick stream has been created, we can view the current price by calling the marketPrice function which show the latest price in our Jupyter Notebook.

data.marketPrice()

How to retrieve market data for cryptos, currencies and precious metals using ib_insync?

The process to retrieve data for other assets is similar to the last example. The only difference is that we instantiate different classes to create a contract.

For currencies, we can create a contract by using the Forex() class. here is an example:

eur_usd_contract = Forex('EURUSD', 'IDEALPRO')
ib.qualifyContracts(eur_usd_contract)

For cryptocurrencies, we can use the futures class to create a contract for Bitcoin futures. Futures are a little bit different in that we have to specify an expiry date as well as the exchange.

btc_fut_contract = Future('BRR', '20201224', 'CMECRYPTO')
ib.qualifyContracts(btc_fut_contract)

This information can be found within the TWS platform. If you have the asset on your watchlist, right-click on it and choose description to pull up the required details.

Alternatively, we can use the continuous contract helper function without having to specify an expiry date, if you’re looking for the front-month contract.

btc_fut_cont_contract = ContFuture('BRR', 'CMECRYPTO')
ib.qualifyContracts(btc_fut_cont_contract)

Precious metals can be traded on various markets and are often traded as Futures. But since we haven’t covered a CFD example, this is how you would create a CFD contract for gold:

gold_cfd_contract = CFD('XAUUSD')
ib.qualifyContracts(gold_cfd_contract)

After creating a contract, the rest of the code remains the same as the last example to request the latest price. Simply swap out the old contract with the newly created one.

How can I retrieve historical data from ib_insync?

Retrieving historical data is similar to the example where we requested normal market data. We start with creating a contract, and then send a command to get historical bar data.

We will work off our existing NFLX contract and use the reqHistoricalData function to request data.

historical_data_nflx = ib.reqHistoricalData(
    nflx_contract, 
    '', 
    barSizeSetting='15 mins', 
    durationStr='2 D', 
    whatToShow='MIDPOINT', 
    useRTH=True
    )

Here are the parameters that we’ve passed through:

  1. Contract – this is the same contract we created for NFLX in our earlier example. Any asset/contract can be used here, as long as you have a data subscription for it.
  2. End time – we’ve left it blank here so that it will pull data up to the most recent candle. But you can input a specific date/time if desired.
  3. Bar size – The size of the candle. You can go as low as 1 second and as high as 1 month. The API requires this text to match exactly. If you forgot the ‘s’ and entered ’15 min’ it wouldn’t work. Fortunately, the library will print out the valid bar sizes if you make a mistake.
  4. Duration string – this is how far back we want data for. In this case, we requested two days of data.
  5. What to show – We requested the midpoint which returns the mid between the bid and ask price. Other options are BID, ASK, or TRADES.
  6. Use RTH – This stands for Use Regular Trading Hours. We’ve set it to true which means it won’t show pre or post-market data.

We should now have 15-minute bars for NFLX.

Let’s check to see what kind of format our data was returned in:

type(historical_data_nflx)

We can see that it is a custom object called BarDataList.

From here, we can access the most recent candle, just like any other Python list, by using a negative index.

historical_data_nflx[-1]

Since this is a list of objects, we can easily access parts of the data. If we want to know just the open price of the last candle as an example, we can access it as follows.

historical_data_nflx[-1].open

There is built-in support for Pandas DataFrames in ib_insync. We can create a DataFrame from our candle data like this:

util.df(historical_data_nflx)

How can I calculate indicators such as a simple moving average?

A big advantage of using Pandas DataFrames is that it makes it easy to calculate indicators like a simple moving average.

Building on our earlier example, let’s first save our NFLX DataFrame to a variable:

nflx_df = util.df(historical_data_nflx)

We can then use the rolling() and mean() functions from Pandas to calculate a moving average.

nflx_df.close.rolling(20).mean()

We’ve passed the number 20 into the rolling function. This means Pandas will calculate the mean, or average, on a rolling window of 20 data points.

We now have a Pandas Series that contains the 20-period simple moving average data for our NFLX candles.

There are a few things that can be done from here. We can attach this to our original DataFrame for further calculations as follows.

nflx_df['20SMA'] = nflx_df.close.rolling(20).mean()

This will create a new column called 20SMA that will store the appropriate moving average data.

Alternatively, if you only need the very last value for the 20SMA, we can access it as follows:

nflx_df.close.rolling(20).mean().iloc[-1]

If you plan to use a lot of indicators it makes sense to use a third-party library. There are several out there, TA-LIB is popular. Btalib is also a good choice as it works off of Pandas and is much more lightweight.

Here is an example of calculating Bollinger bands using the BTA library with the default settings.

from btalib import bbands

bbands(nflx_df).df

if you’ve never used btalib before, it can be installed via pip.

pip install bta-lib

How can I fire an order through ib_insync?

There are three things involved in firing an order. Creating a contract, an order, and sending a request to the IB API.

We will continue working with the NFLX contract created earlier.

The ib_insync library has a few predefined classes to help with creating various order types. Specifically, these are the order types you can choose from:

  1. MarketOrder
  2. LimitOrder
  3. StopOrder
  4. StopLimitOrder

We will go through an example of a market order. We start by creating an order object.

nflx_order = MarketOrder('BUY', 200)

Here we’ve specified that we want to buy 200 shares.

Next, we use the placeOrder function to submit a request to the IB API for the order. We will need to pass two things through: the order object we just created and the contract object we created earlier on.

trade = ib.placeOrder(nflx_contract, nflx_order)

Note that we are saving the response to a variable called trade. We can access the log attribute of this variable to view useful information regarding the order status.

trade.log

The trade variable will also have an orderStatus attribute. We can check if the order is filled as follows:

trade.orderStatus.status

This should read ‘Filled‘ if the order has been filled.

One thing to keep in mind is that ib_insync will automatically and continuously update the trade variable we used in the above example with any order status message it might receive from the API.

This is very much different from using a REST API where it’s common practice to make another call to the API to verify the order status.

With this in mind, there a few different ways we can manage our order status.

If we were to run all these cells at the same time it will likely show that the order is in PendingSubmit status.

This is because there hasn’t been enough time for the order to reach the API, and then the exchange, and then fill.

If you’re following along in a jupyter notebook, you might not notice this as you run each cell with time in between. However, in script mode, it will run too fast for any order status messages to get processed other than than the first update which is PendingSubmit.

We could build a small loop that checks the order status and pauses the script as follows:

for _ in range(100):
    if not trade.isActive():
        print(f'Your order status - {trade.orderStatus.status}')
        break
    time.sleep(0.5)
else:
    print('Order is still active')

The above code will check if the trade is no longer active. If so, it will break out of the loop and exit. Otherwise, it will sleep for half a second and check again.

If the order remains active after 100 attempts (or 500 seconds) it will notify us that the trade is still active and that something is likely wrong considering this is a market order.

While a small loop like the one above will allow you to manage your order, it is not the most effective way to do things.

Keep in mind that ib_insync is a asynchronous library. That means it can process things in the background while we continue with other tasks.

So unless you have a need to pause the script, you would be better off creating a callback function. We will cover this is more detail in the next section.

The trade variable has several other attributes that can be useful. If you’re following along in Jupyter notebooks, try typing in trade. and hit tab. This will auto suggest all the attributes available that you can try out.

How to create a callback function for order status?

If you don’t want to wait on an order status, you can create a callback function. This will run a block of code whenever the order status is updated, allowing you to continue with other tasks in the mean time.

We’ll go through the same scenario as above and use a callback function instead. Let’s start by creating a function that will contain the code to execute once the trade is filled.

def order_status(trade):
    if trade.orderStatus.status == 'Filled':
        fill = trade.fills[-1]
        
        print(f'{fill.time} - {fill.execution.side} {fill.contract.symbol} {fill.execution.shares} @ {fill.execution.avgPrice}')

The above code will get called once an order is filled. When this function is called by the library, it will pass through a trade object that contains details about our order. This is very similar to the trade variable we created earlier on.

The function will print out the following

  • The time the trade was executed
  • The execution side ( bought or sold)
  • Contract symbol or otherwise known as the ticker symbol
  • How many shares were traded
  • The average price the trades were executed

Now all we need to do is attach this callback function to our order. Here is an example.

nflx_order = MarketOrder('BUY', 100)
trade = ib.placeOrder(nflx_contract, nflx_order)

trade.filledEvent+= order_status

The first two lines are unchanged from our earlier example. All we’ve done here is added a third line which points to the function we created whenever there is a filled event.

The output should look something like this:

2020-12-23 10:16:27 - BOT NFLX 100.0 @ 523.0

The ib_insync library allows callback functions for various tasks like streaming data, or scanner updates. For the order status, there are two main events that you can utilize.

The filled event, which is what we used in our example, and the fill event. The former only gets called when an order gets filled. The latter will receive every event.

Typically, an order will get 4 updates before it is filled. In most cases, you may only be interested in the very last update which will let you know if the order is filled or not.

How to attach a take profit and stop loss to orders using ib_insync?

If you’ve used the native IB API, you’ll know that stop loss and take profits are created by making new order objects and then attaching them as “child” orders to the main “parent” order.

Fortunately, ib_insync has a helper function that does most of this leg work for you. We will use the previously created EURUSD contract to enter a trade with a specified stop loss and take profit.

eurusd_bracket_order = ib.bracketOrder(
    'BUY',
    100000,
    limitPrice=1.19,
    takeProfitPrice=1.20,
    stopLossPrice=1.18
)

The code above creates a bracket order. The main order is to buy 1 lot of EURUSD at 1.19. The stop loss is set at 1.18 and the take profit at 1.20.

The reason why we started with a mention of how the native IB API does this is because the part that comes next may not seem logical otherwise.

What ib_insync has done here is created three different orders which are now stored in our bracket_order variable. Therefore, we need to place each of these three orders individually.

We can iterate through the bracket_order object that we’ve created to do this.

for ord in eurusd_bracket_order:
    ib.placeOrder(eur_usd_contract, ord)

Note that we also passed through the EURUSD contract object that we created before.

Lastly, we specified the names of a few parameters in our function like stopLossPrice. It’s not necessary although it makes the code more readable and easier to understand. Nevertheless, the following snippet would have served to accomplish the same thing:

eurusd_bracket_order = ib.bracketOrder('BUY', 100000, 1.19, 1.20, 1.18)

How to fire an order for Mastercard when Visa hits a certain price using ib_insync?

Normally in this type of scenario, we might think of monitoring Visa’s price via a live tick stream and then using an IF statement to fire an order on Mastercard when our price is met.

However, Interactive Brokers has an order type for this and can manage it on the server end.

We will start by creating a contract for Visa:

visa_contract = Stock('V', 'SMART', 'USD')
ib.qualifyContracts(visa_contract)

Next, we will need to create what’s called a price condition. This will eventually merge with our order to specify an additional condition that the order should not be executed until Visa hits a certain price.

price_condition = PriceCondition(
    price=200,
    conId=visa_contract.conId,
    exch=visa_contract.exchange
    )

These are the parameters we passed through in the code above:

  1. Price – this is the price Visa needs to hit to trigger the entry order for Mastercard
  2. ConId – this is the conId for Visa. This gets automatically populated when we qualified the visa contract.
  3. Exchange – the exchange Visa trades on. Again, we can just grab this info from our visa contract and pass it on.

Now we will create our main order. This will be for Mastercard, so let’s create a contract for MA first.

ma_contract = Stock('MA', 'SMART', 'USD')
ib.qualifyContracts(ma_contract)

With our contract created, let’s create a market order. This is no different than the earlier market order example.

visa_ma_order = MarketOrder('BUY', 100)

The last thing we need to do here is append our price condition into the market order.

visa_ma_order.conditions.append(price_condition)

And now we are ready to place an order.

visa_ma_trade = ib.placeOrder(ma_contract, visa_ma_order)

Note that we are only passing through the Mastercard contract when placing the order since that is the stock we are looking to buy.

The Visa contract was only created to get the attributes needed to create a price condition.

Once we submit the order, it will sit on the IB server. That means that even if you shut down your script, the order will get triggered once the price condition is met.

How to fire an order for Mastercard when Visa moves more than 5% in the last 5 minutes using ib_insync?

IB has conditional orders that can be triggered based on daily percentage changes. Similar to the last example, we could create a condition and it would be handled on the server end.

However, this example is a bit different. We want to see what the percentage change has been over the past five minutes.

IB doesn’t have this type of functionality built-in, but we can easily code a solution.

Since this is a full strategy, we will code it in script mode rather than interactive mode.

Our approach to this will be to start with a request for tick data which we can then store in a Pandas DataFrame. Then we can filter out just the last five minutes’ worth of data. From there, if the price has moved more than 5% in the last five minutes, we can execute a trade.

We start the script like we normally would with our imports and connecting to the API. One addition here is that we’ve imported the Pandas library.

import pandas as pd
from ib_insync import *

ib = IB()
ib.connect()

Next, we are going to write a function that gets executed every time new data comes in.

def new_data(tickers):
	''' process incoming data and check for trade entry '''
	for ticker in tickers:
		df.loc[ticker.time] = ticker.last

In this first part, we are iterating over the incoming data and saving it to a Pandas DataFrame. This will save the time and date as the index and the last price as a row item.

	five_mins_ago = df.last[-1] - pd.Timedelta(minutes=5)

Continuing with our function, we will store the time stamp from five minutes ago to a variable. We do this by taking the very last index value and subtracting 5 minutes from it, using the TimeDelta function from Pandas.

Before we look for trades, we need to make sure that we have at least five minutes worth of data. The following IF statement will check if that is the case.

	if df.index[0] < five_mins_ago:
		df = df[five_mins_ago:]

If we have five minutes of data, we can slice off the last five minutes worth of data as that’s all we are interested in. This also helps to keep our DataFrame from getting too large and unnecessarily consuming memory.

We then check for the highest and lowest price during that time.

		price_min = df.last.min()
		price_max = df.last.max()

		if df.last[-1] > price_min * 1.05:
			place_order('BUY')

		elif df[-1] < price_max * 0.95:
			place_order('SELL')

If the current price is more than 5% higher than the lowest price over the last 5 minutes, we will execute a buy order. Otherwise, if the current price is more than 5% lower than the highest price in the past 5 minutes, we will send a sell order.

That completes our new_data function which will serve as a callback function for every time new data comes in.

Next, we can program our trade entry.

def place_order(direction):
	''' place order with IB - exit script if order gets filled '''
	mastercard_order = MarketOrder(direction, 100)
	trade = ib.placeOrder(mastercard_contract, mastercard_order)
	ib.sleep(3)
	if trade.orderStatus.status == 'Filled':
		ib.disconnect()
		quit(0)

This function simply places an order and then exits the program after confirmation that the order has filled

You may have noticed that we are storing data in a DataFrame that has not been defined yet. Let’s do that now.

#init dataframe
df = pd.DataFrame(columns=['date', 'last'])
df.set_index('date', inplace=True)

This creates a blank dataframe with the columns date and last (for last price).

We also need to create two contracts. One for Mastercard, and one for Visa. The Mastercard contract is being used in our place_order function that we just created.

# Create contracts
mastercard_contract = Stock('MA', 'SMART', 'USD')
visa_contract = Stock('V', 'SMART', 'USD')

ib.qualifyContracts(mastercard_contract)
ib.qualifyContracts(visa_contract)

We will use the Visa contract to request data with as that is the stock that we will be monitoring for our trade entry.

# Request market data for Visa
ib.reqMktData(visa_contract)

Lastly, we need to let the ib_insync library know that the new_data function should be called every time new data is received.

# Set callback function for tick data
ib.pendingTickersEvent += new_data

Note that our entire trade logic takes place inside the callback function. That means we don’t have anything running within our main script.

The last thing we need to do is call ib.run(). This will start and run the event loop (event loops are how asynchronous code is run).

# Run infinitely
ib.run()

How can I buy call and put options using ib_insync?

Trading options is very similar to other order types. We start with creating a contract, then specify our order parameters, and lastly submit a request to the API.

We can create the options contract as follows:

msft_option_contract = Option('MSFT', '20201218', 215, 'C', 'SMART')

The parameters we’ve passed through here are:

  1. The ticker symbol of the asset
  2. The option contract expiry date
  3. the strike price (as an integer)
  4. ‘C’ for Call or ‘P’ for Put
  5. The exchange. Most of the time we can use “SMART” to take advantage of IB’s automatic routing system

After creating the contract, we can qualify it to fill in any missing details like the conId.

ib.qualifyContracts(msft_option_contract)

Next, we create an order. This where we can specify if we are buying or selling as well as the quantity.

msft_option_order = MarketOrder('BUY', 1)

And lastly, we submit a request to the IB API to place the order by passing on the options contract and order object we just created.

msft_option_trade = ib.placeOrder(msft_option_contract, msft_option_order)

How to deploy a full trading strategy using ib_insync?

We are going to go through a full trading example using the ib_insync library.

The strategy trades lean hog futures and will sell on a three standard deviation rally and buy on a three standard deviation drop.

This will be written in script mode rather than Jupyter Notebooks.

To start, we will import the ib_insync library and establish a connection to the IB API.

from ib_insync import *

ib = IB()
ib.connect()

Next, we will write a few functions. The first one will simplify placing an order.

def place_order(direction, qty, df, tp, sl):
    bracket_order = ib.bracketOrder(
        direction,
        qty,
        limitPrice=df.close.iloc[-1],
        takeProfitPrice=tp,
        stopLossPrice=sl,
    )

    for ord in bracket_order:
        ib.placeOrder(contract, ord)

The code snippet above will allow us to place an order. We will need to pass through the direction which will either be buy or sell. We will also enter the desired quantity, take profit and stop loss. Lastly, we will pass through a df (DataFrame) here from which we can extract the latest price.

The second function is a callback function. This will get run every time new bar data is received from Interactive Brokers. The bulk of our trade logic will go here.

This way, we can check for a trade everytime new data comes in.

def on_new_bar(bars: BarDataList, has_new_bar: bool):
    if has_new_bar:
        df = util.df(data)
        sma = df.close.tail(50).mean()
        std_dev = df.close.tail(50).std() * 3

        # Check if we are in a trade
        if contract not in [i.contract for i in ib.positions()]:
            # We are not in a trade - Look for a signal

            if df.close.iloc[-1] > sma + std_dev:
                # Trading more than 3 standard deviations above average - SELL
                place_order('SELL', 1, df, sma, sma + std_dev * 2)

            elif df.close.iloc[-1] < sma - std_dev:
                # Trading more than 3 standard deviations below average - BUY
                place_order('BUY', 1, df, sma, sma=std_dev * 2)

The ib_insync data update will pass through two things into this function. A BarDataList object that contains our bars, and a boolean value to let us know if there is new data or not.

We start by checking the boolean value to see if there is new data. If there isn’t, there is no need to check for a new trade.

If we’ve received new data, we will start by converting the data to a Pandas DataFrame.

Then, we will do a simple moving average calculation, similar to earlier example in the section that discussed technical indicators.

After that, we can calculate a standard deviation by using the built-in std() function from Pandas. Since we want to make trades based on 3 standard deviations, we multiply this figure by 3.

Now that our calculations are done, it’s time to check if we are already in a trade. We only want to take one trade at a time.

We do this by making a call to the positions endpoint and checking if there is already an open position with the same contract we are trading. We haven’t made the contract that we will be trading yet, but will get to it soon.

If we are not already in a position, we can check to see if the latest price is greater than the simple moving average plus three standard deviations. We get the latest price by checking the last line of the DataFrame we created.

If the price is higher, than we have a sell signal and can call the place_order function that we created to submit a trade request.

The inverse is true for buy orders. We are looking for a decline below the sma minus three standard deviations.

When we call place_order, note that we are passing through the SMA plus the three standard deviation times two. That means we are using a six standard deviation stop loss. This will give us a 1:1 risk to reward ratio.

That’s it for our functions, and that covers a bulk of our trade logic.

We can now create a contract for the asset we are trading.

# Create Contract
contract = ContFuture('HE', 'GLOBEX')
ib.qualifyContracts(contract)

The above code create a continuous contract, this way we don’t have to worry about the expiry date and will always trade the front month contract which is the most liquid.

We can request streaming bar data.

# Request Streaming bars
data = ib.reqHistoricalData(
    contract,
    endDateTime='',
    durationStr='2 D',
    barSizeSetting='15 mins',
    whatToShow='MIDPOINT',
    useRTH=True,
    keepUpToDate=True,
)

Note the keepUpToDate=True. This keeps the data stream alive and tells the IB API to keep pushing new bar data out automatically.

Remember the earlier callback function we created? We have to let ib_insync know to call that function everytime new data is received.

# Set callback function for streaming bars
data.updateEvent += on_new_bar

And that mostly covers it. The last thing we need to do is execute the code which we can do as follows:

# Run infinitely
ib.run()

What is asynchronous programming and how does it differ from other programming methods?

As discussed earlier, one of the biggest advantages of using ib_insync is that it offers asynchronous programming.

Asynchronous methods help to reduce delays in I/O operations. Common I/O operations are writing information to a hard drive or requesting information from a third-party application or API.

As an example, let’s say your request some historical data from the IB API. Typically, you would send out a request and then wait perhaps 500 milliseconds for the API to return a response.

That doesn’t sound like a big deal. But what if you need historical data for 50 instruments? Now you’re waiting 25 seconds to collect all the necessary data. That’s probably a bit too long for most trading systems.

This is because in a normal, or otherwise known as synchronous programming, Python will wait for one task to complete before starting another.

Synchronous Programming

Notice in the image above that there is a lot of idle time, where the script is just “waiting” for a response.

Asynchronous methods differ in that they can continue performing other tasks during idle times. So with an asynchronous framework, additional requests can be sent before the first response comes in to minimize the wait time.

Asynchronous Programming

This means you could probably get historical data for all 50 instruments in maybe 1 second rather than 25 by using asynchronous methods.

One thing to keep in mind is that with asynchronous programming, there is a queue of tasks that need to be completed. 

But these will only get executed when there is idle time. That means if your script is constantly busy doing calculations, some of these tasks won’t get completed.

As an example, let’s say you request tick data from Interactive Brokers. The incoming data will be received in the background.

But if your script is busy calculating a complex indicator that takes say 20 seconds, then there is no way for that data to come in as the script is not available to process incoming data.

This is why it’s important to insert a “pause” during any CPU intensive parts of the script. This can be done by using ib.sleep() which will provide background tasks an opportunity to complete.

Note that ib.sleep() is not the same as the sleep function from the time module. Using the sleep function from the time module will not clear the way for background tasks and hence should not be used with the ib_insync framework.

Download the full codebase

All the code used in this article can be found this GitHub repository.

Jignesh Davda