Stock Screening

Looking for some other things I came across a question on one of the StackOverlow family sites: Quantitative Finance aka Quant StackExchange. The question:

It is tagged as Python, so it is worth seeing if backtrader is up to the task.

The Analyzer itself

The problem seems appropriate for an easy analyzer. Although the problem just wants those above the moving average, we’ll keep extra information like the stocks which don’t meet the criteria, to make sure the grain is being actually separated from the chaff.

class Screener_SMA(bt.Analyzer):
    params = dict(period=10)

    def start(self):
        self.smas = {data: bt.indicators.SMA(data, period=self.p.period)
                     for data in self.datas}

    def stop(self):
        self.rets['over'] = list()
        self.rets['under'] = list()

        for data, sma in self.smas.items():
            node = data._name, data.close[0], sma[0]
            if data > sma:  # if data.close[0] > sma[0]
                self.rets['over'].append(node)
            else:
                self.rets['under'].append(node)

Note

Of course one also needs import backtrader as bt

That pretty much solves the problem. Analysis of the Analyzer:

  • Have the period as a parameter to have a flexible analyzer

  • start method

    For each data in the system make a Simple Moving Average (SMA) for it.

  • stop method

    Look which data (close if nothing else is specified) is above its sma and store that in a list under the key over in the returns (rets)

    The member rets is standard in analyzers and happens to be a collections.OrderedDict. Created by the base class.

    Keep the ones that doesn’t meet the criteria under a key under

The issue now: getting the analyzer up and running.

Note

We assume the code has been put in a file named st-screener.py

Approach 1

backtrader includes, since almost the beginning of time, an automated script running called btrun, which can load strategies, indicators, analyzers from python modules, parse arguments and of course plot.

Let’s do a run:

$ btrun --format yahoo --data YHOO --data IBM --data NVDA --data TSLA --data ORCL --data AAPL --fromdate 2016-07-15 --todate 2016-08-13 --analyzer st-screener:Screener_SMA --cerebro runonce=0 --writer --nostdstats
===============================================================================
Cerebro:
  -----------------------------------------------------------------------------
  - Datas:
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data0:
      - Name: YHOO
      - Timeframe: Days
      - Compression: 1
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data1:
      - Name: IBM
      - Timeframe: Days
      - Compression: 1
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data2:
      - Name: NVDA
      - Timeframe: Days
      - Compression: 1
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data3:
      - Name: TSLA
      - Timeframe: Days
      - Compression: 1
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data4:
      - Name: ORCL
      - Timeframe: Days
      - Compression: 1
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data5:
      - Name: AAPL
      - Timeframe: Days
      - Compression: 1
  -----------------------------------------------------------------------------
  - Strategies:
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Strategy:
      *************************************************************************
      - Params:
      *************************************************************************
      - Indicators:
        .......................................................................
        - SMA:
          - Lines: sma
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Params:
            - period: 10
      *************************************************************************
      - Observers:
      *************************************************************************
      - Analyzers:
        .......................................................................
        - Value:
          - Begin: 10000.0
          - End: 10000.0
        .......................................................................
        - Screener_SMA:
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Params:
            - period: 10
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Analysis:
            - over: ('ORCL', 41.09, 41.032), ('IBM', 161.95, 161.221), ('YHOO', 42.94, 39.629000000000005), ('AAPL', 108.18, 106.926), ('NVDA', 63.04, 58.327)
            - under: ('TSLA', 224.91, 228.423)

We have used a set of well known tickers:

  • AAPL, IBM, NVDA, ORCL, TSLA, YHOO

And the only one that happens to be under the 10 days Simple Moving Average is TSLA.

Let’s try a 50 days period. Yes, this can also be controlled with btrun. The run (output shortened):

$ btrun --format yahoo --data YHOO --data IBM --data NVDA --data TSLA --data ORCL --data AAPL --fromdate 2016-05-15 --todate 2016-08-13 --analyzer st-screener:Screener_SMA:period=50 --cerebro runonce=0 --writer --nostdstats
===============================================================================
Cerebro:
  -----------------------------------------------------------------------------
  - Datas:
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data0:
...
...
...
        - Screener_SMA:
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Params:
            - period: 50
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Analysis:
            - over: ('ORCL', 41.09, 40.339), ('IBM', 161.95, 155.0356), ('YHOO', 42.94, 37.9648), ('TSLA', 224.91, 220.4784), ('AAPL', 108.18, 98.9782), ('NVDA', 63.04, 51.4746)
            - under:

Notice how the 50 days period has been specified in the command line:

  • st-screener:Screener_SMA:period=50

    In the previous run this was st-screener:Screener_SMA and the default 10 from the code was used.

We also needed to adjust fromdate to make sure there were enough bars to consider for the calculation of the Simple Moving Averages

In this case all tickers are above the 50 days moving average.

Approach 2

Craft a small script (see below for the full code) to have finer control of what we do. But the results are the same.

The core is rather small:

    cerebro = bt.Cerebro()
    for ticker in args.tickers.split(','):
        data = bt.feeds.YahooFinanceData(dataname=ticker,
                                         fromdate=fromdate, todate=todate)
        cerebro.adddata(data)

    cerebro.addanalyzer(Screener_SMA, period=args.period)
    cerebro.run(runonce=False, stdstats=False, writer=True)

Being the rest about argument parsing mostly.

For 10 days (again shortening the output):

$ ./st-screener.py
===============================================================================
Cerebro:
...
...
...
        - Screener_SMA:
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Params:
            - period: 10
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Analysis:
            - over: (u'NVDA', 63.04, 58.327), (u'AAPL', 108.18, 106.926), (u'YHOO', 42.94, 39.629000000000005), (u'IBM', 161.95, 161.221), (u'ORCL', 41.09, 41.032)
            - under: (u'TSLA', 224.91, 228.423)

Same results. So let’s avoid repeating it for 50 days.

Concluding

Both the btrun from Approach 1 and the small script from Approach 2 use exactly the same analyzer and therefore deliver the same results.

And backtrader has been able to withstand yet another small challenge

Two final notes:

  • Both approaches use the built-in writer functionality to deliver the output.

    • As parameter to btrun with --writer

    • As parameter to cerebro.run with writer=True

  • In both cases runonce has been deactivated. This is to make sure the online data keeps synchronized, because the results could have different lengths (one of the stocks could have traded less)

Script usage

$ ./st-screener.py --help
usage: st-screener.py [-h] [--tickers TICKERS] [--period PERIOD]

SMA Stock Screener

optional arguments:
  -h, --help         show this help message and exit
  --tickers TICKERS  Yahoo Tickers to consider, COMMA separated (default:
                     YHOO,IBM,AAPL,TSLA,ORCL,NVDA)
  --period PERIOD    SMA period (default: 10)

The full script

#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
#
# Copyright (C) 2015, 2016 Daniel Rodriguez
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import argparse
import datetime

import backtrader as bt


class Screener_SMA(bt.Analyzer):
    params = dict(period=10)

    def start(self):
        self.smas = {data: bt.indicators.SMA(data, period=self.p.period)
                     for data in self.datas}

    def stop(self):
        self.rets['over'] = list()
        self.rets['under'] = list()

        for data, sma in self.smas.items():
            node = data._name, data.close[0], sma[0]
            if data > sma:  # if data.close[0] > sma[0]
                self.rets['over'].append(node)
            else:
                self.rets['under'].append(node)


DEFAULTTICKERS = ['YHOO', 'IBM', 'AAPL', 'TSLA', 'ORCL', 'NVDA']


def run(args=None):
    args = parse_args(args)
    todate = datetime.date.today()
    # Get from date from period +X% for weekeends/bank/holidays: let's double
    fromdate = todate - datetime.timedelta(days=args.period * 2)

    cerebro = bt.Cerebro()
    for ticker in args.tickers.split(','):
        data = bt.feeds.YahooFinanceData(dataname=ticker,
                                         fromdate=fromdate, todate=todate)
        cerebro.adddata(data)

    cerebro.addanalyzer(Screener_SMA, period=args.period)
    cerebro.run(runonce=False, stdstats=False, writer=True)


def parse_args(pargs=None):

    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description='SMA Stock Screener')

    parser.add_argument('--tickers', required=False, action='store',
                        default=','.join(DEFAULTTICKERS),
                        help='Yahoo Tickers to consider, COMMA separated')

    parser.add_argument('--period', required=False, action='store',
                        type=int, default=10,
                        help=('SMA period'))

    if pargs is not None:
        return parser.parse_args(pargs)

    return parser.parse_args()


if __name__ == '__main__':
    run()