10 August 2020

Advanced Techniques in TWS API - Interactive Brokers Part 3

In the first part of the blog on the TWS API of Interactive Brokers, I have dealt with the Installation of TWS API on a Windows machine for Anaconda distro.

In the second part of the blog, titled Fundamentals of TWS API, I have dealt with the essentials to get started.

Now in this third part I seek to explore the concepts like using a scanner, retrieving the OHLCV data and writing it to a CSV file, Hedging a Futures Contract with Options and many others including a fully functional sample trading system.


Table of Contents :


Optionable Contracts in NSE using the Scanner

Let's get to the point straight - The TWS API is just an interface to the TWS. If you are having problems defining a scanner viz the API , always make sure you can create a similar scanner using the TWS or the Mosaic Market Scanner.


To get a better understanding, take a look at the screenshot of the TWS scanner 



Here the instrument is "Asia Stocks", the Location is  "India" , the filter is "Has Options is" and the Parameter is "Shortable Shares"

 

Now let's look at how we can code these criteria in Python using the TWS API and get the desired results.

For this the first thing we need to do is find out what are the scanner parameters. We can do this via reqScannerParameters. (Note that not all of the returned parameters to scannerParameters are available from the API scanners)

    # Request the scanner parameters
    client.reqScannerParameters()

The callback to the above request is scannerParameters.  A string containing all available XML formatted parameters will then be returned. The XML file size was around 2 MB .
    def scannerParameters(self, xml: str):
        ''' Callback for reqScannerParameters '''
        super().scannerParameters(xml)
        #open('log/scanner.xml', 'w').write(xml)
        open('scanner.xml', 'w').write(xml)
        print("ScannerParameters received."

Open the XML file and peruse it. Then you will find the right parameters to be used while creating the ScannerSubscription object to retrieve NSE stocks.

Take a look at a scan shot of the XML image and now a question - What location code will you use in the API to retrieve stocks from the NSE?


Accessing the market scanner programmatically involves the following steps:

  1. Create a scanner subscription using  ScannerSubscription
  2. Request a scanner subscription by calling reqScannerSubscription.
  3. Access the scanner results using the callback scannerData.
  4. Stop the scanner by calling cancelScannerSubscription. 

The scannerData callback will be called once for each desired result up to a maximum of 50. When the data transmission is complete, the scannerDataEnd callback function will be called.


Firstly we need to create an instance of ScannerSubscription. There are various fields that can be set but the following three are the most important:

  1. Instrument
  2. locationCode
  3. scanCode

 

The below code is a typical example of what I would set for the NSE stocks:

    # Create the object ScannerSubscription
    scanSub = ScannerSubscription() # Defines a market scanner request
    scanSub.instrument = 'STOCK.HK'
    scanSub.locationCode = 'STK.HK.NSE'
    scanSub.scanCode = 'ALL_SYMBOLS_ASC' 

We get only 50 securities in one call to reqScannerSubscription . So, to setup simple filters, we have flexibility. As an example to ensure that we get the stocks whose volume is above 50,000, we can set the ScannerSubscription's aboveVolume field to 50000.

    scanSub.aboveVolume = '50000'

There are a host of permutations and combinations that you can set thee fields to and these are available in the XML file. For more examples, open up the ScannerSubscriptionSamples.py program in C:\TWS API\samples\Python\Testbed and check them.

 

The ScannerSubscription Class is in the scanner.py program located at C:\TWS API\source\pythonclient\ibapi. This Class does not have any methods of its own and it just holds data for the desired subscription. Our Python program passes this data to IB by calling the reqScannerSubscription.


reqScannerSubscription(int reqId, ScannerSubscription subscription, List< TagValue > 	scannerSubscriptionOptions,
List< TagValue > ScannerSubscriptionFilterOptions ) 

The third argument "scannerSubscriptionOptions" is reserved for internal use by IB. The last argument "ScannerSubscriptionFilterOptions" is of importance to us. This is how we set additional filter conditions. This accepts a container of name = value pairs . Here the name identifies the nature of the filter and the value sets the value that the criteria is checked against.

 

Now let's take a look at the screen shot of the TWS scanner above. Here we have used a filter "Has Options" and set this to Yes. Now if we open the XML file received from scannerParameters callback and make a search, this is what we find:


Highlighted in the above screenshot is the <displayName> tag. Above that is the <AbstractField> tag. Just below is the <Code> tag and this contains the filter names. If you search through the XML file, you will find many filter names and some are given below:

  • hasOptionsIs
  • optVolumeAbove
  • impVolatAbove
Actually the idea is use these to see if we can filter all the optionable stocks. Let's check the code below:

    # Create the object ScannerSubscription
    scanSub = ScannerSubscription() # Defines a market scanner request
    scanSub.instrument = 'STOCK.HK'
    scanSub.locationCode = 'STK.HK.NSE'
    scanSub.scanCode = 'ALL_SYMBOLS_ASC'
    
    # Set additional filter criteria
    tagvalues = []
    tagvalues.append(TagValue('avgOptVolumeAbove', '0'))
    tagvalues.append(TagValue('hasOptionsIs', 'Yes'))
    print(tagvalues)
    # Request the scanner subscription
    client.reqScannerSubscription(7, scanSub, [], tagvalues)

When I ran the above code, the callback did not give me the desired results. So basically the "hasOptionsIs" does not work.  Then I tried with tagvalue "avgOptVolumeAbove" and that also didn’t work. That’s when I realised that I will have to seek an alternative path to try and get the optionable stocks. A search on the  TWS API group  reveals that these tagvalues have been documented in the XML file but are yet to be incorporated in the Python based API. So yeah, this could possibly be there in the next/future versions of the TWS API. (This program has been tested on the API version 9.79 with release date 05 Feb 2020).

So I kept searching  for a possible solution and it turns out that IB has a concept called Single Stock Futures(SSF).   Check this link forthe SSF in Asia Pacific.


If you click on the corresponding link to NSE , you will find that it gives you the IB symbol and the actual NSE Symbol. In the case of Baja Auto, the IB symbol is "BAJAJ-AUT" while the actual NSE symbol is "BAJAJ-AUTO". Be aware of this.

 

If Now lets get back to the XML file to find the corresponding entries for Single Stock Futures(SSF)


And now the Python Code.
    # Create the object ScannerSubscription
    scanSub = ScannerSubscription() # Defines a market scanner request
    scanSub.instrument = 'SSF.HK' # SSF = Single Stock Futures
    scanSub.locationCode = 'SSF.HK.NSE'
    scanSub.scanCode = 'ALL_SYMBOLS_ASC'

This is the output in my terminal.


Now if you watch closely, each SSF is repeated three times.  This is becos NSE has three future contracts for a given  instrument at any given point of time - Near, Next and Far.

 

So now, our goal is to just get all the available contracts and then ensure that each symbol is unique in the list. This will give us the complete list of Optionable Contracts. We can then cross verify this list with the official NSE list of Underlyings & Underlying Info .

 

But unfortunately, at the moment, using the TWS API, we can get only 50 rows from each scanner request. That’s how it is with the TWS also. Also at the moment, You can't have more than 10 scanner subscriptions running same time.

 

So yeah, what we can do is run the scanner multiple times with different scan parameters and collect the output from the call backs.  Let us tweak the scan parameters once again to get better results. Check the code below:

def main():

    # Create the client and connect to TWS
    client = StockScanner('127.0.0.1', 7497, 7)
    time.sleep(3)

    # Create the object ScannerSubscription
    scanSub = ScannerSubscription() # Defines a market scanner request
    scanSub.instrument = 'STOCK.HK'
    scanSub.locationCode = 'STK.HK.NSE'
    # scanSub.scanCode = 'ALL_SYMBOLS_ASC'
    scanSub.scanCode = 'TOP_TRADE_COUNT'

    # Set additional filter criteria
    tagvalues = []
    tagvalues.append(TagValue('hasOptionsIs', 'true'))
    tagvalues.append(TagValue("usdPriceAbove", "0"))
    tagvalues.append(TagValue("usdPriceBelow", "2"))
    print(tagvalues)

    # Request the scanner subscription
    client.reqScannerSubscription(7, scanSub, [], tagvalues)

    # Disconnect from TWS
    time.sleep(5)
    client.disconnect() 
Most important point to note here is that, You can keep varying the usdPriceAbove and usdPriceBelow parameters to get different list of records. Compile all the records by running the scanner multiple times and you will get the complete list. When I run this, the output of the program is thus: 


 

Check out the Code for this on GitHub


Note that at the time of writing,  I am using the TWS API version 9.79 with release date 05 Feb 2020. Maybe for latter versions we can Hope that things will be better !!!

 

Work Around Solution: So what we can do is check out the link for Single Stock Futures (SSF's) in NSE. Copy them into an Excel or CSV file or run a webscraping program . And use this list as the starting point for all your strategies that require "Optionable Stocks". 


Write OHLCV data to Pandas and CSV files

Lets go through the code straight.


    def __init__(self, addr, port, client_id):
        EClient.__init__(self, self)

        # Initialize properties
        self.symbols = {'ACC':'44652144','INFY':'44652017', 'WIPRO':'44652030'} # IB symbol could be different from NSE symbol, so use conId
        self.candle_dict = {}

The first thing to realize is that the Symbols that IB uses internally is not the same as the symbols used by the National Stock Exchange(NSE). 


If you go to the market watchlist on TWS, click on a Symbol --> Financial Instrument Info --> Description , in the popup windows, you get all the basic  information about the stock. That includes ISIN number, which is used by the NSE. I would have loved to plug this into the "value" in the key-value pair of the self.symbols dictionary. 


Now in TW go to  Symbol --> Financial Instrument Info --> Details. It will open up a new tab in your browser where you have more details. Check out for "Contract Identifiers" and you will find that whether we are referring to a Futures instrument or its underlying, the conId is a constant. So let us stick to that instead of ISIN number.


I have investigated and it appears that there are two ways of getting the conId of a given stock:

1) Take this info from the IB website.

2) Make a call to  reqcontractDetails and get the conId it from the callback. This is my preferred method.
    @iswrapper
    def historicalData(self, req_id, bar):
        ''' Callback to reqHistoricalData '''

        # Add the stock prices to the dictionary
        self.candle_dict['Date'].append(bar.date)
        self.candle_dict['Open'].append(bar.open)
        self.candle_dict['High'].append(bar.high)
        self.candle_dict['Low'].append(bar.low)
        self.candle_dict['Close'].append(bar.close)
        self.candle_dict['Volume'].append(bar.volume) # whatToShow in reqHistoricalData should be TRADES

