Python for Finance Cookbook

4.7 (3 reviews total)
By Eryk Lewinson
    Advance your knowledge in tech with a Packt subscription

  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Technical Analysis in Python

About this book

Python is one of the most popular programming languages used in the financial industry, with a huge set of accompanying libraries.

In this book, you'll cover different ways of downloading financial data and preparing it for modeling. You'll calculate popular indicators used in technical analysis, such as Bollinger Bands, MACD, RSI, and backtest automatic trading strategies. Next, you'll cover time series analysis and models, such as exponential smoothing, ARIMA, and GARCH (including multivariate specifications), before exploring the popular CAPM and the Fama-French three-factor model. You'll then discover how to optimize asset allocation and use Monte Carlo simulations for tasks such as calculating the price of American options and estimating the Value at Risk (VaR). In later chapters, you'll work through an entire data science project in the financial domain. You'll also learn how to solve the credit card fraud and default problems using advanced classifiers such as random forest, XGBoost, LightGBM, and stacked models. You'll then be able to tune the hyperparameters of the models and handle class imbalance. Finally, you'll focus on learning how to use deep learning (PyTorch) for approaching financial tasks.

By the end of this book, you’ll have learned how to effectively analyze financial data using a recipe-based approach.

Publication date:
January 2020
Publisher
Packt
Pages
432
ISBN
9781789618518

 

Technical Analysis in Python

In this chapter, we will cover the basics of technical analysis (TA) in Python. In short, TA is a methodology for determining (forecasting) the future direction of asset prices and identifying investment opportunities, based on studying past market data, especially the prices themselves and the traded volume.

We begin by introducing a simple way of visualizing stock prices using the candlestick chart. Then, we show how to calculate selected indicators (with hints on how to calculate others using selected Python libraries) used for TA. Using established Python libraries, we show how easy it is to backtest trading strategies built on the basis of TA indicators. In this way, we can evaluate the performance of these strategies in a real-life context (even including commission fees and so on).

At the end of the chapter, we also demonstrate how to create an interactive dashboard in Jupyter Notebook, which enables us to add and inspect the predefined TA indicators on the fly.

We present the following recipes in this chapter:

  • Creating a candlestick chart
  • Backtesting a strategy based on simple moving average
  • Calculating Bollinger Bands and testing a buy/sell strategy
  • Calculating the relative strength index and testing a long/short strategy
  • Building an interactive dashboard for TA
 

Creating a candlestick chart

A candlestick chart is a type of financial graph, used to describe a given security's price movements. A single candlestick (typically corresponding to one day, but a higher frequency is possible) combines the open, high, low, and close prices (OHLC). The elements of a bullish candlestick (where the close price in a given time period is higher than the open price) are presented in the following image (for a bearish one, we should swap the positions of the open and close prices):

In comparison to the plots introduced in the previous chapter, candlestick charts convey much more information than a simple line plot of the adjusted close price. That is why they are often used in real trading platforms, and traders use them for identifying patterns and making trading decisions.

In this recipe, we also add moving average lines (which are one of the most basic technical indicators), as well as bar charts representing volume.

Getting ready

In this recipe, we download Twitter's (adjusted) stock prices for the year 2018. We use Yahoo Finance to download the data, as described in the Getting data from Yahoo Finance recipe, found in Chapter 1, Financial Data and Preprocessing. Follow these steps:

  1. Import the libraries:
import pandas as pd 
import yfinance as yf
  1. Download the adjusted prices:
df_twtr = yf.download('TWTR', 
start='2018-01-01',
end='2018-12-31',
progress=False,
auto_adjust=True)

For creating the plot, we use the plotly and cufflinks libraries. For more details, please refer to the Visualizing time series data recipe, found in Chapter 1, Financial Data and Preprocessing.

How to do it...

Execute the following steps to create an interactive candlestick chart.

  1. Import the libraries:
import cufflinks as cf
from plotly.offline import iplot, init_notebook_mode

init_notebook_mode()
  1. Create the candlestick chart, using Twitter's stock prices:
qf = cf.QuantFig(df_twtr, title="Twitter's Stock Price", 
                 legend='top', name='TWTR')
  1. Add volume and moving averages to the figure:
qf.add_volume()
qf.add_sma(periods=20, column='Close', color='red')
qf.add_ema(periods=20, color='green')

  1. Display the plot:
qf.iplot()

We can observe the following plot (it is interactive in the notebook):

In the plot, we can see that the exponential moving average (EMA) adapts to the changes in prices much faster than the SMA. Some discontinuities in the chart are caused by the fact that we are using daily data, and there is no data for weekends/bank holidays.

How it works...

