Photo by Ryan Stone on Unsplash

Welcome to part two of this two-part series on how to create a trading strategy using Backtrader and Alpaca. In part one, we discussed how to connect Alpaca and Backtrader. In this post, we'll be building on our previous code and create an RSI stack strategy.

Alpaca & Backtrader: Tools of the Trade (Part 1)
RSI stack strategy w/ Alpaca and Backtrader. In #1, we’ll cover connecting the Backtrader backtesting to Alpaca to load in data for multiple time frames.

If we haven't met yet, my name is Leo Smigel, and I write about algorithmic trading and investing at Analyzing Alpha. With introductions out of the way and Backtrader receiving data from Alpaca, let's create that RSI stack.

Also, before I forget, all of the code will be on the Analyzing Alpha GitHub Repo.


Create the Strategy

Creating our RSI Stack strategy is relatively easy. We create our RSIStack class by inheriting all of the functionality from backtrader.strategy. We then set the parameters for our strategy in the params dictionary. The parameters dictionary is part of the Backtrader framework and makes our code more readable and maintainable. For our strategy, we set our overbought/oversold thresholds based on the conventional RSI settings, and we set our risk-to-reward ratio to 2. While this will work for our simple strategy, for a production strategy, you'll want to use market structure to determine the optimal risk-to-reward ratio.

Risk-Reward Ratio: Defined & Determined
The risk-reward ratio is the ratio between the value at risk and the profit target. It measures the potential profit for every dollar risked. For example, if you buy a stock for $10 with a profit target of $12 and a stop-loss at $9, the risk-reward ratio is 1:2 because you’re risking $1 to make $2.
class RSIStack(bt.Strategy):
    params = dict(
        rsi_overbought=70,
        rsi_oversold=30,
        rrr=2
    )

With our derived strategy class created, we can now initialize a few attributes and create our indicators in __init__. __init__ preprocesses our data to prepare it for later use, making backtesting faster.  Let's create an attribute to hold our 'alive' orders and our indicators. We create our RSI indicators on every data/timeframe and only create our ATR indicator on the timeframe we're using for position sizing.

def __init__(self):
        self.orefs = None
        self.inds = {}
        for d in self.datas:
            self.inds[d] = {}
            self.inds[d]['rsi'] = bt.ind.RSI(d)
            self.inds[d]['rsiob'] = self.inds[d]['rsi'] >= self.p.rsi_overbought
            self.inds[d]['rsios'] = self.inds[d]['rsi'] <= self.p.rsi_oversold
        for i in range(len(timeframes)-1, len(self.datas), len(timeframes)):
            self.inds[self.datas[i]]['atr'] = bt.ind.ATR(self.datas[i])

With the initialization out of the way, it's time to start the strategy. Start enables us to run code before next processes each bar. For our strategy, we use it to record the length of the lowest timeframe. Remember, the RSI stack requires all timeframes to be oversold or overbought. We need to reset the stack after each bar passes on the lowest timeframe.

 def start(self):
        # Timeframes must be entered from the highest to the lowest frequency.
        # Getting the length of the lowest frequency timeframe will
        # show us how many periods have passed
        self.lenlowtframe = len(self.datas[-1])
        self.stacks = {}

Now for  next  This is where the bulk of the action happens as we process each bar.

First, we determine if the period has changed by saving the length of the lowest timeframe bar and checking it against the current length of the lowest timeframe bar.

def next(self):
        # Reset all of the stacks if a bar has passed on our
        # lowest frequency timeframe
        if not self.lenlowtframe == len(self.datas[-1]):
            self.lenlowtframe += 1
            self.stacks = {}

Second, we determine if there is an RSI stack by iterating through all of the data feeds (datas) for each ticker and incrementing our stacks dictionary by one for each oversold/overbought condition. For this, we use the modulo operator and the timeframe length to determine when we're on a new ticker.

for i, d in enumerate(self.datas):
        	# Create a dictionary for each new symbol.
        	ticker = d.p.dataname
        	if i % len(timeframes) == 0:
            	self.stacks[ticker] = {}
            	self.stacks[ticker]['rsiob'] = 0
            	self.stacks[ticker]['rsios'] = 0
        	if i % len(timeframes) == len(timeframes) -1:
            	self.stacks[ticker]['data'] = d
        	self.stacks[ticker]['rsiob'] += self.inds[d]['rsiob'][0]
        	self.stacks[ticker]['rsios'] += self.inds[d]['rsios'][0]

We then delete any dictionary entry where an RSI stack isn't found.

for k,v in list(self.stacks.items()):
            if v['rsiob'] < len(timeframes) and v['rsios'] < len(timeframes):
                del self.stacks[k]

Now that we have our stacks, we're ready to trade.  We only want to enter a position if there are dollars to deploy. In our case, if there aren't any existing positions, open orders and a stack is found. Then it's just a matter of buying/selling depending on if we’re extended to the downside/upside.