Since I want the OHLCV data along with the DateTime, this is what my code would look like in the historicalData callback. Now lets go to the main function in the program: :
def main():

    # Create the client and connect to TWS
    client = ReadTicker('127.0.0.1', 7497, 7)
    time.sleep(3) #Sleep interval to allow time for connection to server

    # Get expiration dates for contracts
    for symbol in client.symbols:

        # Define the contract
        contract = Contract()
        contract.symbol = symbol
        contract.secType = 'STK'
        contract.exchange = 'NSE'
        contract.conId = client.symbols[symbol]
        print('Contract ID for symbol {} is: {}'.format(symbol, client.symbols[symbol])) #take conId from IB website or contractDetails callback
        contract.currency = "INR"

        # Initialize the candle data dictionary
        for v in ['Date','Open', 'High', 'Low', 'Close', 'Volume']:
            client.candle_dict[v] = []

        # Request OHLCV data
        now = datetime.now().strftime("%Y%m%d %H:%M:%S")
        client.reqHistoricalData(4, contract, now, '2 D', '5 mins', 'TRADES', False, 1, False, [])
        time.sleep(3) # Allow enough time for data to be returned.
        df = pd.DataFrame(client.candle_dict, columns=['Date', 'Open', 'High', 'Low', 'Close', 'Volume'])
        df['Date'] = pd.to_datetime(df['Date']) # Convert to pandas DateTime format
        # print(df.tail())
        # print(df.columns)                
        df.to_csv(symbol + '.csv', encoding='utf-8', index=False)
        client.candle_dict.clear()

    # Disconnect from TWS
    client.disconnect() 
