Automated trading with IB API for Dummies

Why automate your trading? 

Modern trading tools provide extensive functionality, where a lot can be automated through setting time of the day to trade, attaching brackets, stop losses, and trailing orders. However, it all takes time and being too general, these tools are likely not adjusted to your own strategy. So, time is one reason. Another reason is the possibility of manual mistake. Especially, when time is limited, trying to submit or change orders often leads to mistakes.

So, today we will write a very simple script that will randomly select 10 stocks from Nasdaq, find those whose name was mentioned in the news the most over the past 180 days, buy them with attached profit taker of 1 dollar.

We will use IB TWS Python API. To start, download an install IB Trading Workstation and TWS API.

The setup is fairly straightforward and if you need help, consult the Initial Setup section in the IB documentation.

The setup of the TWS API should create a folder on your chosen drive:

TWS API\samples\Python\Testbed

We will rely extensively on the code in Program.py in this folder, however, it does not mean you need to understand all of it. The example provided with the installation is rather complex, and here we want to do something much simpler.

1. Trading Workstation Setup

Firstly, it is important to understand that all trading is executed by the TWS. Your trading script is doing nothing more than communicating with the TWS to get the data from it and send commands to it. So, to start with, make sure your TWS is running throughout your coding and is running in paper trading mode. Then configure the settings as per TWS API documentation. To start with, check the Read-Only API as well to be sure. 

2. Opening the connection

To start with, we need to open a connection to a running instance of TWS. This can be done using TestApp class provided in TWS API\samples\Python\Testbed\Program.py. For simplicity, copy the whole class with the dependencies to your main program script app.py.
Since the TestApp class executes a lot of operations on the start, we'll replace the start function with a shorter version:
def start(self):
if self.started:
return

self.started = True

if self.globalCancelOnly:
print("Executing GlobalCancel only")
self.reqGlobalCancel()


While we are not going to use most of the functions in the TestApp class, it is convenient to keep them there for future use, as they override the base App class.
To start the connection with the TWS, call the following code:



 

Note, that the last argument in app.connect is the client id. Theoretically, you can match it to the value specified in the TWS API settings, however, 0 works just as well.

3. Getting historical data

Historical data for stocks is not available in IB TWS without market data subscription. Of course, you can always subscribe to the data for just a bit more than USD10 per month. Alternatively, you can use the test contracts offered by IB for testing. You can see examples of calling historical data throughout the TestApp class. For example:

timeStr = datetime.datetime.fromtimestamp(time.time()).strftime('%Y%m%d %H:%M:%S')
app.reqHistoricalData(18002, ContractSamples.ContFut(), timeStr, "1 Y", "1 month", "TRADES", 0, 1, False, [])

The example above will get a one year of 1 month bar data ending at the present moment, and showing TRADES data. 
In our example, to get a more practical usage, we request News history. For example, in practice, you can get the latest news articles and based on their positive tone decide to buy a stock. The benefit of using news is that several news subscriptions included with the IB account are free. Additionally, the API for news history is exactly the same as for prices! So, you can always practice on news and then use the same approach for price history.
So, first things first, let's get our contract based on its symbol(ticker). Of course, we can select a symbol on the front end and then copy all the attributes; however, it is somethings more bulletproof to do the search within the software itself. 
Based on the API architecture, the requests are issued asynchronously and the results are further collected in a corresponding TestApp function. To send the request for symbols like "IBM", we add the following call:
app.reqMatchingSymbols(app.nextValidOrderId, "IBM")

The data matching this request will be sent to symbolSamples function of TestApp. By default, the results are logged into log files. This might be difficult to process though. While a safe approach is to write the data to a transactional storage, a simple script can also write to a convenient csv file, which can be overwritten once the processing is complete. To achieve this functionality, we extend symbolSamples with the following code (datalogger code is already present):

