Backtrader: Stock Screener with Alpha Vantage

Backtrader, oh how we have missed you! It has been too long since the last article on this excellent platform. Hopefully today, we can make up for that neglect by using Backtrader as the engine for a stock screener. The platform is a perfect choice for a stock screener given how easy it is to create custom Indicators. Couple that with an already impressive library of built-in options and it becomes easy to screen for whether your favourite indicator is bullish or bearish.

Pre-Reading

Before we start, it is worth pointing out that this article builds on the shoulders of some other posts. More specifically, we will be using Alpha Vantage to provide data for the screener. As such, if you are interested in the mechanics of how we download the data, it would be worth looking at those articles first. Even if not, at a bare minimum, you will need to sign up for an API key from Alpha Vantage. Again, if you are not sure how to do this, the same articles will cover those steps:
  1. Replacing Quandl Wiki Data with Alpha Vantage
  2. Backtrader: Alpha Vantage Data: Direct Ingest

Scope

Moving onto scope, the code should allow users to download daily data for up to 500 instruments. The data will then be fed into Backtrader which will, in turn, run through the data and calculating indicator values. At the end of the run, we will create a report based on the values of the last bar of data to show if the indicators are bullish or bearish. The idea is that you could then follow the example to add to or replace indicators that interest you.

Before Running

Make sure you find the line the following line in the code example and insert your API key.
Apikey = 'INSERT YOUR API KEY HERE'
As you might imagine, the script will not work if you don’t!

Code