The main function is pretty straight forward. Read all the symbols in a For loop , initialise the dictionary that will used for storing the OHLCV data. Once the data is collected in the dictionary, convert that to a pandas DataFrame and write it to a CSV file. Now add as many symbols as you want in the class and get the OHLCV data.Check out the Code for this on GitHub


Web Scrap IB site for Single Stock Futures (SSF)


This is the URLwhere Interactive broker's has data about all the Single Stock Futures(SSF).

 

Let's look at the Web Scraping option's to get hold of all the SSF's.  We have basically three modules that we can consider - Scrapy, Selenium and Beautiful Soup.

 

In this specific case, we are primarily concerned about getting the contents of the table below. And this table spreads across to two pages. So our job narrows down to getting the contents of the table from two different URL's



Since it’s a pretty straightforward case of merely extracting some information from a website, I feel that Selenium and Scrapy will be too much of an overkill. So let's settle down for Beautiful Soup. In conjunction with requests module, it should do the trick.


Step1: Identify the element to extract.

On the Chrome browser, right click on Inspect and hover over the elements until you get to the point where the entire table that we want is covered . Now you know that the element is a table element in the HTML document. Or another simple method is right click on the web page and click on "View Page Source". Scan through the page source code until you find the  Table tag . (The <table> tag defines an HTML table).