In Step 2, we created a QuantFig object by passing a DataFrame containing the input data, as well as some arguments for the title and legend's position. We could have created a simple candlestick chart by running the iplot method of QuantFig immediately afterward.

However, in Step 3, we also added two moving average lines by using the add_sma/add_ema methods. We decided to consider 20 periods (days, in this case). By default, the averages are calculated using the close column, however, we can change this by providing the column argument.

The difference between the two moving averages is that the exponential one puts more weight on recent prices. By doing so, it is more responsive to new information and reacts faster to any changes in the general trend.

See also

 

Backtesting a strategy based on simple moving average

The general idea behind backtesting is to evaluate the performance of a trading strategy—built using some heuristics or technical indicators—by applying it to historical data.

In this recipe, we introduce one of the available frameworks for backtesting in Python: backtrader. Key features of this framework include:

  • A vast amount of available technical indicators (backtrader also provides a wrapper around the popular TA-Lib library) and performance measures
  • Ease of building and applying new indicators
  • Multiple data sources available (including Yahoo Finance, Quandl)
  • Simulating many aspects of real brokers, such as different types of orders (market, limit, stop), slippage (the difference between the intended and actual execution prices of an order), commission, going long/short, and so on
  • A one-line call for a plot, with all results

For this recipe, we consider a basic strategy based on the SMA. The key points of the strategy are as follows:

  • When the close price becomes higher than the 20-day SMA, buy one share.
  • When the close price becomes lower than the 20-day SMA and we have a share, sell it.
  • We can only have a maximum of one share at any given time.
  • No short selling is allowed.

We run the backtesting of this strategy, using Apple's stock prices from the year 2018.

How to do it...

In this example, we present two possible approaches: building a trading strategy, using a signal (bt.Signal) or defining a full strategy (bt.Strategy). Both yield the same results, however, the lengthier one, using bt.Strategy, provides more logging of what is actually happening in the background. This makes it easier to debug and keep track of all operations (the level of detail included in the logging depends on our needs).

Signal

Execute the following steps to create a backtest, using the bt.Signal class.

  1. Import the libraries:
from datetime import datetime
import backtrader as bt
  1. Define a class representing the trading strategy:
class SmaSignal(bt.Signal):
params = (('period', 20), )

def __init__(self):
self.lines.signal = self.data - bt.ind.SMA(period=self.p.period)
  1. Download data from Yahoo Finance:
data = bt.feeds.YahooFinanceData(dataname='AAPL', 
fromdate=datetime(2018, 1, 1),
todate=datetime(2018, 12, 31))
  1. Set up the backtest:
cerebro = bt.Cerebro(stdstats = False)

cerebro.adddata(data)
cerebro.broker.setcash(1000.0)
cerebro.add_signal(bt.SIGNAL_LONG, SmaSignal)
cerebro.addobserver(bt.observers.BuySell)
cerebro.addobserver(bt.observers.Value)
  1. Run the backtest:
print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')
cerebro.run()
print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')
  1. Plot the results:
cerebro.plot(iplot=True, volume=False)

The plot is divided into three parts: the evolution of the portfolio's value, the price of the asset (together with the buy/sell signals), and—lastly—the technical indicator of our choosing, as shown in the following plot:

From the preceding plot, we can see that, in the end, the trading strategy made money: the terminal value of the portfolio is $1011.56.

Strategy

To make the code more readable, we first present the general outline of the class (trading strategy) and then define separate pieces in the following code blocks.

  1. The template of the strategy is presented below:
class SmaStrategy(bt.Strategy):
params = (('ma_period', 20), )

def __init__(self):
# some code

def log(self, txt):
# some code

def notify_order(self, order):
# some code

def notify_trade(self, trade):
# some code

def next(self):
# some code

The __init__ block is defined as:

def __init__(self):
self.data_close = self.datas[0].close

self.order = None
self.price = None
self.comm = None

self.sma = bt.ind.SMA(self.datas[0],
period=self.params.ma_period)

The log block is defined as:

def log(self, txt):
dt = self.datas[0].datetime.date(0).isoformat()
print(f'{dt}, {txt}')

The notify_order block is defined as:

def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return

if order.status in [order.Completed]:
if order.isbuy():
self.log(f'BUY EXECUTED --- Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Commission: {order.executed.comm:.2f}')
self.price = order.executed.price
self.comm = order.executed.comm
else:
self.log(f'SELL EXECUTED --- Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Commission: {order.executed.comm:.2f}')

self.bar_executed = len(self)

elif order.status in [order.Canceled, order.Margin,
order.Rejected]:
self.log('Order Failed')

self.order = None

The notify_trade block is defined as:

def notify_trade(self, trade):
if not trade.isclosed:
return

self.log(f'OPERATION RESULT --- Gross: {trade.pnl:.2f}, Net: {trade.pnlcomm:.2f}')

