Basic Algo-Trading Part 2 - Backtrading

Welcome back to part 2 of my series on basic algo-trading! In this post, I actually use the prediction function we created previously to create a strategy and backtrade it on a mainstream stock! Let’s dive in…

For backtrading, we could potentially use the pandas data source from the last part but there’s something even better. Thanks to the beautiful world of open source software, we can use the backtrader python library created by Daniel Rodriguez.

From its site, backtrader is…

A feature-rich Python framework for backtesting and trading. Backtrader allows you to focus on writing reusable trading strategies, indicators and analyzers instead of having to spend time building infrastructure.

Sound like exactly what we need.

Backtrader has a very impressive range of functionality, but for this example, we going to stick to the most important features that we need to test our predictions.

First, we are going to need to create a strategy. The most important function in our strategy will be the next function. The next function is called when Backtrader makes one step through the data. For example, if you have minute-to-minute prices, each call of next will represent the passing of one minute. Since we are going to use Yahoo Finance API data, each call of our next function will represent the passing of one day.

Within next we will do some basic sanity checks before making a prediction. Next comes our basic strategy.

You will notice what was make_predictions has now become get_prophet_moves. Although the name is now hopefully more intuitive, it still has the functionality.

In our strategy, we will do a dead simple analysis:

if the predicted movement is mostly positive: buy

if the predicted movement is mostly negative: sell

Anyone with experience will cringe at this but it is quick and easy to implement for an example.

classTestStrategy(bt.Strategy):deflog(self,txt,dt=None):''' Logging function fot this strategy'''dt=dtorself.datas[0].datetime.date(0)print('%s, %s'%(dt.isoformat(),txt))def__init__(self):# keep array of dates and closesself.date_array=[]self.close_array=[]# To keep track of pending orders and buy price/commissionself.order=Noneself.buyprice=Noneself.buycomm=Nonedefnotify_order(self,order):iforder.statusin[order.Submitted,order.Accepted]:# Buy/Sell order submitted/accepted to/by broker - Nothing to doreturn# Check if an order has been completed# Attention: broker could reject order if not enough cashiforder.statusin[order.Completed]:iforder.isbuy():self.log('BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f'%(order.executed.price,order.executed.value,order.executed.comm))self.buyprice=order.executed.priceself.buycomm=order.executed.commelse:# Sellself.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f'%(order.executed.price,order.executed.value,order.executed.comm))self.bar_executed=len(self)eliforder.statusin[order.Canceled,order.Margin,order.Rejected]:self.log('Order Canceled/Margin/Rejected')self.order=Nonedefnotify_trade(self,trade):ifnottrade.isclosed:returnself.log('OPERATION PROFIT, GROSS %.2f, NET %.2f'%(trade.pnl,trade.pnlcomm))defnext(self):# Simply log the closing price of the series from the referenceself.log('Close, %.2f'%self.datas[0].close[0])# append date and close to arraysself.date_array.append(self.datas[0].datetime.date(0))self.close_array.append(self.datas[0].close[0])# Check if an order is pending ... if yes, we cannot send a 2nd oneifself.order:return# make sure we have a decent amount of dataiflen(self.date_array)<90:return# only invest once a weekiflen(self.date_array)%5!=0:return# get predictionsmax_move,expected_move,min_move=self.get_prophet_moves(7,False)# if the predicted movement is mostly positive, buyifmax_move>0andabs(max_move)>abs(min_move):self.log('BUY CREATE, %.2f'%self.datas[0].close[0])# Keep track of the created order to avoid a 2nd orderself.order=self.buy()# if the predicted movement is mostly negative, sellelifmin_move<0andabs(min_move)>abs(max_move):# make sure we have some stock to sellifself.position:self.log('SELL CREATE, %.2f'%self.datas[0].close[0])# Keep track of the created order to avoid a 2nd orderself.order=self.sell()defget_prophet_moves(self,daysOut=7,showCharts=False):# create stock dataframe for prophetstock_df=pd.DataFrame({'ds':self.date_array,'y':self.close_array})# fit data using prophet modelm=Prophet()m.fit(stock_df)# create future datesfuture_prices=m.make_future_dataframe(periods=365)# predict pricesforecast=m.predict(future_prices)# view resultsifshowCharts:fig=m.plot(forecast)ax1=fig.add_subplot(111)ax1.set_title("Stock Price Forecast",fontsize=16)ax1.set_xlabel("Date",fontsize=12)ax1.set_ylabel("Close Price",fontsize=12)fig2=m.plot_components(forecast)plt.show()# calculate predicted returnsend_of_period=self.datas[0].datetime.date(0)+dt.timedelta(days=daysOut)future_close_max=forecast.loc[forecast.ds>end_of_period].iloc[0].yhat_upperfuture_close_expected=forecast.loc[forecast.ds>end_of_period].iloc[0].yhatfuture_close_min=forecast.loc[forecast.ds>end_of_period].iloc[0].yhat_lower# calculate percent changes based on predictionsmax_move=(future_close_max-self.datas[0].close[0])/self.datas[0].close[0]expected_move=(future_close_expected-self.datas[0].close[0])/self.datas[0].close[0]min_move=(future_close_min-self.datas[0].close[0])/self.datas[0].close[0]return(max_move,expected_move,min_move)

Next comes the fun part…backtrading!

Hopefully, the comments are explanatory enough but I just want to take the time to commend the dev/team behind Backtrader because the library is very readible.

You’ll notice that we are going to backtrade this strategy on Wal-Mart (WMT). Although not the most seasonal stock, it’s likely affected by shoppers’ habits during the year. For our date range, we’ll start the algorithm ten years ago from August 1st. Being casual investors, we’ll start with a modest $1000. Finally, for our own sake, we’ll pretend Robinhood existed ten years ago and set the commision for our broker to 0%.

It really is as simple as the code below. You can change a wide range of parameters easily and test your algorithm in a variety of situations.

# Create a cerebro entitycerebro=bt.Cerebro()# Add a strategycerebro.addstrategy(TestStrategy)# Create a Data Feeddata=bt.feeds.YahooFinanceData(dataname='WMT',fromdate=dt.datetime(2008,8,1),todate=dt.datetime(2018,8,1),reverse=False)cerebro.adddata(data)# Set our desired cash startcerebro.broker.setcash(1000.0)# Add the Data Feed to Cerebro# Add a FixedSize sizer according to the stakecerebro.addsizer(bt.sizers.FixedSize,stake=1)# Set the commissioncerebro.broker.setcommission(commission=0.0)# Print out the starting conditionsprint('Starting Portfolio Value: %.2f'%cerebro.broker.getvalue())# Run over everythingcerebro.run()# Print out the final resultprint('Final Portfolio Value: %.2f'%cerebro.broker.getvalue())

Conclusion

Wow, we have an ending porfolio value of $1301.42! I’ll have to admit that I’m a bit surprised. Although our gains probably didn’t beat inflation or compensate for brokerage fees, with a lot of further investigation and tweaking, this algorithm might be viable.

But if we step back, I think this simple example is important for a greater reason. With algo-trading, we can see the power of using data to your advantage. Instead of taking financial risk and using an algorithm right away, you can look back in time and see how it would’ve preformed. Today’s market is of course always different than yesterday’s, but backtrading can give you meaningful insight into whether or not you should go forward with using an algorithm.

The next step will be to move to paper trading in order to see how well your algorithm would perform if you were to deploy it. Part 3?

Disclaimer

The above references an opinion and is for information purposes only. It is not intended to be investment advice. Seek a duly licensed professional for investment advice.