'''
Author: www.backtest-rookies.com

MIT License

Copyright (c) 2019 backtest-rookies.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''

from alpha_vantage.timeseries import TimeSeries
import pandas as pd
import numpy as np
import backtrader as bt
from datetime import datetime
import time

# IMPORTANT!
# ----------
# Register for an API at:
# https://www.alphavantage.co/support/#api-key
# Then insert it here.
Apikey = 'INSERT YOUR API KEY HERE'


def alpha_vantage_eod(symbol_list, compact=False, debug=False, *args, **kwargs):
    '''
    Helper function to download Alpha Vantage Data.

    This will return a nested list with each entry containing:
        [0] pandas dataframe
        [1] the name of the feed.
    '''
    data_list = list()

    size = 'compact' if compact else 'full'

    count = 0
    total = len(symbol_list)

    for symbol in symbol_list:
        count += 1

        print('\nDownloading: {}'.format(symbol))
        print('Symbol: {} of {}'.format(count, total, symbol))
        print('-'*80)

        # Submit our API and create a session
        alpha_ts = TimeSeries(key=Apikey, output_format='pandas')

        data, meta_data = alpha_ts.get_daily(symbol=symbol, outputsize=size)

        #Convert the index to datetime.
        data.index = pd.to_datetime(data.index)
        data.columns = ['Open', 'High', 'Low', 'Close','Volume']

        if debug:
            print(data)

        data_list.append((data, symbol))

        # Sleep to avoid hitting API limit
        print('Sleeping |', end='', flush=True)
        for x in range(12):
            print('=', end='', flush=True)
            time.sleep(1)
        print('| Done!')

    return data_list

class TestStrategy(bt.Strategy):

    def __init__(self):

        self.inds = dict()
        self.inds['RSI'] = dict()
        self.inds['SMA'] = dict()

        for i, d in enumerate(self.datas):

            # For each indicator we want to track it's value and whether it is
            # bullish or bearish. We can do this by creating a new line that returns
            # true or false.

            # RSI
            self.inds['RSI'][d._name] = dict()
            self.inds['RSI'][d._name]['value']  = bt.indicators.RSI(d, period=14)
            self.inds['RSI'][d._name]['bullish'] = self.inds['RSI'][d._name]['value']  > 50
            self.inds['RSI'][d._name]['bearish'] = self.inds['RSI'][d._name]['value']  < 50

            # SMA
            self.inds['SMA'][d._name] = dict()
            self.inds['SMA'][d._name]['value']  = bt.indicators.SMA(d, period=20)
            self.inds['SMA'][d._name]['bullish'] = d.close > self.inds['SMA'][d._name]['value']
            self.inds['SMA'][d._name]['bearish'] = d.close < self.inds['SMA'][d._name]['value']

    def stop(self):
        '''
        Called when backtrader is finished the backtest. Here we will just get
        the final values at the end of testing for each indicator.
        '''

        # Assuming all symbols are going to have the same data on the same days.
        # If that is not the case and you are mixing assets from different classes,
        # regions or exchanges, then you might want to conisder adding an extra
        # column to the final results.
        print('{}: Results'.format(self.datas[0].datetime.date()))
        print('-'*80)


        results = dict()
        for key, value in self.inds.items():
            results[key] = list()

            for nested_key, nested_value in value.items():

                if nested_value['bullish'] == True or nested_value['bearish'] == True:
                    results[key].append([nested_key, nested_value['bullish'][0],
                            nested_value['bearish'][0], nested_value['value'][0]])


        # Create and print the header
        headers = ['Indicator','Symbol','Bullish','Bearish','Value']
        print('|{:^10s}|{:^10s}|{:^10s}|{:^10s}|{:^10s}|'.format(*headers))
        print('|'+'-'*10+'|'+'-'*10+'|'+'-'*10+'|'+'-'*10+'|'+'-'*10+'|')

        # Sort and print the rows
        for key, value in results.items():
            #print(value)
            value.sort(key= lambda x: x[0])# Sort by Ticker

            for result in value:
                print('|{:^10s}|{:^10s}|{:^10}|{:^10}|{:^10.2f}|'.format(key, *result))


# Create an instance of cerebro
cerebro = bt.Cerebro()

# Add our strategy
cerebro.addstrategy(TestStrategy)

# Download our data from Alpha Vantage.
symbol_list = ['LGEN.L','LLOY.L','NG.L', 'BDEV.L']
data_list = alpha_vantage_eod(
                symbol_list,
                compact=True,
                debug=False)

for i in range(len(data_list)):

    data = bt.feeds.PandasData(
                dataname=data_list[i][0], # This is the Pandas DataFrame
                name=data_list[i][1], # This is the symbol
                timeframe=bt.TimeFrame.Days,
                compression=1
                )

    #Add the data to Cerebro
    cerebro.adddata(data)

print('\nStarting Analysis')
print('-'*80)
# Run the strategy
cerebro.run()

Commentary

We kick-off this script by downloading data for each ticker in our ticker list. For this version, we house them in a simple list. If your list of tickers is going to be large or maintained somewhere else, users might want to build on this by importing a ticker list from a file. When we start downloading data, we loop through each item in the list and make a call to the API. Unfortunately, due to Alpha Vantage having strict API limits, we need to perform a long wait between each data download request. This results in the script waiting 12 seconds between download calls because we are limited to 5 API calls per minute. Showing Backtrader Screener downloading data and sleeping For this reason, this screener is not really intended to be used on intra-day timeframes. It would be too slow unless your ticker list was quite small. Having said that, if you wish to swap out the data part with another service, then the rest of the code should still work. Another limitation is that Alpha Vantage’s free API is limited to 500 calls a day. Therefore, if you are thinking of intra-day screening, you would need to avoid making too many sweeps. After we have all the data, it is just a matter of adding our favorite indicators to a dictionary. The dictionary is structured in such a way that we can conveniently make a report from it later. During __init__()we create that dictionary and add the indicators and create some “data feeds”. These new feeds signal whether the indicator is bullish or bearish. Creating a feed is as simple as creating a condition which returns TrueorFalse. That is one of the great things about Backtrader, it is really easy to create new data feeds. The RSI example shows this on the line:
self.inds['RSI'][d._name]['bullish'] = self.inds['RSI'][d._name]['value'] > 50
As you can see, we are just checking if the RSI is over 50. If it is, then the data feed will be True, if not, it will be False. For more information see: https://www.backtrader.com/docu/concepts/#almost-everything-is-a-data-feed

Adding Extra Indicators

So if you want to add extra indicators, you simply need to follow the same format of the examples
self.inds = dict()
self.inds['RSI'] = dict()
self.inds['SMA'] = dict()

for i, d in enumerate(self.datas):

    # For each indicator we want to track it's value and whether it is
    # bullish or bearish. We can do this by creating a new line that returns
    # true or false.

    # RSI
    self.inds['RSI'][d._name] = dict()
    self.inds['RSI'][d._name]['value']  = bt.indicators.RSI(d, period=14)
    self.inds['RSI'][d._name]['bullish'] = self.inds['RSI'][d._name]['value']  > 50
    self.inds['RSI'][d._name]['bearish'] = self.inds['RSI'][d._name]['value']  < 50
That means:
  1. Create a new entry in self.indsand make it as a dict()type.
  2. As you loop through the data feeds, use the data feed name (d._name) to create another new entry in the entry you just made.
  3. Add the indicator at a ['value']key.
  4. Create a check for whether the indicator is bullish or bearish. This should be a boolean value.
As long as you follow the correct format when adding new indicators during __init__()then the stop()method will not require any updates.

Running the Code

After running the Backtrader screener, you should have a table output which looks like this: Showing the final output of the backtrader screener If you are checking a lot of instruments, you might want to consider running this as a nightly job after the markets have closed.

Find This Post Useful?

If this post saved you time and effort, please consider support the site! There are many ways to support us and some won’t even cost you a penny. 

Backtest Rookies is a registered with Brave publisher!

Brave users can drop us a tip.

Alternatively, support us by switching to Brave using this referral link and we will receive some BAT! 


https://brave.com/bac691

Referral Link

Enjoying the content and thinking of subscribing to Tradingview? Support this site by clicking the referral link before you sign up! 

https://tradingview.go2cloud.org/SHpB


Donate with PayPal using any payment method you are comfortable with! 


3HxNVyh5729ieTfPnTybrtK7J7QxJD9gjD

0x9a2f88198224d59e5749bacfc23d79507da3d431

M8iUBnb8KamHR18gpgi2sSG7jopddFJi8S