The next block is defined as:

def next(self):
if self.order:
return

if not self.position:
if self.data_close[0] > self.sma[0]:
self.log(f'BUY CREATED --- Price: {self.data_close[0]:.2f}')
self.order = self.buy()
else:
if self.data_close[0] < self.sma[0]:
self.log(f'SELL CREATED --- Price: {self.data_close[0]:.2f}')
self.order = self.sell()
The code for data is the same as in the signal strategy, so it is not included here, to avoid repetition.
  1. Set up the backtest:
cerebro = bt.Cerebro(stdstats = False)

cerebro.adddata(data)
cerebro.broker.setcash(1000.0)
cerebro.addstrategy(SmaStrategy)
cerebro.addobserver(bt.observers.BuySell)
cerebro.addobserver(bt.observers.Value)
  1. Run the backtest:
print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')
cerebro.run()
print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')
  1. Plot the results:
cerebro.plot(iplot=True, volume=False)

The resulting graph is presented below:

From the preceding graph, we see that the strategy managed to make $11.56 over the year. Additionally, we present a piece of the log:

The log contains information about all the created and executed trades, as well as the operation results, in case it was a sell.

How it works...

The key idea of working with backtrader is that there is the main brain—Cerebro—and by using different methods, we provided it with historical data, the designed trading strategy, additional metrics we wanted to calculate (for example, Portfolio Value over the investment horizon, or the overall Sharpe ratio), information about commissions/slippage, and so on. These were the common elements between the two approaches. The part that differed was the definition of the strategy. We start by describing the common elements of the backtrader framework while assuming a trading strategy already exists, and we then explain the details of the particular strategies.

Common elements

We started with downloading price data from Yahoo Finance, with the help of the bt.feeds.YahooFinanceData() function. What followed was a series of operations connected to Cerebro, as described here:

  1. Creating the instance of bt.Cerebro and setting stdstats = False, in order to suppress a lot of default elements of the plot. Doing so avoided cluttering the output, and then we manually picked the interesting elements (observers and indicators).
  2. Adding data, using the adddata method.
  3. Setting up the amount of available money, using the broker.setcash method.
  4. Adding the signal/strategy, using the add_signal/addstrategy methods.
  5. Adding Observers, using addobserver. We selected two Observers: BuySell, to display the buy/sell decisions on the plot (denoted by blue and red triangles), and Value, for tracking how the portfolio value changed over time.
You can also add data from a CSV file, a pandas DataFrame, Quandl, and other sources. For a list of available options, please refer to bt.feeds.

The last step involved running the backtest with cerebro.run() and displaying the resulting plot with cerebro.plot(). In the latter step, we disabled displaying the volume bar charts, to avoid cluttering the graph.

Signal

The signal was built as a class, inheriting from bt.Signal. The signal was represented as a number—in this case, the difference between the current data point (self.data) and the moving average (bt.ind.SMA). If the signal is positive, it is an indication to go long (buy). A negative one indicates short (selling). The value of 0 means there is no signal.

The next step was to add the signal to Cerebro, using the add_signal method. When doing so, we also had to specify what kind of signal we were adding.

The following is a description of the available signal types:

  • LONGSHORT: This takes into account both long and short indications from the signal.
  • LONG: Positive signals indicate going long; negative ones are used to close the long position.
  • SHORT: Negative signals indicate shorting; positive ones are used to close the short position.
  • LONGEXIT: A negative signal is used to exit a long position.
  • SHORTEXIT: A positive signal is used to exit a short position.

However, exiting positions can be more complex (enabling users to build more sophisticated strategies), as described here:

  • LONG: If there is a LONGEXIT signal, it is used to exit the long position, instead of the default behavior mentioned previously. If there is a SHORT signal and no LONGEXIT signal, the SHORT signal is used to close the long position before opening a short one.
  • SHORT: If there is a SHORTEXIT signal, it is used to exit the short position, instead of the default behavior mentioned previously. If there is a LONG signal and no SHORTEXIT signal, the LONG signal is used to close the short position before opening a long one.
As you might have already realized, the signal is calculated for every time point (as visualized in the bottom of the plot), which effectively creates a continuous stream of positions to be opened/closed (the signal value of 0 is not very likely to happen). That is why backtrader, by default, disables accumulation (the constant opening of new positions, even when we have one already opened) and concurrency (generating new orders without hearing back from the broker whether the previously submitted ones were executed successfully).

Strategy