# Check if there are any stacks from the previous period
# And buy/sell stocks if there are no existing positions or open orders
positions = [d for d, pos in self.getpositions().items() if pos]
    if self.stacks and not positions and not self.orefs:
        for k,v in self.stacks.items():
                    d = v['data']
                    size = self.broker.get_cash() // d
                    if v['rsiob'] == len(timeframes) and \
                                     d.close[0] < d.close[-1]:
                        print(f"{d.p.dataname} overbought")
                        risk = d + self.inds[d]['atr'][0]
                        reward = d - self.inds[d]['atr'][0] * self.p.rrr
                        os = self.sell_bracket(data=d,
                                               price=d.close[0],
                                               size=size,
                                               stopprice=risk,
                                               limitprice=reward)
                        self.orefs = [o.ref for o in os]
                    elif v['rsios'] == len(timeframes) and d.close[0] > d.close[-1]:
                        print(f"{d.p.dataname} oversold")
                        risk = d - self.inds[d]['atr'][0]
                        reward = d + self.inds[d]['atr'][0] * self.p.rrr
                        os = self.buy_bracket(data=d,
                                              price=d.close[0],
                                              size=size,
                                              stopprice=risk,
                                              limitprice=reward)
                        self.orefs = [o.ref for o in os]

Finally, we use notify_order to print out our orders and to manage the self.orefs list attribute.

def notify_order(self, order):
        self.log(f'Order - {order.getordername()} {order.ordtypename()} {order.getstatusname()} for {order.size} shares @ ${order.price:.2f}')

        if not order.alive() and order.ref in self.orefs:
            self.orefs.remove(order.ref)

With our strategy class complete, we run cerebro and check out our results. Looking at the console output and the matplotlib figure shows what we expect -- it appears we have a profit for the period.

...
SPY overbought
2020-08-05T15:15:00: Order - Limit Sell Submitted for -306.0 shares @ $331.65
2020-08-05T15:15:00: Order - Stop Buy Submitted for 306.0 shares @ $332.80
2020-08-05T15:15:00: Order - Limit Buy Submitted for 306.0 shares @ $329.37
2020-08-05T15:15:00: Order - Limit Sell Accepted for -306.0 shares @ $331.65
2020-08-05T15:15:00: Order - Stop Buy Accepted for 306.0 shares @ $332.80
2020-08-05T15:15:00: Order - Limit Buy Accepted for 306.0 shares @ $329.37
2020-08-05T15:15:00: Order - Limit Sell Completed for -306.0 shares @ $331.65
2020-08-06T17:00:00: Order - Stop Buy Completed for 306.0 shares @ $332.80
2020-08-06T17:00:00: Order - Limit Buy Canceled for 306.0 shares @ $329.37
Trade PNL: $-286.15
Final Portfolio Value: 101353.63

So what's next?

A lot ;). I do use an RSI stack as part of an indicator. Still, without divulging all of my secrets but pointing aspiring algorithmic traders in the right direction, perhaps you could combine RSI stacks with the order book looking for potential stock trades playing off of longer timeframe support/resistance in the direction of the ETF that the stock is a member of with SPY’s support...

You'll also want to convert this code into a custom indicator, create a sizing algorithm to trade multiple assets, and make sure you've got a statistically significant backtest with a strong rationale behind it.

And with that being said, you’re still just getting started. You have to love the journey if you want to become an algorithmic trader.

Algorithmic Trading: Is It Worth It?
Only one in five day traders is profitable. Algorithmic trading improves these odds through better strategy design, backtesting, and execution.

That's it for this series, but don't worry, there are at least two series to go.

If you enjoy learning about algorithmic trading, please check out my website where I write about all things quantamental and continue learning about my favorite Python trading tools -- Alpaca and Backtrader.

Quantamental: What It Is & Why It Works
Quantamental refers to an investment strategy that combines quantitative approaches using computers, mathematical models, and big data with fundamental methods that analyze individual company cash flows, growth, and risk to generate better risk-adjusted returns.
The Top 22 Python Trading Tools for 2020
Rapid increases in technology availability have put systematic and algorithmic trading in reach for the retail trader. Below you’ll find a curated list of trading platforms, data providers, broker-dealers, return analyzers, and other useful trading libraries for aspiring Python traders.

Again all of the code will be on the Analyzing Alpha GitHub Repo.

leosmigel/analyzingalpha
Contribute to leosmigel/analyzingalpha development by creating an account on GitHub.

*Keep in mind that all investments involve risk and the past performance of a security or financial product does not guarantee future results or returns.

Follow @AlpacaHQ on Twitter!

Brokerage services are provided by Alpaca Securities LLC ("Alpaca"), member FINRA/SIPC, a wholly-owned subsidiary of AlpacaDB, Inc. Technology and services are offered by AlpacaDB, Inc.