Using Pandas to implement Stop Losses and Take Profits for trading strategy backtests

Backtesting is an important part of any strategy. An essential part of most strategies is having a Stop Loss and a Take Profit. In Python, there are a myriad of backtesting options available to you that allow for this but sometimes simple is better. Today, we’re going to look at how one can implement a Stop Loss and Take Profit using pandas before then using bt to run the full backtest.


  1. Using pandas to implement Stop Loss and Take Profits
  2. Setting weights for bt
  3. Running the backtest

1. Overview of bt

This makes backtesting very simple because we just have to generate a dataframe of weights and bt will run through the weights. What makes it challenging then is setting the weights... and that's where pandas comes into play!

2. Using pandas to prepare the weights (with SL and TP implementation!)

To make things clear, the logic of the strategy is as follows:

  1. If RSI > 50 and MACD > 0: Buy Signal
  2. If RSI < 50 and MACD < 0: Sell Signal
  3. Once we enter a position, the SL will be 0.97x(entry price) and the TP will be 1.5x(entry price).

I used Tiingo to get my historical data but you can use your data provider of choice! For this backtest, we are only going to use three stocks (but once you implement this, you can use as many stocks as you want)

Once we have our data, we now need to write a function that would allow us to set our target weights (as per the dataframe shown previously). The code may seem long and tricky but its really simple once you think about it.

The first thing we do is initialize our indicators and create another column to input their values. Next, we create a number of columns that will allow us to keep track of our positions — we have our buy_signal, our sell_signal, our SL and TP, a column to show whether we are in a Position or not, and finally our target weights (TW).

The next section of the function starts with asking if there is a position open. We use [i-1] as a reference because we need to refer to the row above in order to determine if a position is currently open. If there is no position open, we then issue our buy and sell orders! (I'm aware that selling short should technically be a 'sell signal' but for simplicity, as long as we're opening a new position, we call it a 'buy_signal')

We then set our positions to either 1 (for long positions) or -1 (for short positions). We calculate our SL and TP and also set those.

Of course, if there is no signal, we leave position as 0 (for no position)

The next block is important because it sets the logic that allows the SL and TP to be used. It checks if the position is a long or short position and then sets the current SL and TP to the preceeding one. This allows the SL and TP to be persistent. After setting the SL and TP for the current line, we check if the price of the ticker is below or above the SL and TP (respectively, for long positions and vice versa for short positions).

If the price has crossed the SL or TP, we set our positions to 0 (i.e. exit our positions) and then set the sell_signal to 1 (indicating a sell)

Whew! We’re at the end of the function! Finally, in the last part of the function, we calculate the TW for our positions. Its simply a matter of referring to our positions column — if the position is a long column, we set the weight to 1/len(datalist) and if short the negative of that (under the assumption we are going to balance the portfolio equally). Of coure, if no position is open, we set the TW to 0 (which will also trigger a close of the position if there was previously an open position).

3. Settings weights for bt

I also clean up the “data” dataframe, removing all the unnecessary columns that we created and simply retaining the stock price data.

4. Running the backtest

Test1 0% [############################# ] 100% | ETA: 00:00:00
Benchmark 0% [############################# ] 100% | ETA: 00:00:00
Stat                Test1       Benchmark 
------------------- ---------- -----------
Start 2015-02-01 2015-02-01
End 2020-10-09 2020-10-09
Risk-free rate 0.10% 0.10%
Total Return 189.47% 424.46%
Daily Sharpe 0.93 1.14
Daily Sortino 1.49 1.88
CAGR 20.55% 33.83%
Max Drawdown -30.97% -34.69%

So, obviously NOT a market-beating strategy but not too terrible if I do say so myself.

In this article, we saw how we can work within pandas to create a strategy that has bracket orders of SL and TP. We then used bt to run the strategy. That’s all for now. Thanks for reading and let me know if you have any questions!

Hi! I’m learning to explore data and think about personal finance (not always in that order)

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store