The strategy was built as a class, inheriting from bt.Strategy. Inside the class, we defined the following methods (we were actually overwriting them to make them tailor-made for our needs):

  • __init__: Here, we defined the objects that we would like to keep track of, for example, close price, order, buy price, commission, indicators such as SMA, and so on.
  • log: This is defined for logging purposes.
  • notify_order: This is defined for reporting the status of the order (position). In general, on day t, the indicator can suggest opening/closing a position based on the close price (assuming we are working with daily data). Then, the (market) order will be carried out on the next day (using day t + 1's open price). However, there is no guarantee that the order will be executed, as it can be canceled, or we might have insufficient cash. This behavior is also true for strategies built with signals. It also removes any pending order, by setting self.order = None.
  • notify_trade: This is defined for reporting the results of trades (after the positions are closed).
  • next: This is the place containing the trading strategy's logic. First, we check whether there is an order already pending, and do nothing if there is. The second check is to see whether we already have a position (enforced by our strategy; not a must), and, if we do not, we check whether the close price is higher than the moving average. A positive outcome results in an entry to the log, and the placing of a buy order self.order = self.buy(). This is also the place where we can choose the stake (number of assets we want to buy). A default outcome in self.buy(size=1).

Here are some general notes:

  • Cerebro should only be used once. If we want to run another backtest, we should create a new instance, not add something to it after prior calculations.
  • The strategy built on bt.Signal inherits from bt.Signal, and uses only one signal. However, we can combine multiple signals, based on different conditions, when we use bt.SignalStrategy instead.
  • When we do not specify otherwise, all trades are carried out on one unit of the asset.
  • backtrader automatically handles the warm-up period. In this case, no trade can be carried out until there are enough data points to calculate the 20-day SMA. When considering multiple indicators at once, backtrader automatically selects the longest necessary period.

There's more...

It is worth mentioning that backtrader has parameter optimization capabilities, which we present in the code that follows. The code is a modified version of the strategy from this recipe, in which we optimize the number of days in the SMA.

The following list provides details of modifications to the code (we only show the relevant ones, as the bulk of the code is identical to that using bt.Strategy):

  • We add an extra attribute called stop to the class definition—it returns the Terminal portfolio value for each parameter:
def stop(self):
self.log(f'(ma_period = {self.params.ma_period:2d}) --- Terminal Value: {self.broker.getvalue():.2f}')
  • Instead of using cerebro.addstrategy(), we use cerebro.optstrategy(), and provide the strategy name and parameter values:
cerebro.optstrategy(SmaStrategy, ma_period=range(10, 31))
  • We increase the number of CPU cores when running the backtesting: cerebro.run(maxcpus=4)

We present the results in the following summary (the order of parameters is not preserved, as the testing was carried out on four cores):

We see that the strategy performed best for ma_period = 22.

See also

Additional resources are available here:

  • https://www.zipline.io/: An alternative framework for backtesting. Developed and actively maintained by Quantopian.
 

Calculating Bollinger Bands and testing a buy/sell strategy

Bollinger Bands are a statistical method, used for deriving information about the prices and volatility of a certain asset over time. To obtain the Bollinger Bands, we need to calculate the moving average and standard deviation of the time series (prices), using a specified window (typically, 20 days). Then, we set the upper/lower bands at K times (typically, 2) the moving standard deviation above/below the moving average.

The interpretation of the bands is quite sample: the bands widen with an increase in volatility and contract with a decrease in volatility.

In this recipe, we build a simple trading strategy, with the following rules:

  • Buy when the price crosses the lower Bollinger Band upwards.
  • Sell (only if stocks are in possession) when the price crosses the upper Bollinger Band downward.
  • All-in strategy—when creating a buy order, buy as many shares as possible.
  • Short selling is not allowed.

We evaluate the strategy on Microsoft's stock in 2018. Additionally, we set the commission to be equal to 0.1%.

How to do it...

Execute the following steps to backtest a strategy based on the Bollinger Bands.

  1. Import the libraries:
import backtrader as bt
import datetime
import pandas as pd
  1. The template of the strategy is presented:
class BBand_Strategy(bt.Strategy):
params = (('period', 20),
('devfactor', 2.0),)

def __init__(self):
# some code

def log(self, txt):
# some code

def notify_order(self, order):
# some code

def notify_trade(self, trade):
# some code

def next_open(self):
# some code

The __init__ block is defined as:

    def __init__(self):
        # keep track of close price in the series
        self.data_close = self.datas[0].close
        self.data_open = self.datas[0].open

        # keep track of pending orders/buy price/buy commission
        self.order = None
        self.price = None
        self.comm = None

        # add Bollinger Bands indicator and track the buy/sell signals
        self.b_band = bt.ind.BollingerBands(self.datas[0], 
                                            period=self.p.period, 
                                            devfactor=self.p.devfactor)
        self.buy_signal = bt.ind.CrossOver(self.datas[0], 
                                           self.b_band.lines.bot)
        self.sell_signal = bt.ind.CrossOver(self.datas[0], 
                                            self.b_band.lines.top)

The log block is defined as:

def log(self, txt):
dt = self.datas[0].datetime.date(0).isoformat()
print(f'{dt}, {txt}')

The notify_order block is defined as:

 def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return

if order.status in [order.Completed]:
if order.isbuy():
self.log(
f'BUY EXECUTED --- Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Commission: {order.executed.comm:.2f}'
)
self.price = order.executed.price
self.comm = order.executed.comm
else:
self.log(
f'SELL EXECUTED --- Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Commission: {order.executed.comm:.2f}'
)

elif order.status in [order.Canceled, order.Margin,
order.Rejected]:
self.log('Order Failed')

self.order = None

The notify_trade block is defined as:

def notify_trade(self, trade):
if not trade.isclosed:
return

self.log(f'OPERATION RESULT --- Gross: {trade.pnl:.2f}, Net: {trade.pnlcomm:.2f}')

The next_open block is defined as:

def next_open(self):
if not self.position:
if self.buy_signal > 0:
size = int(self.broker.getcash() / self.datas[0].open)
self.log(f'BUY CREATED --- Size: {size}, Cash: {self.broker.getcash():.2f}, Open: {self.data_open[0]}, Close: {self.data_close[0]}')
self.buy(size=size)
else:
if self.sell_signal < 0:
self.log(f'SELL CREATED --- Size: {self.position.size}')
self.sell(size=self.position.size)

  1. Download the data:
data = bt.feeds.YahooFinanceData(
dataname='MSFT', fromdate=datetime.datetime(2018, 1, 1), todate=datetime.datetime(2018, 12, 31)
)
  1. Set up the backtest:
cerebro = bt.Cerebro(stdstats = False, cheat_on_open=True)

cerebro.addstrategy(BBand_Strategy)
cerebro.adddata(data)
cerebro.broker.setcash(10000.0)
cerebro.broker.setcommission(commission=0.001)
cerebro.addobserver(bt.observers.BuySell)
cerebro.addobserver(bt.observers.Value)
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='time_return')
  1. Run the backtest:
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
backtest_result = cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
  1. Plot the results:
cerebro.plot(iplot=True, volume=False)

The resulting graph is presented below:

The log is presented below:

We can see that the strategy managed to make money, even after accounting for commission costs. We now turn to an inspection of the analyzers.

  1. Run the following code to investigate different returns metrics:
print(backtest_result[0].analyzers.returns.get_analysis())

The output of the preceding line is as follows:

OrderedDict([('rtot', 0.06155731237239935), 
('ravg', 0.00024622924948959743),
('rnorm', 0.06401530037885826),
('rnorm100', 6.401530037885826)])
  1. Create a plot of daily portfolio returns:
returns_dict = backtest_result[0].analyzers.time_return.get_analysis()
returns_df = pd.DataFrame(list(returns_dict.items()),
columns = ['report_date', 'return']) \
.set_index('report_date')
returns_df.plot(title='Portfolio returns')

Running the code results in the following plot:

The flat lines represent periods when we have no open positions.

How it works...

There are a lot of similarities between the code used for creating the Bollinger Bands-based strategy and that used in the previous recipe. That is why we only discuss the novelties, and refer you to the Backtesting a strategy based on simple moving average recipe for more details.

As we were going all-in in this strategy, we had to use a method called cheat_on_open. This means that we calculated the signals on day t's close price, but calculated the number of shares we wanted to buy based on day t+1's open price. To do so, we had to set cheat_on_open=True when creating the bt.Cerebro object. As a result, we also defined a next_open method instead of next within the Strategy class. This clearly indicated to Cerebro that we were cheating-on-open. Before creating a potential buy order, we calculated size = int(self.broker.getcash() / self.datas[0].open), which is the maximum number of shares we could buy (the open price comes from day t+1). The last novelty was that we also added commission directly to Cerebro by using cerebro.broker.setcommission(commission=0.001).

When calculating the buy/sell signals based on the Bollinger Bands, we used the CrossOver indicator. It returned the following:

  • 1 if the first data (price) crossed the second data (indicator) upward
  • -1 if the first data (price) crossed the second data (indicator) downward


We can also use CrossUp and CrossDown when we want to consider crossing from only one direction. The buy signal would look like this: self.buy_signal = bt.ind.CrossUp(self.datas[0], self.b_band.lines.bot).

The last addition included utilizing analyzers—backtrader objects that help to evaluate what is happening with the portfolio. In the following example, we used two analyzers:

  • Returns: A collection of different logarithmic returns, calculated on the entire timeframe: total compound return, the average return over the entire period, and the annualized return.
  • TimeReturn: A collection of returns over time (using a provided time-frame, in this case, daily data).