In case you want to learn the finer details of extracting the HTML elements, Tags, CSS etc. by using Beautiful Soup, check out the comprehensive Beautiful Soup documentation


Step2: Add header and URL to the requests.

This helps to create a request to the website. The header is sometimes necessary and useful to spoof your request so that it looks like it comes from a legitimate browser. Quite a few financial sites actually block requests that do not have a legit header.

requests_headers = {'User-Agent': 'Mozilla/5.0 6(Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36'}
Step3: Identify the URL's from which we need to extract the SSF's
There are basically two URL's from which we have to extract the data. These two are: 
url1 = "https://www.interactivebrokers.com/en/index.php?f=2222&exch=nse&showcategories=SSF" # first page has 100 ssf
url2 = "https://www.interactivebrokers.com/en/index.php?f=2222&exch=nse&showcategories=SSF&p=&cc=&limit=100&page=2" 

Step4: Pass to requests module.

When we pass any URL, the  requests library will make a GET request to a web server, which will download the HTML contents of a given web page for us. After running our request, we get a Response object. This object has a status_code property, which indicates if the page was downloaded successfully. A status_code of 200 means that the page downloaded successfully. We won’t fully dive into status codes here, but a status code starting with a 2 generally indicates success, and a code starting with a 4 or a 5 indicates an error. We can print out the HTML content of the page using the content property:

    response = requests.get(url, headers=requests_headers)
    print('\nThe URL: {}'.format(response.url))
    print('The Status Code from Requests: {}'.format(response.status_code)) # print status code

Step5: Initiate BS and list element to extract all the rows in the table

We can use the BeautifulSoup library to parse this document. We have to create an instance of the BeautifulSoup class to parse our document. Next if we watch the structure of the webpage carefully, we will notice that there are thee tables and the third one is what we want to scrap.


    soup = BeautifulSoup(response.content, 'lxml') # Parse the HTML as a string
    table = soup.find_all('table')[2] # Grab the third table on the webpage
    df = pd.DataFrame(columns=range(0,4), index = [0])
For all the tr elements (The tr element defines a row in a table), pick up the td and th elements.
    for row_marker, row in enumerate(table.find_all('tr')):

        if data==True:
            columns = row.find_all(['td']) # Capture only the Table Data Cells.
        else:
            columns = row.find_all(['th']) # Capture only the Table Header Cells.
            
        try:
            df.loc[row_marker] = [column.get_text() for column in columns]
        except ValueError:
            # It's a safe way to handle when [column.get_text() for column in columns] is empty list.
            continue
Step6: Get the table header cells and table data cells into one DataFrame.
    df_table_header = get_table_data(url1, data=False)
    if df_table_header.iloc[0,0] == 'IB Symbol': # confirm 'IB Symbol' content of first row , then change next column
        df_table_header.iloc[0,1] = 'Product Description' # Original description is too long. Cut it short
    else:
        print ("WARNING !!!.Some Change in the Header Data of IB webitse. ***CHECK ALL DATA***")
    
    df_table_data_P1 = get_table_data(url1, data=True)
    df_table_data_P2 = get_table_data(url2, data=True)
    
    df_final = pd.concat([df_table_header, df_table_data_P1, df_table_data_P2])