dfs = []
for contractDescription in contractDescriptions:
derivSecTypes = ""
for derivSecType in contractDescription.derivativeSecTypes:
derivSecTypes += derivSecType
derivSecTypes += " "
datalogger.info("Contract: conId:%s, symbol:%s, secType:%s primExchange:%s, "
"currency:%s, derivativeSecTypes:%s" % (
contractDescription.contract.conId,
contractDescription.contract.symbol,
contractDescription.contract.secType,
contractDescription.contract.primaryExchange,
contractDescription.contract.currency, derivSecTypes))
dfs.append({"Contract": contractDescription.contract.conId,
"symbol":contractDescription.contract.symbol,
"secType":contractDescription.contract.secType,
"primExchange":contractDescription.contract.primaryExchange,
"currency":contractDescription.contract.currency, "derivativeSecTypes":derivSecTypes})
df=pd.DataFrame(dfs)
df.to_csv(str(reqId)+"symbolSamples.csv", index=False)
Now, we can easily get the list of matching contracts from the main function by reading the corresponding csv:
df = pd.read_csv(str(app.nextValidOrderId)+"symbolSamples.csv")

For this example, we don't want all contracts, just the main IBM stock listed on NYSE is sufficient:

df=df[df["primExchange"]=="NYSE"] # is only one
Further, we can get all the news for the selected symbol:
queryTime = (datetime.datetime.today() - datetime.timedelta(days=30)).strftime("%Y%m%d %H:%M:%S")
# get all news
for index, row in df.iterrows():
print(row['symbol'])
row[
'Contract']
app.reqHistoricalNews(app.nextValidOrderId+
1, row['Contract'], "BRFG", queryTime , "", 40, [])

Remember, that the above is just a request, the collection of the news should be done in historicalNews function of TestApp. Again, the default function logs the news to the default logger, which is not so convenient. To make it easier, we add a few lines to write to the news spreadsheet.

if os.path.exists(os.path.join(os.getcwd(),"news.csv")):
df_news = pd.read_csv("news.csv",index_col=None)
df_news=df_news.append(pd.DataFrame({"HistoricalNews. ReqId":[str(reqId)],"Time":[str(time)],
"ProviderCode":[str(providerCode)],"ArticleId":[str(articleId)],
"Headline":[str(headline)]}))
df_news.to_csv("news.csv", index=False)

4. Sending orders

Now we have our contract for IBM and potentially some news. As our goal was to by by some stock if it was covered in the news, we will now do just that: verify that news were received and if so, buy 10 shares an market price, while setting a profit taker and stop less at +/- 10% of the purchase price correspondingly:


df_news = pd.read_csv("news.csv")
df_news = df_news.dropna()
orders=[]
for index, row in df.iterrows():
print(row['symbol'])

if len(df_news[df_news['Headline'].str.contains(row['symbol'],False)]) > 0:
contract = Contract()
contract.conId=row['Contract']
contract.primaryExchange=row['primExchange']
contract.exchange=row['primExchange']
contract.currency = row['currency']
contract.derivativeSecTypes = row['derivativeSecTypes']
contract.secType = row['secType']
contract.symbol = row['symbol']
orders.append(contract)
print("Selected orders: " + str(len(orders)))
for contract in orders:
#create an order with an attached bracket orders
order_hist=app.nextValidOrderId+1
print(order_hist)
#place an order
quantity = 10
last_price = 1 #or get the price using: app.reqMktData(1000, contract, "", False, False, [])   
    percent = 0.10
bracketOrder = OrderSamples.BracketOrder(order_hist, "BUY", quantity,
last_price, last_price*(1+percent),
last_price*(1-percent))
#place all orders in the bracket
for o in bracketOrder:
o.tif="DTC"
app.placeOrder(o.orderId, contract, o)
app.nextOrderId() # need to advance this


Note, that it is a good time to remove Read-only in TWS API Settings. If you want to have more precautions, you can also set transmit=False for all orders (o) before placing them. In this case, you can review the orders in the TWS and transmit only when you are happy with them.
Finally, don't forget to disconnect your script when you are done.
app.disconnect()
print("Disconnected")


Comments