We can obtain the same result as from the TimeReturn analyzer by adding an observer with the same name: cerebro.addobserver(bt.observers.TimeReturn). The only difference is that the Observer will be plotted on the main results plot, which is not always desired.
 

Calculating the relative strength index and testing a long/short strategy

The RSI is an indicator that uses the closing prices of an asset to identify oversold/overbought conditions. Most commonly, the RSI is calculated using a 14-day period, and it is measured on a scale from 0 to 100 (it is an oscillator). Traders usually buy an asset when it is oversold (if the RSI is below 30), and sell when it is overbought (if the RSI is above 70). More extreme high/low levels, such as 80-20, are used less frequently and, at the same time, imply stronger momentum.

In this recipe, we build a trading strategy with the following rules:

  • We can go long and short.
  • For calculating the RSI, we use 14 periods (trading days).
  • Enter a long position if the RSI crosses the lower threshold (standard value of 30) upwards; exit the position when the RSI becomes larger than the middle level (value of 50).
  • Enter a short position if the RSI crosses the upper threshold (standard value of 70) downwards; exit the position when the RSI becomes smaller than 50.
  • Only one position can be open at a time.

We evaluate the strategy on Facebook's stock in 2018, and apply a commission of 0.1%.

How to do it...

Execute the following steps to implement a strategy based on the RSI.

  1. Import the libraries:
from datetime import datetime
import backtrader as bt
  1. Define the signal strategy, based on bt.SignalStrategy:
class RsiSignalStrategy(bt.SignalStrategy):
params = dict(rsi_periods=14, rsi_upper=70,
rsi_lower=30, rsi_mid=50)

def __init__(self):

rsi = bt.indicators.RSI(period=self.p.rsi_periods,
upperband=self.p.rsi_upper,
lowerband=self.p.rsi_lower)

bt.talib.RSI(self.data, plotname='TA_RSI')


rsi_signal_long = bt.ind.CrossUp(rsi, self.p.rsi_lower,
plot=False)
self.signal_add(bt.SIGNAL_LONG, rsi_signal_long)
self.signal_add(bt.SIGNAL_LONGEXIT, -(rsi >
self.p.rsi_mid))

rsi_signal_short = -bt.ind.CrossDown(rsi, self.p.rsi_upper,
plot=False)
self.signal_add(bt.SIGNAL_SHORT, rsi_signal_short)
self.signal_add(bt.SIGNAL_SHORTEXIT, rsi < self.p.rsi_mid)
  1. Download the data:
data = bt.feeds.YahooFinanceData(dataname='FB', 
fromdate=datetime(2018, 1, 1),
todate=datetime(2018, 12, 31))
  1. Set up and run the backtest:
cerebro = bt.Cerebro(stdstats = False)

cerebro.addstrategy(RsiSignalStrategy)
cerebro.adddata(data)
cerebro.broker.setcash(1000.0)
cerebro.broker.setcommission(commission=0.001)
cerebro.addobserver(bt.observers.BuySell)
cerebro.addobserver(bt.observers.Value)

cerebro.run()
  1. Plot the results:
cerebro.plot(iplot=True, volume=False)

Running the code results in the following graph:

We look at the triangles in pairs. The first one indicates opening a position (going long if the triangle is blue and facing up; going short if the triangle is red and facing down). The next triangle (of the opposite color and direction) indicates closing a position. We can match the opening and closing of positions with the RSI below the chart. Sometimes, there are multiple triangles of the same color in sequence. That is because the RSI fluctuates around the line of opening a position, crossing it multiple times, as we can see on the preceding RSI chart. But the actual position is only opened on the first instance of a signal (no accumulation is the default setting).

How it works...

In this recipe, we built a trading strategy on top of bt.SignalStrategy. First, we defined the indicator (RSI), with selected arguments. We also added bt.talib.RSI(self.data,plotname='TA_RSI'), just to show that backtrader provides an easy way to use indicators from the popular TA-Lib library (the TA-Lib library must be installed for the code to work). The trading strategy does not depend on this second indicator; it is only plotted for reference, and we could add an arbitrary number of indicators.

Even when adding indicators for reference only, their existence influences the "warm-up period." For example, if we additionally included a 200-day SMA indicator, no trade would be carried out before there exists at least one value for the SMA indicator.

The next step was to define signals. To do so, we used the bt.CrossUp/bt.CrossDown indicators, which returned 1 if the first series (price) crossed the second (upper or lower RSI threshold) from below/above, respectively. For entering a short position, we made the signal negative, by adding a - in front of the bt.CrossDown indicator.

We can disable printing any indicator, by adding plot=False to the function call.

