Interactive Brokers Price Monitor.
How to setup an asynchronous data monitor to check alert prices and send alerts when triggered
This is the next installment in my series of how to do pretty trivial things with python that may ultimately save you some time.
In my last article (my magnum opus!) I shared an example notebook where we read alerts from a csv file and sent any new ones to our phone via SMS. This was a pretty easy task, namely because we were simply inheriting some data, cleaning it, doing a few checks for repetition and shipping it out the door to our phone. The key thing being that we were not checking any real time price data. We didn’t need to subscribe to any API or do any checks for incoming prices.
Today I intend to raise the difficulty and hopefully, the utility by incorporating an Interactive Brokers API connection to monitor prices for a set of candidates and prices passed in from a spreadsheet. This time, we will inherit some Symbol and trigger prices, subscribe to data for those symbols and send an alert if a price crosses our triggers. This will ensure that we can make sick coin daily while sitting on a beach all day, every day…
Workflow
Once again we need to start by reading in some data. For todays example I have included a sample spreadsheet with some made up strategy candidates and pertinent information.
With our sweet alpha filled list of symbols, connect to Interactive Brokers API and create some contract objects for each candidate to subscribe to live market data.
Monitor incoming prices and check if any have exceeded their respective trigger levels.
If yes, send an SMS alert message with the important details.
Sounds simple right?
Lets break it down…
The Deets
First, you will need two python packages installed to make this work so get your pips installed. I use asyncio
for our asynchronous functions as well as ib_insync
for our IB API client.
Get some data. Nothing new here. You should be familiar with Panda’s
read_csv
function by now. We are reading the included CSV and passing it to a Data Frame.
def get_alert_df(self):
'''
read csv and return dataframe
'''
alert_df = pd.read_csv("candidate_example.csv")
return alert_df
Don’t worry, it only get’s harder from here…
Connect to Interactive Brokers API and create some contract objects for each candidate to subscribe to live market data.
Setting up a connection with IB is a bit of a pain if you’ve never done it, but it is well worth the effort. They don’t charge anything for API access (I’m looking at you DAS Trader…) and there’s a great python package called IB-insync for dealing with their sometimes mercurial API logic.
Frustratingly, IB requires a gateway to be running on your computer at all times while you are connected, but for me it’s a small price to pay for not having to shell over my hard earned cash for a data feed. Read the in-sync package docs to get comfortable with setting up a connection to IB’s API. They also have some helpful notebooks, that should make things a bit clearer.
Here’s the start of our connection function:
import asyncio import ib_insync as ibi ibi.util.startLoop() class App: async def run(self): self.ib = ibi.IB() alerted_list = [] alert_df = self.get_alert_df() with await self.ib.connectAsync('127.0.0.1', 7496, clientId=10): contracts = self.get_contracts(alert_df)
The first thing we do is create an IB object that will be used to create an IB connection.
self.ib = ibi.IB()
Note that we are creating a Class called App so that we can freely pass our IB object data between functions.
We then create an empty
alerted_list
to store previously alerted tickers and get a Data Frame from our spreadsheet file much like in our last project.In this script, we will be creating an Asynchronous connection with IB using our IB object. We want this to run asynchronously as we will have many ticks coming in at irregular intervals and we would like to parse them and the data as they are passed to us.
We are essentially building an event driven system as opposed to a sample driven one as we are doing our logic as prices come in rather than asking IB (sampling the data) if it has any more prices every N seconds. Without getting too much into the weeds, it’s would be more preferable to run a sample driven system when things are less time sensitive, or if we are doing very complex calculations with that incoming data. In this use case, I elected to run things asynchronously mainly because I want to beat Kenneth to the punch with my superior speed.
Once we have a connection we are going to need to create some contract objects. We need to tell IB what tickers we want to subscribe to and IB wants us to pass it things like exchange and currency used to define our instrument as it provides data for many markets and instruments across the globe. IB-insync has some helpful python notebooks that should ease the pain of getting up to speed with IB contracts. Either way, I’ve kept things simple here with the standard “SMART” exchange and “USD” currency in our contracts.
def get_contracts(self, alert_df): """ get symbols from dataframe create IB contract object subscribe to contract """ contracts = [ ibi.Stock(symbol, "SMART", "USD") for symbol in set(alert_df.Symbol) ] for contract in contracts: data = self.ib.reqMktData(contract) return contracts
In the
get_contracts
function, we use IB-insync’s Stock object to create a bunch of contracts, passing each one a Symbol name for each symbol in our spreadsheet and using our ib connection object to then request market data.Monitor incoming prices as they arrive and check if any have exceeded their respective trigger price.
Now that we’ve got some incoming data for all our candidates, it’s just a matter of adding some logic to check if conditions have been met before firing off an alert. First, we need to make sure we have a list of limit prices to match with incoming prices:
async for tickers in self.ib.pendingTickersEvent: for ticker in tickers: if type(ticker.contract.symbol) == str: ticker_df = alert_df[alert_df["Symbol"] == Ticker.contract.symbol].iloc[-1]
For all the tickers we have subscribed to, we will grab the
ticker_df
row from our candidate Data Frame that matches the current ticker. Due to IB’s annoying tendency to sometimes pass a number as tick data without a Symbol, we wait for a string to come in. We also need to make sure we are using the last row of the Dataframe to further filter out unwanted elements. Feel free to play around with the frame here to see if you can make things more efficient.Next we create a long and short trigger condition:
short_trigger = max(ticker.last, ticker.high) long_trigger = min(ticker.last, ticker.low)
I know this may seem like redundant logic, but sometimes IB’s data for highs and lows can be wonky, especially with lower liquidity names so I like to check both.
if (ticker_df.OrderSide == -1 and short_trigger >= ticker_df.LimitPrice) or (ticker_df.OrderSide == 1 and long_trigger <= ticker_df.LimitPrice):
Do your logic and if true…
Send an SMS alert message with the important details.
If the ticker has not been alerted yet today, we want to send an alert for this new candidate.
alerted_ticker = ticker.contract.symbol if alerted_ticker not in alerted_list: alert_string = self.print_alert_string(ticker_df, ticker) alerted_list.append(alerted_ticker)
For this one I created a
print_alert_string
function where we can pass in ourticker_df
and our IB ticker object,ticker,
to format a string with pertinent details like Strategy Name, Symbol, Price etc.def print_alert_string(self, ticker_df, ticker): """ get alerted ticker details print strings to console and pass object for sms function """ alert_string = "{} - {} - {} Lmt:{} S:{} Side: {}".format( ticker_df.StrategyName, ticker_df.Symbol, self.get_trigger_price(ticker_df.OrderSide, ticker), ticker_df.LimitPrice, round(POSITION_SIZE / ticker_df.LimitPrice), self.get_ticker_side(ticker_df.OrderSide), ) print("Symbol {} alerted at {} for {}".format( ticker_df.Symbol, datetime.datetime.now(), ticker_df.StrategyName)) print(alert_string) send_sms(str(alert_string)) win32api.MessageBox(0, str(ticker_df.LimitPrice), str(ticker_df.Symbol), 0x00001000) return alert_string
As you can see, in my string I include the Strategy Name, Symbol, a function that will return the trigger high for short or low for long, position size based on a preset Global Variable and a function that returns “Short” or “Long” string based on the strategy side.
def get_ticker_side(self, ticker_side):
"""
get string to print for side of ticker
"""
if ticker_side == -1:
order_side = "Short"
else:
order_side = "Long"
return order_side
def get_trigger_price(self, ticker_side, ticker):
"""
get string to print if trigger is highs for short or lows for long
"""
if ticker_side == -1:
trigger_price = "H: {}".format(max(ticker.last, ticker.high))
else:
trigger_price = "L: {}".format(min(ticker.last, ticker.low))
return trigger_price
Feel free to get creative with the print function, you can try passing in things like volume or percentage change into your alerts if you’re feeling frisky.
I currently print the string to my terminal as well as using the same send_sms
function from my last article. You may notice I’ve also added a windows dialogue box using the win32api package, that creates a pop up on my desktop to ensure maximum annoyance from any new alerts. Feel free to add any other alert service or bot to push your notifications. Just ask ChatGPT how to build a telegram bot…
Wrapping things up
I think that pretty much sums things up for this implementation. You can find the whole script with the examples spreadsheet here on my GitHub.
I’ve tried to keep things self explanatory and simplistic but I’m sure I’ve missed some details. Feel free to ask any questions in the comments or let me know how shite my code is and if you found this helpful let me know as well. I’m making this blog up as I go and appreciate any feedback. I will be writing about more diverse finance topics as I go along, so subscribe to get more of my latest mediocre writing straight to your inbox.
Cheers and thanks for reading!