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
methodFor each data in the system make a Simple Moving Average (
SMA
) for it. -
stop
methodLook which data (
close
if nothing else is specified) is above its sma and store that in a list under the keyover
in the returns (rets
)The member
rets
is standard in analyzers and happens to be acollections.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 default10
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
withwriter=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()