As the last step of defining the strategy, we added tracking of all the signals, by using the signal_add method. For exiting the positions, the conditions we used (an RSI value higher/lower than 50) resulted in a Boolean, which we had to make negative in case of exiting a long position: -True is the same as -1.

Setting up and running the backtest is analogous to the previous recipe, so please refer to it if in doubt regarding any of the steps.

 

Building an interactive dashboard for TA

In this recipe, we show how to build an interactive dashboard for technical analysis in Jupyter Notebook. Of course, the same result could be achieved without any interactivity, by writing the initial code, and then changing the parameter values inline multiple times. However, we believe it is much better to create an interactive tool that can ease the pain, as well as reduce the number of potential mistakes.

In order to do so, we leverage a tool called IPython widgets (ipywidgets), in combination with plotly and cufflinks. We select a few US tech stocks and three indicators (Bollinger Bands, MACD, and RSI) for the dashboard, but this selection can be extended to many more.

Getting ready

After installing the ipywidgets library, we need to run the following line in Terminal to enable the extension:

jupyter nbextension enable --py widgetsnbextension

How to do it...

Execute the following steps to create an interactive dashboard inside Jupyter Notebook.

  1. Import the libraries:
import ipywidgets as wd
import cufflinks as cf
import pandas as pd
import yfinance as yf
from plotly.offline import iplot, init_notebook_mode
from ipywidgets import interact, interact_manual

init_notebook_mode()
  1. Define the possible values for assets and technical indicators:
stocks = ['TWTR', 'MSFT', 'GOOGL', 'FB', 'TSLA', 'AAPL']
indicators = ['Bollinger Bands', 'MACD', 'RSI']
  1. Define a function for creating the interactive plot:
def ta_dashboard(asset, indicator, start_date, end_date, 
bb_k, bb_n, macd_fast, macd_slow, macd_signal,
rsi_periods, rsi_upper, rsi_lower):


df = yf.download(asset,
start=start_date,
end=end_date,
progress=False,
auto_adjust=True)

qf = cf.QuantFig(df, title=f'TA Dashboard - {asset}',
legend='right', name=f'{asset}')

if 'Bollinger Bands' in indicator:
qf.add_bollinger_bands(periods=bb_n,
boll_std=bb_k)
if 'MACD' in indicator:
qf.add_macd(fast_period=macd_fast,
slow_period=macd_slow,
signal_period=macd_signal)
if 'RSI' in indicator:
qf.add_rsi(periods=rsi_periods,
rsi_upper=rsi_upper,
rsi_lower=rsi_lower,
showbands=True)

return qf.iplot()
  1. Define the selectors:
stocks_selector = wd.Dropdown(
options=stocks,
value=stocks[0],
description='Asset'
)

indicator_selector = wd.SelectMultiple(
description='Indicator',
options=indicators,
value=[indicators[0]]
)

start_date_selector = wd.DatePicker(
description='Start Date',
value=pd.to_datetime('2018-01-01'),
continuous_update=False
)

end_date_selector = wd.DatePicker(
description='End Date',
value=pd.to_datetime('2018-12-31'),
continuous_update=False
)
  1. Define a label, and group the selectors inside a container:
main_selector_label = wd.Label('Main parameters', 
layout=wd.Layout(height='45px'))

main_selector_box = wd.VBox(children=[main_selector_label,
stocks_selector,
indicator_selector,
start_date_selector,
end_date_selector])
  1. Define the secondary selectors for the Bollinger Bands:
bb_label = wd.Label('Bollinger Bands')

n_param = wd.IntSlider(value=20, min=1, max=40, step=1,
description='N:', continuous_update=False)

k_param = wd.FloatSlider(value=2, min=0.5, max=4, step=0.5,
description='k:', continuous_update=False)

bollinger_box = wd.VBox(children=[bb_label, n_param, k_param])
  1. Define the secondary selectors for the MACD:
macd_label = wd.Label('MACD')

macd_fast = wd.IntSlider(value=12, min=2, max=50, step=1,
description='Fast avg:',
continuous_update=False)

macd_slow = wd.IntSlider(value=26, min=2, max=50, step=1,
description='Slow avg:',
continuous_update=False)

macd_signal = wd.IntSlider(value=9, min=2, max=50, step=1,
description='MACD signal:',
continuous_update=False)

macd_box = wd.VBox(children=[macd_label, macd_fast,
macd_slow, macd_signal])
  1. Define the secondary selectors for the RSI:
rsi_label = wd.Label('RSI')

rsi_periods = wd.IntSlider(value=14, min=2, max=50, step=1,
description='RSI periods:',
continuous_update=False)

rsi_upper = wd.IntSlider(value=70, min=1, max=100, step=1,
description='Upper Thr:',
continuous_update=False)