Step7: Process and Write to CSV.
    df_final = df_final.rename(columns=df_final.iloc[0]) # Rename first row to column header
    df_final = df_final.dropna() # Drop rows where atleast one element is missing
    df_final = df_final[df_final.Symbol != 'Symbol'] # if there is row with text 'symbol', exclude that
    df_final = df_final[df_final.Symbol != 'NIFTY'] # If there is row with 'NIFTY50', exclude that
    df_final = df_final[df_final.Symbol != 'BANKNIFTY'] # If there is row with 'BANKNIFTY', exclude that
    df_final = df_final[df_final.Symbol != 'USDINR'] # If there is row with 'USDINR', exclude that
    df_final = df_final[df_final.Symbol != 'EURINR'] # If there is row with 'EURINR', exclude that
    df_final = df_final[df_final.Symbol != 'GBPINR'] # If there is row with 'GBPINR', exclude that
    df_final = df_final[df_final.Symbol != 'JPYINR'] # If there is row with 'JPYINR', exclude that
    df_final = df_final.reset_index(drop=True)
    df_final.to_csv('scraped_data.csv', index=False) # dont write row names

Creating a Stock Universe


So now that we have a file with all the Stocks which are optionable (scraped_data.csv), our next step is to garner  the Contract ID (conID) of all these stocks. The Contract ID is a  unique identifier that IB uses for every single instrument in its database. Now theoretically, we can just use the IB Symbol (Note that the IB Symbol for any given instrument could be different from NSE Symbol) to place orders to TWS. But it is always safe to input the Contract ID along with the Symbol and Primary Exchange also when we define the contract in the Python program.

    # Define a Contract
    contract = Contract()
    contract.symbol = 'INFY'
    contract.conId = '437440510'
    contract.secType = 'STK'
    contract.currency = 'INR' 
    contract.primaryExchange = "NSE"
Check out this url for the conID of stock symbol Infy

To get the Contract Id for any given IB Symbol, we can make a  call to reqContractDetails. In the contractDetails callback, we will get the Contract ID. This Contract ID is stored in a deque. But before we can store it in a deque, we need to import collections library and initialise the properties in the constructor.
import collections

class CreateStockUniverse(EWrapper, EClient):
    ''' Class that Serves as the Client and the Wrapper '''

    def __init__(self, addr, port, client_id):
        EWrapper.__init__(self)
        EClient.__init__(self, self)
        
        # Initialize properties
        df_scraped_data = pd.read_csv('scraped_data.csv') # Read csv into pandas DataFrame
        self.df_scraped_data = df_scraped_data
        self.conId_dq = collections.deque() # Store Contract ID in deque

        # Connect to TWS API
        self.connect(addr, port, client_id)

        # Launch the client thread
        thread = threading.Thread(target=self.run)
        thread.start()

    @iswrapper
    def contractDetails(self, reqId, details):
        ''' Callback from '''
        print('Details from contractDetails Callback follows:')
        print('Market Name: {}'.format(details.marketName)) # symbol=TATACONSU, marketname=TATACONSUM
        print('Long Name: {}'.format(details.longName))
        print('Contract ID: {}'.format(details.contract.conId))
        # print('Time Zone for the Product: {}'.format(details.timeZoneId))
        # print(details)
        self.conId_dq.append(details.contract.conId)
In the main function, we define each contract and call the reqContractDetails. Upon completion, the deque is converted to a List and then to a pandas DataFrame. From here it is written to a csv file titled "stock_universe_web_scraping.csv"

    conId_list = list(client.conId_dq)
    df_conId = pd.DataFrame([conId_list]).transpose()
    df_conId.columns = ['Contract ID']
    df_final = client.df_scraped_data.join(df_conId)
    print ('Length of df_scraped_data: {}'.format(len(client.df_scraped_data)))
    print ('Length of df_conId: {}'.format(len(df_conId)))
    print('Length of df_final: {}'.format(len(df_final)))
    df_final.to_csv('stock_universe_web_scraping'+ '.csv', encoding='utf-8', index=False)
Check out the full program on GitHub

Write OHLCV Data of the Stock Universe to CSV file



IntradayTrading System: OpenEQ



Full Codebase on GitHub

No comments:

Post a Comment