rsi_lower = wd.IntSlider(value=30, min=1, max=100, step=1,
description='Lower Thr:',
continuous_update=False)

rsi_box = wd.VBox(children=[rsi_label, rsi_periods,
rsi_upper, rsi_lower])
  1. Create the labels and group the selectors into containers:
sec_selector_label = wd.Label('Secondary parameters', 
layout=wd.Layout(height='45px'))
blank_label = wd.Label('', layout=wd.Layout(height='45px'))

sec_box_1 = wd.VBox([sec_selector_label, bollinger_box, macd_box])
sec_box_2 = wd.VBox([blank_label, rsi_box])

secondary_selector_box = wd.HBox([sec_box_1, sec_box_2])
  1. Group the boxes and prepare the interactive output:
controls_dict = {'asset':stocks_selector, 
'indicator':indicator_selector,
'start_date':start_date_selector,
'end_date':end_date_selector,
'bb_k':k_param,
'bb_n':n_param,
'macd_fast': macd_fast,
'macd_slow': macd_slow,
'macd_signal': macd_signal,
'rsi_periods': rsi_periods,
'rsi_upper': rsi_upper,
'rsi_lower': rsi_lower}

ui = wd.HBox([main_selector_box, secondary_selector_box])
out = wd.interactive_output(ta_dashboard, controls_dict)
  1. Display the dashboard:
display(ui, out)

Running the last line displays the following graphical user interface (GUI):

By selecting values of interest in the GUI, we can influence the interactive chart, for example, by changing the technical indicators we want to display.

This time, we plotted both the Bollinger Bands and the MACD on top of the candlestick chart. Inside of the Notebook, we can zoom in on areas of interest, to further inspect the patterns.

How it works...

After importing the libraries, we defined lists of possible assets (represented by their tickers), and the technical indicators from which to select.

In Step 3, we defined a function called ta_dashboard, which took as input all parameters we made configurable: asset, technical indicators, range of dates, and indicator-specific parameters. The function itself downloaded historical stock prices from Yahoo Finance and used cufflinks to draw a candlestick chart, as we presented in the Creating a candlestick chart recipe. Then, we added additional indicators to the figure, by using methods such as add_bollinger_bands and providing the required arguments. For a list of all supported technical indicators, please refer to the cufflinks documentation.

Having prepared the function, we started defining the elements of the GUI. In Step 4 and Step 5, we defined the main selectors (such as the asset, technical indicators, and start/end dates for downloading the data) and grouped them inside a vertical box (VBox), which serves as storage for smaller elements and makes it easier to design the GUI. To indicate which selectors belonged to a given box, we provided a list of the objects as the children argument.

In Steps 6 to 9, we created the secondary container, this time with all the parameters responsible for tuning the technical indicators. Some general notes about using selectors and boxes are:

  • We can turn off the continuous updating of sliders with continuous_update=False, so the plot only updates when a new value is set, not while moving it around.
  • We can define the default value for a selector by providing the value argument.
  • We can use blank labels (without any text) to align the elements of the boxes.

In Step 10, we used the wd.interactive_output, to indicate that the output of the ta_dashboard function would be modified by the interactive widgets (in a dictionary, we assigned widgets to certain arguments of the function). Lastly, we ran display(ui, out) to display the GUI, which in turn generated the plot.

There's more...

The main advantage of the dashboard presented in this recipe is that it is embedded within Jupyter Notebook. However, we might want to move it outside of the local notebook and make it available for everyone as a web application. To do so, we could use Dash, which is Python's equivalent of R's vastly popular Shiny framework.

About the Author

  • Eryk Lewinson

    Eryk Lewinson received his Master's degree in Quantitative Finance from Erasmus University Rotterdam. In his professional career, he gained experience in the practical application of data science methods while working for two Big 4 companies and a Dutch FinTech scale-up. In his work, he focuses on using machine learning for providing business value to the company. In his free time, he enjoys writing about topics related to data science, playing video games, and traveling with his girlfriend.

    Browse publications by this author

Latest Reviews

(3 reviews total)
The book is comprehensive and obviously written by an author with expertise is the field. The disadvantage is code files are all for Juypter. I used Juypter when taking the first half of Python for Beginners class. I have moved on to PyCharm. I wish he had included .py files that are regular Python files. I had some errors with the code, part of which may be due to the Juypter/PyCharm difference. Also I search for creditcardfraud.csv file and it was not there. Later I noticed creditcardfraud.csv.zip was in one of the chapter files. Please do not zip it so it can be found. It is not that large.
Great overview. Covers a wide area
Great Deals

Recommended For You

Book Title
Unlock this book and the full library for FREE
Start free trial