Quickstart
Note
The data files used in the quickstart guide are updated from time to time,which means that theadjusted close changes and with it theclose (andthe other components). That means that the actual output may be differentto what was put in the documentation at the time of writing.
Using the platform
Let’s run through a series of examples (from almost an empty one to a fullyfledged strategy) but not without before roughly explaining 2 basic conceptswhen working withbacktrader
Lines
Data Feeds, Indicators and Strategies havelines.
A line is a succession of points that when joined together form this line. When talking about the markets, a Data Feed has usually the following set of points per day:
- Open, High, Low, Close, Volume, OpenInterest
The series of “Open”s along time is a Line. And therefore a Data Feed has usually 6 lines.
If we also consider “DateTime” (which is the actual reference for a single point), we could count 7 lines.
Index 0 Approach
When accessing the values in a line, the current value is accessed with index:0
And the “last” output value is accessed with-1. This in line with Python conventions for iterables (and a line can be iterated and is therefore an iterable) where index-1 is used to access the “last” item of the iterable/array.
In our case is the lastoutput value what’s getting accessed.
As such and being index0 right after-1, it is used to access the current moment in line.
With that in mind and if we imagine a Strategy featuring a Simple Movingaverage created during initialization:
self.sma=SimpleMovingAverage(.....)
The easiest and simplest way to access the current value of this moving average:
There is no need to know how many bars/minutes/days/months have been processed,because “0” uniquely identifies the current instant.
Following pythonic tradition, the “last” output value is accessed using-1:
previous_value=self.sma[-1]
Of course earlier output values can be accessed with -2, -3, …
From 0 to 100: the samples
Basic Setup
Let’s get running.
from__future__import(absolute_import,division,print_function,unicode_literals)importbacktraderasbtif__name__=='__main__':cerebro=bt.Cerebro()print('Starting Portfolio Value:%.2f'%cerebro.broker.getvalue())cerebro.run()print('Final Portfolio Value:%.2f'%cerebro.broker.getvalue())
After the execution the output is:
Starting Portfolio Value: 10000.00Final Portfolio Value: 10000.00
In this example:
backtrader was imported
The Cerebro engine was instantiated
The resultingcerebro instance was told torun (loop over data)
And the resulting outcome was printed out
Although it doesn’t seem much, let’s point out something explicitly shown:
This behind the scenes broker instantiation is a constant trait in the platformto simplify the life of the user. If no broker is set by the user, a default oneis put in place.
And 10K monetary units is a usual value with some brokers to begin with.
Setting the Cash
In the world of finance, for sure only “losers” start with 10k. Let’s change thecash and run the example again.
from__future__import(absolute_import,division,print_function,unicode_literals)importbacktraderasbtif__name__=='__main__':cerebro=bt.Cerebro()cerebro.broker.setcash(100000.0)print('Starting Portfolio Value:%.2f'%cerebro.broker.getvalue())cerebro.run()print('Final Portfolio Value:%.2f'%cerebro.broker.getvalue())
After the execution the output is:
Starting Portfolio Value: 1000000.00Final Portfolio Value: 1000000.00
Mission accomplished. Let’s move to tempestuous waters.
Adding a Data Feed
Having cash is fun, but the purpose behind all this is to let an automatedstrategy multiply the cash without moving a finger by operating on an assetwhich we see as aData Feed
Ergo … NoData Feed ->No Fun. Let’s add one to the ever growingexample.
from__future__import(absolute_import,division,print_function,unicode_literals)importdatetime# For datetime objectsimportos.path# To manage pathsimportsys# To find out the script name (in argv[0])# Import the backtrader platformimportbacktraderasbtif__name__=='__main__':# Create a cerebro entitycerebro=bt.Cerebro()# Datas are in a subfolder of the samples. Need to find where the script is# because it could have been called from anywheremodpath=os.path.dirname(os.path.abspath(sys.argv[0]))datapath=os.path.join(modpath,'../../datas/orcl-1995-2014.txt')# Create a Data Feeddata=bt.feeds.YahooFinanceCSVData(dataname=datapath,# Do not pass values before this datefromdate=datetime.datetime(2000,1,1),# Do not pass values after this datetodate=datetime.datetime(2000,12,31),reverse=False)# Add the Data Feed to Cerebrocerebro.adddata(data)# Set our desired cash startcerebro.broker.setcash(100000.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())
After the execution the output is:
Starting Portfolio Value: 1000000.00Final Portfolio Value: 1000000.00
The amount of boilerplate has grown slightly, because we added:
Aside from that, theData Feed is created and added tocerebro.
The output has not changed and it would be a miracle if it had.
Note
Yahoo Online sends the CSV data in date descending order, which is notthe standard convention. Thereversed=True prameter takes intoaccount that the CSV data in the file has already beenreversedand has the standard expected date ascending order.
Our First Strategy
The cash is in thebroker and theData Feed is there. It seems like riskybusiness is just around the corner.
Let’s put a Strategy into the equation and print the “Close” price of each day(bar).
DataSeries (the underlying class inData Feeds) objects have aliases toaccess the well known OHLC (Open High Low Close) daily values. This should easeup the creation of our printing logic.
from__future__import(absolute_import,division,print_function,unicode_literals)importdatetime# For datetime objectsimportos.path# To manage pathsimportsys# To find out the script name (in argv[0])# Import the backtrader platformimportbacktraderasbt# Create a StrateyclassTestStrategy(bt.Strategy):deflog(self,txt,dt=None):''' Logging function for this strategy'''dt=dtorself.datas[0].datetime.date(0)print('%s,%s'%(dt.isoformat(),txt))def__init__(self):# Keep a reference to the "close" line in the data[0] dataseriesself.dataclose=self.datas[0].closedefnext(self):# Simply log the closing price of the series from the referenceself.log('Close,%.2f'%self.dataclose[0])if__name__=='__main__':# Create a cerebro entitycerebro=bt.Cerebro()# Add a strategycerebro.addstrategy(TestStrategy)# Datas are in a subfolder of the samples. Need to find where the script is# because it could have been called from anywheremodpath=os.path.dirname(os.path.abspath(sys.argv[0]))datapath=os.path.join(modpath,'../../datas/orcl-1995-2014.txt')# Create a Data Feeddata=bt.feeds.YahooFinanceCSVData(dataname=datapath,# Do not pass values before this datefromdate=datetime.datetime(2000,1,1),# Do not pass values before this datetodate=datetime.datetime(2000,12,31),# Do not pass values after this datereverse=False)# Add the Data Feed to Cerebrocerebro.adddata(data)# Set our desired cash startcerebro.broker.setcash(100000.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())
After the execution the output is:
Starting Portfolio Value: 100000.002000-01-03T00:00:00, Close, 27.852000-01-04T00:00:00, Close, 25.392000-01-05T00:00:00, Close, 24.05.........2000-12-26T00:00:00, Close, 29.172000-12-27T00:00:00, Close, 28.942000-12-28T00:00:00, Close, 29.292000-12-29T00:00:00, Close, 27.41Final Portfolio Value: 100000.00
Someone said the stockmarket was risky business, but it doesn’t seem so.
Let’s explain some of the magic:
Uponinit being called the strategy already has a list of datas that are present in the platform
This is a standard Pythonlist and datas can be accessed in the order theywere inserted.
The first data in the list self.datas[0] is the default data for tradingoperations and to keep all strategy elements synchronized (it’s the systemclock)
self.dataclose = self.datas[0].close keeps a reference to theclose line. Only one level of indirection is later needed to access the close values.
The strategy next method will be called on each bar of the system clock (self.datas[0]). This is true until other things come into play likeindicators, which need some bars to start producing an output. More on that later.
Adding some Logic to the Strategy
Let’s try some crazy idea we had by looking at some charts
- If the price has been falling 3 sessions in a row … BUY BUY BUY!!!
from__future__import(absolute_import,division,print_function,unicode_literals)importdatetime# For datetime objectsimportos.path# To manage pathsimportsys# To find out the script name (in argv[0])# Import the backtrader platformimportbacktraderasbt# Create a StrateyclassTestStrategy(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 a reference to the "close" line in the data[0] dataseriesself.dataclose=self.datas[0].closedefnext(self):# Simply log the closing price of the series from the referenceself.log('Close,%.2f'%self.dataclose[0])ifself.dataclose[0]<self.dataclose[-1]:# current close less than previous closeifself.dataclose[-1]<self.dataclose[-2]:# previous close less than the previous close# BUY, BUY, BUY!!! (with all possible default parameters)self.log('BUY CREATE,%.2f'%self.dataclose[0])self.buy()if__name__=='__main__':# Create a cerebro entitycerebro=bt.Cerebro()# Add a strategycerebro.addstrategy(TestStrategy)# Datas are in a subfolder of the samples. Need to find where the script is# because it could have been called from anywheremodpath=os.path.dirname(os.path.abspath(sys.argv[0]))datapath=os.path.join(modpath,'../../datas/orcl-1995-2014.txt')# Create a Data Feeddata=bt.feeds.YahooFinanceCSVData(dataname=datapath,# Do not pass values before this datefromdate=datetime.datetime(2000,1,1),# Do not pass values before this datetodate=datetime.datetime(2000,12,31),# Do not pass values after this datereverse=False)# Add the Data Feed to Cerebrocerebro.adddata(data)# Set our desired cash startcerebro.broker.setcash(100000.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())
After the execution the output is:
Starting Portfolio Value: 100000.002000-01-03, Close, 27.852000-01-04, Close, 25.392000-01-05, Close, 24.052000-01-05, BUY CREATE, 24.052000-01-06, Close, 22.632000-01-06, BUY CREATE, 22.632000-01-07, Close, 24.37.........2000-12-20, BUY CREATE, 26.882000-12-21, Close, 27.822000-12-22, Close, 30.062000-12-26, Close, 29.172000-12-27, Close, 28.942000-12-27, BUY CREATE, 28.942000-12-28, Close, 29.292000-12-29, Close, 27.41Final Portfolio Value: 99725.08
Several “BUY” creation orders were issued, our porftolio value wasdecremented. A couple of important things are clearly missing.
The order was created but it is unknown if it was executed, when and at what price.
The next example will build upon that by listening to notifications of orderstatus.
The curious reader may ask how many shares are being bought, what asset is beingbought and how are orders being executed. Where possible (and in this case it is)the platform fills in the gaps:
self.datas[0] (the main data aka system clock) is the target asset if no other one is specified
The stake is provided behind the scenes by aposition sizer which uses a fixed stake, being the default “1”. It will be modified in a later example
The order is executed “At Market”. The broker (shown in previous examples) executes this using the opening price of the next bar, because that’s the 1st tick after the current under examination bar.
The order is executed so far without any commission (more on that later)
Do not only buy … but SELL
After knowing how to enter the market (long), an “exit concept” is needed andalso understanding whether the strategy is in the market.
Luckily a Strategy object offers access to aposition attribute for the defaultdata feed
Methodsbuy andsell return thecreated (not yet executed) order
Changes in orders’ status will be notified to the strategy via anotify method
The“exit concept” will be an easy one:
Exit after 5 bars (on the 6th bar) have elapsed for good or for worse
Please notice that there is no “time” or “timeframe” implied: number ofbars. The bars can represent 1 minute, 1 hour, 1 day, 1 week or any othertime period.
Although we know the data source is a daily one, the strategy makes noassumption about that.
Additionally and to simplify:
- Do only allow a Buy order if not yet in the market
Note
Thenext method gets no “bar index” passed and therefore it seemsobscure how to understand when 5 bars may have elapsed, but this hasbeen modeled in pythonic way: calllen on an object and it will tellyou the length of itslines. Just write down (save in a variable) atwhich length in an operation took place and see if the current lengthis 5 bars away.
from__future__import(absolute_import,division,print_function,unicode_literals)importdatetime# For datetime objectsimportos.path# To manage pathsimportsys# To find out the script name (in argv[0])# Import the backtrader platformimportbacktraderasbt# Create a StrateyclassTestStrategy(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 a reference to the "close" line in the data[0] dataseriesself.dataclose=self.datas[0].close# To keep track of pending ordersself.order=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,%.2f'%order.executed.price)eliforder.issell():self.log('SELL EXECUTED,%.2f'%order.executed.price)self.bar_executed=len(self)eliforder.statusin[order.Canceled,order.Margin,order.Rejected]:self.log('Order Canceled/Margin/Rejected')# Write down: no pending orderself.order=Nonedefnext(self):# Simply log the closing price of the series from the referenceself.log('Close,%.2f'%self.dataclose[0])# Check if an order is pending ... if yes, we cannot send a 2nd oneifself.order:return# Check if we are in the marketifnotself.position:# Not yet ... we MIGHT BUY if ...ifself.dataclose[0]<self.dataclose[-1]:# current close less than previous closeifself.dataclose[-1]<self.dataclose[-2]:# previous close less than the previous close# BUY, BUY, BUY!!! (with default parameters)self.log('BUY CREATE,%.2f'%self.dataclose[0])# Keep track of the created order to avoid a 2nd orderself.order=self.buy()else:# Already in the market ... we might selliflen(self)>=(self.bar_executed+5):# SELL, SELL, SELL!!! (with all possible default parameters)self.log('SELL CREATE,%.2f'%self.dataclose[0])# Keep track of the created order to avoid a 2nd orderself.order=self.sell()if__name__=='__main__':# Create a cerebro entitycerebro=bt.Cerebro()# Add a strategycerebro.addstrategy(TestStrategy)# Datas are in a subfolder of the samples. Need to find where the script is# because it could have been called from anywheremodpath=os.path.dirname(os.path.abspath(sys.argv[0]))datapath=os.path.join(modpath,'../../datas/orcl-1995-2014.txt')# Create a Data Feeddata=bt.feeds.YahooFinanceCSVData(dataname=datapath,# Do not pass values before this datefromdate=datetime.datetime(2000,1,1),# Do not pass values before this datetodate=datetime.datetime(2000,12,31),# Do not pass values after this datereverse=False)# Add the Data Feed to Cerebrocerebro.adddata(data)# Set our desired cash startcerebro.broker.setcash(100000.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())
After the execution the output is:
Starting Portfolio Value: 100000.002000-01-03T00:00:00, Close, 27.852000-01-04T00:00:00, Close, 25.392000-01-05T00:00:00, Close, 24.052000-01-05T00:00:00, BUY CREATE, 24.052000-01-06T00:00:00, BUY EXECUTED, 23.612000-01-06T00:00:00, Close, 22.632000-01-07T00:00:00, Close, 24.372000-01-10T00:00:00, Close, 27.292000-01-11T00:00:00, Close, 26.492000-01-12T00:00:00, Close, 24.902000-01-13T00:00:00, Close, 24.772000-01-13T00:00:00, SELL CREATE, 24.772000-01-14T00:00:00, SELL EXECUTED, 25.702000-01-14T00:00:00, Close, 25.18.........2000-12-15T00:00:00, SELL CREATE, 26.932000-12-18T00:00:00, SELL EXECUTED, 28.292000-12-18T00:00:00, Close, 30.182000-12-19T00:00:00, Close, 28.882000-12-20T00:00:00, Close, 26.882000-12-20T00:00:00, BUY CREATE, 26.882000-12-21T00:00:00, BUY EXECUTED, 26.232000-12-21T00:00:00, Close, 27.822000-12-22T00:00:00, Close, 30.062000-12-26T00:00:00, Close, 29.172000-12-27T00:00:00, Close, 28.942000-12-28T00:00:00, Close, 29.292000-12-29T00:00:00, Close, 27.412000-12-29T00:00:00, SELL CREATE, 27.41Final Portfolio Value: 100018.53
Blistering Barnacles!!! The system made money … something must be wrong
The broker says: Show me the money!
And the money is called “commission”.
Let’s add a reasonable0.1% commision rate per operation (both for buying andselling … yes the broker is avid …)
A single line will suffice for it:
# 0.1% ... divide by 100 to remove the %cerebro.broker.setcommission(commission=0.001)
Being experienced with the platform we want to see the profit or loss after abuy/sell cycle, with and without commission.
from__future__import(absolute_import,division,print_function,unicode_literals)importdatetime# For datetime objectsimportos.path# To manage pathsimportsys# To find out the script name (in argv[0])# Import the backtrader platformimportbacktraderasbt# Create a StrateyclassTestStrategy(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 a reference to the "close" line in the data[0] dataseriesself.dataclose=self.datas[0].close# 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.dataclose[0])# Check if an order is pending ... if yes, we cannot send a 2nd oneifself.order:return# Check if we are in the marketifnotself.position:# Not yet ... we MIGHT BUY if ...ifself.dataclose[0]<self.dataclose[-1]:# current close less than previous closeifself.dataclose[-1]<self.dataclose[-2]:# previous close less than the previous close# BUY, BUY, BUY!!! (with default parameters)self.log('BUY CREATE,%.2f'%self.dataclose[0])# Keep track of the created order to avoid a 2nd orderself.order=self.buy()else:# Already in the market ... we might selliflen(self)>=(self.bar_executed+5):# SELL, SELL, SELL!!! (with all possible default parameters)self.log('SELL CREATE,%.2f'%self.dataclose[0])# Keep track of the created order to avoid a 2nd orderself.order=self.sell()if__name__=='__main__':# Create a cerebro entitycerebro=bt.Cerebro()# Add a strategycerebro.addstrategy(TestStrategy)# Datas are in a subfolder of the samples. Need to find where the script is# because it could have been called from anywheremodpath=os.path.dirname(os.path.abspath(sys.argv[0]))datapath=os.path.join(modpath,'../../datas/orcl-1995-2014.txt')# Create a Data Feeddata=bt.feeds.YahooFinanceCSVData(dataname=datapath,# Do not pass values before this datefromdate=datetime.datetime(2000,1,1),# Do not pass values before this datetodate=datetime.datetime(2000,12,31),# Do not pass values after this datereverse=False)# Add the Data Feed to Cerebrocerebro.adddata(data)# Set our desired cash startcerebro.broker.setcash(100000.0)# Set the commission - 0.1% ... divide by 100 to remove the %cerebro.broker.setcommission(commission=0.001)# 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())
After the execution the output is:
Starting Portfolio Value: 100000.002000-01-03T00:00:00, Close, 27.852000-01-04T00:00:00, Close, 25.392000-01-05T00:00:00, Close, 24.052000-01-05T00:00:00, BUY CREATE, 24.052000-01-06T00:00:00, BUY EXECUTED, Price: 23.61, Cost: 23.61, Commission 0.022000-01-06T00:00:00, Close, 22.632000-01-07T00:00:00, Close, 24.372000-01-10T00:00:00, Close, 27.292000-01-11T00:00:00, Close, 26.492000-01-12T00:00:00, Close, 24.902000-01-13T00:00:00, Close, 24.772000-01-13T00:00:00, SELL CREATE, 24.772000-01-14T00:00:00, SELL EXECUTED, Price: 25.70, Cost: 25.70, Commission 0.032000-01-14T00:00:00, OPERATION PROFIT, GROSS 2.09, NET 2.042000-01-14T00:00:00, Close, 25.18.........2000-12-15T00:00:00, SELL CREATE, 26.932000-12-18T00:00:00, SELL EXECUTED, Price: 28.29, Cost: 28.29, Commission 0.032000-12-18T00:00:00, OPERATION PROFIT, GROSS -0.06, NET -0.122000-12-18T00:00:00, Close, 30.182000-12-19T00:00:00, Close, 28.882000-12-20T00:00:00, Close, 26.882000-12-20T00:00:00, BUY CREATE, 26.882000-12-21T00:00:00, BUY EXECUTED, Price: 26.23, Cost: 26.23, Commission 0.032000-12-21T00:00:00, Close, 27.822000-12-22T00:00:00, Close, 30.062000-12-26T00:00:00, Close, 29.172000-12-27T00:00:00, Close, 28.942000-12-28T00:00:00, Close, 29.292000-12-29T00:00:00, Close, 27.412000-12-29T00:00:00, SELL CREATE, 27.41Final Portfolio Value: 100016.98
God Save the Queen!!! The system still made money.
Before moving on, let’s notice something by filtering the “OPERATION PROFIT”lines:
2000-01-14T00:00:00, OPERATION PROFIT, GROSS 2.09, NET 2.042000-02-07T00:00:00, OPERATION PROFIT, GROSS 3.68, NET 3.632000-02-28T00:00:00, OPERATION PROFIT, GROSS 4.48, NET 4.422000-03-13T00:00:00, OPERATION PROFIT, GROSS 3.48, NET 3.412000-03-22T00:00:00, OPERATION PROFIT, GROSS -0.41, NET -0.492000-04-07T00:00:00, OPERATION PROFIT, GROSS 2.45, NET 2.372000-04-20T00:00:00, OPERATION PROFIT, GROSS -1.95, NET -2.022000-05-02T00:00:00, OPERATION PROFIT, GROSS 5.46, NET 5.392000-05-11T00:00:00, OPERATION PROFIT, GROSS -3.74, NET -3.812000-05-30T00:00:00, OPERATION PROFIT, GROSS -1.46, NET -1.532000-07-05T00:00:00, OPERATION PROFIT, GROSS -1.62, NET -1.692000-07-14T00:00:00, OPERATION PROFIT, GROSS 2.08, NET 2.012000-07-28T00:00:00, OPERATION PROFIT, GROSS 0.14, NET 0.072000-08-08T00:00:00, OPERATION PROFIT, GROSS 4.36, NET 4.292000-08-21T00:00:00, OPERATION PROFIT, GROSS 1.03, NET 0.952000-09-15T00:00:00, OPERATION PROFIT, GROSS -4.26, NET -4.342000-09-27T00:00:00, OPERATION PROFIT, GROSS 1.29, NET 1.222000-10-13T00:00:00, OPERATION PROFIT, GROSS -2.98, NET -3.042000-10-26T00:00:00, OPERATION PROFIT, GROSS 3.01, NET 2.952000-11-06T00:00:00, OPERATION PROFIT, GROSS -3.59, NET -3.652000-11-16T00:00:00, OPERATION PROFIT, GROSS 1.28, NET 1.232000-12-01T00:00:00, OPERATION PROFIT, GROSS 2.59, NET 2.542000-12-18T00:00:00, OPERATION PROFIT, GROSS -0.06, NET -0.12
Adding up the “NET” profits the final figure is:
But the system said the following at the end:
2000-12-29T00:00:00, SELL CREATE, 27.41Final Portfolio Value: 100016.98
And obviously15.83 is not16.98. There is no error whatsoever. The “NET”profit of15.83 is already cash in the bag.
Unfortunately (or fortunately to better understand the platform) there is anopen position on the last day of theData Feed. Even if a SELL operation hasbeen sent … IT HAS NOT YET BEEN EXECUTED.
The “Final Portfolio Value” calculated by the broker takes into account the“Close” price on 2000-12-29. The actual execution price would have been set onthe next trading day which happened to be 2001-01-02. Extending theData Feed”to take into account this day the output is:
2001-01-02T00:00:00, SELL EXECUTED, Price: 27.87, Cost: 27.87, Commission 0.032001-01-02T00:00:00, OPERATION PROFIT, GROSS 1.64, NET 1.592001-01-02T00:00:00, Close, 24.872001-01-02T00:00:00, BUY CREATE, 24.87Final Portfolio Value: 100017.41
Now adding the previous NET profit to the completed operation’s net profit:
Which (discarding rounding errors in the “print” statements) is the extraPortfolio above the initial 100000 monetary units the strategy started with.
Customizing the Strategy: Parameters
It would a bit unpractical to hardcode some of the values in the strategy andhave no chance to change them easily.Parameters come in handy to help.
Definition of parameters is easy and looks like:
params=(('myparam',27),('exitbars',5),)
Being this a standard Python tuple with some tuples inside it, the following maylook more appealling to some:
params=(('myparam',27),('exitbars',5),)
With either formatting parametrization of the strategy is allowed when addingthe strategy to the Cerebro engine:
# Add a strategycerebro.addstrategy(TestStrategy,myparam=20,exitbars=7)
Note
Thesetsizing method below is deprecated. This content is kepthere for anyone looking at old samples of the sources. The sourceshave been update to use:
cerebro.addsizer(bt.sizers.FixedSize,stake=10)``
Please read the section aboutsizers
Using the parameters in the strategy is easy, as they are stored in a “params”attribute. If we for example want to set the stake fix, we can pass the stakeparameter to theposition sizer like this durintinit:
# Set the sizer stake from the paramsself.sizer.setsizing(self.params.stake)
We could have also calledbuy andsell with astake parameter andself.params.stake as the value.
The logic to exit gets modified:
# Already in the market ... we might selliflen(self)>=(self.bar_executed+self.params.exitbars):
With all this in mind the example evolves to look like:
from__future__import(absolute_import,division,print_function,unicode_literals)importdatetime# For datetime objectsimportos.path# To manage pathsimportsys# To find out the script name (in argv[0])# Import the backtrader platformimportbacktraderasbt# Create a StrateyclassTestStrategy(bt.Strategy):params=(('exitbars',5),)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 a reference to the "close" line in the data[0] dataseriesself.dataclose=self.datas[0].close# 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.dataclose[0])# Check if an order is pending ... if yes, we cannot send a 2nd oneifself.order:return# Check if we are in the marketifnotself.position:# Not yet ... we MIGHT BUY if ...ifself.dataclose[0]<self.dataclose[-1]:# current close less than previous closeifself.dataclose[-1]<self.dataclose[-2]:# previous close less than the previous close# BUY, BUY, BUY!!! (with default parameters)self.log('BUY CREATE,%.2f'%self.dataclose[0])# Keep track of the created order to avoid a 2nd orderself.order=self.buy()else:# Already in the market ... we might selliflen(self)>=(self.bar_executed+self.params.exitbars):# SELL, SELL, SELL!!! (with all possible default parameters)self.log('SELL CREATE,%.2f'%self.dataclose[0])# Keep track of the created order to avoid a 2nd orderself.order=self.sell()if__name__=='__main__':# Create a cerebro entitycerebro=bt.Cerebro()# Add a strategycerebro.addstrategy(TestStrategy)# Datas are in a subfolder of the samples. Need to find where the script is# because it could have been called from anywheremodpath=os.path.dirname(os.path.abspath(sys.argv[0]))datapath=os.path.join(modpath,'../../datas/orcl-1995-2014.txt')# Create a Data Feeddata=bt.feeds.YahooFinanceCSVData(dataname=datapath,# Do not pass values before this datefromdate=datetime.datetime(2000,1,1),# Do not pass values before this datetodate=datetime.datetime(2000,12,31),# Do not pass values after this datereverse=False)# Add the Data Feed to Cerebrocerebro.adddata(data)# Set our desired cash startcerebro.broker.setcash(100000.0)# Add a FixedSize sizer according to the stakecerebro.addsizer(bt.sizers.FixedSize,stake=10)# Set the commission - 0.1% ... divide by 100 to remove the %cerebro.broker.setcommission(commission=0.001)# 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())
After the execution the output is:
Starting Portfolio Value: 100000.002000-01-03T00:00:00, Close, 27.852000-01-04T00:00:00, Close, 25.392000-01-05T00:00:00, Close, 24.052000-01-05T00:00:00, BUY CREATE, 24.052000-01-06T00:00:00, BUY EXECUTED, Size 10, Price: 23.61, Cost: 236.10, Commission 0.242000-01-06T00:00:00, Close, 22.63.........2000-12-20T00:00:00, BUY CREATE, 26.882000-12-21T00:00:00, BUY EXECUTED, Size 10, Price: 26.23, Cost: 262.30, Commission 0.262000-12-21T00:00:00, Close, 27.822000-12-22T00:00:00, Close, 30.062000-12-26T00:00:00, Close, 29.172000-12-27T00:00:00, Close, 28.942000-12-28T00:00:00, Close, 29.292000-12-29T00:00:00, Close, 27.412000-12-29T00:00:00, SELL CREATE, 27.41Final Portfolio Value: 100169.80
In order to see the difference, the print outputs have also been extended toshow the execution size.
Having multiplied the stake by 10, the obvious has happened: the profit and losshas been multiplied by 10. Instead of16.98, the surplus is now169.80
Adding an indicator
Having heard ofindicators, the next thing anyone would add to the strategy isone of them. For sure they must be much better than a simple“3 lower closes”strategy.
Inspired in one of the examples from PyAlgoTrade a strategy using a SimpleMoving Average.
Buy “AtMarket” if the close is greater than the Average
If in the market, sell if the close is smaller than the Average
Only 1 active operation is allowed in the market
Most of the existing code can be kept in place. Let’s add the average duringinit and keep a reference to it:
self.sma=bt.indicators.MovingAverageSimple(self.datas[0],period=self.params.maperiod)
And of course the logic to enter and exit the market will rely on the Averagevalues. Look in the code for the logic.
Note
The starting cash will be 1000 monetary units to be in line with thePyAlgoTrade example and no commission will be applied
from__future__import(absolute_import,division,print_function,unicode_literals)importdatetime# For datetime objectsimportos.path# To manage pathsimportsys# To find out the script name (in argv[0])# Import the backtrader platformimportbacktraderasbt# Create a StrateyclassTestStrategy(bt.Strategy):params=(('maperiod',15),)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 a reference to the "close" line in the data[0] dataseriesself.dataclose=self.datas[0].close# To keep track of pending orders and buy price/commissionself.order=Noneself.buyprice=Noneself.buycomm=None# Add a MovingAverageSimple indicatorself.sma=bt.indicators.SimpleMovingAverage(self.datas[0],period=self.params.maperiod)defnotify_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.dataclose[0])# Check if an order is pending ... if yes, we cannot send a 2nd oneifself.order:return# Check if we are in the marketifnotself.position:# Not yet ... we MIGHT BUY if ...ifself.dataclose[0]>self.sma[0]:# BUY, BUY, BUY!!! (with all possible default parameters)self.log('BUY CREATE,%.2f'%self.dataclose[0])# Keep track of the created order to avoid a 2nd orderself.order=self.buy()else:ifself.dataclose[0]<self.sma[0]:# SELL, SELL, SELL!!! (with all possible default parameters)self.log('SELL CREATE,%.2f'%self.dataclose[0])# Keep track of the created order to avoid a 2nd orderself.order=self.sell()if__name__=='__main__':# Create a cerebro entitycerebro=bt.Cerebro()# Add a strategycerebro.addstrategy(TestStrategy)# Datas are in a subfolder of the samples. Need to find where the script is# because it could have been called from anywheremodpath=os.path.dirname(os.path.abspath(sys.argv[0]))datapath=os.path.join(modpath,'../../datas/orcl-1995-2014.txt')# Create a Data Feeddata=bt.feeds.YahooFinanceCSVData(dataname=datapath,# Do not pass values before this datefromdate=datetime.datetime(2000,1,1),# Do not pass values before this datetodate=datetime.datetime(2000,12,31),# Do not pass values after this datereverse=False)# Add the Data Feed to Cerebrocerebro.adddata(data)# Set our desired cash startcerebro.broker.setcash(1000.0)# Add a FixedSize sizer according to the stakecerebro.addsizer(bt.sizers.FixedSize,stake=10)# 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())
Now, before skipping to the next sectionLOOK CAREFULLY to the first datewhich is shown in the log:
It’ no longer2000-01-03, the first trading day in the year 2K.
It’s 2000-01-24 …Who has stolen my cheese?
The missing days are not missing. The platform has adapted to the newcircumstances:
An indicator (SimpleMovingAverage) has been added to the Strategy.
This indicator needs X bars to produce an output: in the example: 15
2000-01-24 is the day in which the 15th bar occurs
Thebacktrader platform assumes that the Strategy has the indicator in placefor a good reason,to use it in the decision making process. And it makes nosense to try to make decisions if the indicator is not yet ready and producingvalues.
next will be 1st called when all indicators have already reached the minimum needed period to produce a value
In the example there is a single indicator, but the strategy could have any number of them.
After the execution the output is:
Starting Portfolio Value: 1000.002000-01-24T00:00:00, Close, 25.552000-01-25T00:00:00, Close, 26.612000-01-25T00:00:00, BUY CREATE, 26.612000-01-26T00:00:00, BUY EXECUTED, Size 10, Price: 26.76, Cost: 267.60, Commission 0.002000-01-26T00:00:00, Close, 25.962000-01-27T00:00:00, Close, 24.432000-01-27T00:00:00, SELL CREATE, 24.432000-01-28T00:00:00, SELL EXECUTED, Size 10, Price: 24.28, Cost: 242.80, Commission 0.002000-01-28T00:00:00, OPERATION PROFIT, GROSS -24.80, NET -24.802000-01-28T00:00:00, Close, 22.342000-01-31T00:00:00, Close, 23.552000-02-01T00:00:00, Close, 25.462000-02-02T00:00:00, Close, 25.612000-02-02T00:00:00, BUY CREATE, 25.612000-02-03T00:00:00, BUY EXECUTED, Size 10, Price: 26.11, Cost: 261.10, Commission 0.00.........2000-12-20T00:00:00, SELL CREATE, 26.882000-12-21T00:00:00, SELL EXECUTED, Size 10, Price: 26.23, Cost: 262.30, Commission 0.002000-12-21T00:00:00, OPERATION PROFIT, GROSS -20.60, NET -20.602000-12-21T00:00:00, Close, 27.822000-12-21T00:00:00, BUY CREATE, 27.822000-12-22T00:00:00, BUY EXECUTED, Size 10, Price: 28.65, Cost: 286.50, Commission 0.002000-12-22T00:00:00, Close, 30.062000-12-26T00:00:00, Close, 29.172000-12-27T00:00:00, Close, 28.942000-12-28T00:00:00, Close, 29.292000-12-29T00:00:00, Close, 27.412000-12-29T00:00:00, SELL CREATE, 27.41Final Portfolio Value: 973.90
In the name of the King!!! A winning system turned into a losing one … andthat with no commission. It may well be thatsimply adding anindicator isnot the universal panacea.
Note
The same logic and data with PyAlgoTrade yields a slightly differentresult (slightly off). Looking at the entire printout reveals thatsome operations are not exactly the same. Being the culprit again theusual suspect:rounding.
PyAlgoTrade does not round the datafeed values when applying thedivided “adjusted close” to the data feed values.
The Yahoo Data Feed provided bybacktrader rounds the values downto 2 decimals after applying the adjusted close. Upon printing thevalues everything seems the same, but it’s obvious that sometimesthat 5th place decimal plays a role.
Rounding down to 2 decimals seems more realistic, because MarketExchanges do only allow a number of decimals per asset (being that 2decimals usually for stocks)
Note
The Yahoo Data Feed (starting with version1.8.11.99 allows tospecify if rounding has to happen and how many decimals)
Visual Inspection: Plotting
A printout or log of the actual whereabouts of the system at each bar-instant isgood but humans tend to bevisual and therefore it seems right to offer a viewof the same whereabouts as chart.
Note
To plot you need to havematplotlib installed
Once again defaults for plotting are there to assist the platform user. Plottingis incredibly a 1 line operation:
Being the location for sure after cerebro.run() has been called.
In order to display the automatic plotting capabilities and a couple of easycustomizations, the following will be done:
A 2nd MovingAverage (Exponential) will be added. The defaults will plot it (just like the 1st) with the data.
A 3rd MovingAverage (Weighted) will be added. Customized to plot in an own plot (even if not sensible)
A Stochastic (Slow) will be added. No change to the defaults.
A MACD will be added. No change to the defaults.
A RSI will be added. No change to the defaults.
A MovingAverage (Simple) will be applied to the RSI. No change to the defaults (it will be plotted with the RSI)
An AverageTrueRange will be added. Changed defaults to avoid it being plotted.
The entire set of additions to theinit method of the Strategy:
# Indicators for the plotting showbt.indicators.ExponentialMovingAverage(self.datas[0],period=25)bt.indicators.WeightedMovingAverage(self.datas[0],period=25).subplot=Truebt.indicators.StochasticSlow(self.datas[0])bt.indicators.MACDHisto(self.datas[0])rsi=bt.indicators.RSI(self.datas[0])bt.indicators.SmoothedMovingAverage(rsi,period=10)bt.indicators.ATR(self.datas[0]).plot=False
Note
Even ifindicators are not explicitly added to a member variable ofthe strategy (like self.sma = MovingAverageSimple…), they willautoregister with the strategy and will influence the minimum periodfornext and will be part of the plotting.
In the example onlyRSI is added to a temporary variablersi withthe only intention to create a MovingAverageSmoothed on it.
The example now:
from__future__import(absolute_import,division,print_function,unicode_literals)importdatetime# For datetime objectsimportos.path# To manage pathsimportsys# To find out the script name (in argv[0])# Import the backtrader platformimportbacktraderasbt# Create a StrateyclassTestStrategy(bt.Strategy):params=(('maperiod',15),)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 a reference to the "close" line in the data[0] dataseriesself.dataclose=self.datas[0].close# To keep track of pending orders and buy price/commissionself.order=Noneself.buyprice=Noneself.buycomm=None# Add a MovingAverageSimple indicatorself.sma=bt.indicators.SimpleMovingAverage(self.datas[0],period=self.params.maperiod)# Indicators for the plotting showbt.indicators.ExponentialMovingAverage(self.datas[0],period=25)bt.indicators.WeightedMovingAverage(self.datas[0],period=25,subplot=True)bt.indicators.StochasticSlow(self.datas[0])bt.indicators.MACDHisto(self.datas[0])rsi=bt.indicators.RSI(self.datas[0])bt.indicators.SmoothedMovingAverage(rsi,period=10)bt.indicators.ATR(self.datas[0],plot=False)defnotify_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')# Write down: no pending orderself.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.dataclose[0])# Check if an order is pending ... if yes, we cannot send a 2nd oneifself.order:return# Check if we are in the marketifnotself.position:# Not yet ... we MIGHT BUY if ...ifself.dataclose[0]>self.sma[0]:# BUY, BUY, BUY!!! (with all possible default parameters)self.log('BUY CREATE,%.2f'%self.dataclose[0])# Keep track of the created order to avoid a 2nd orderself.order=self.buy()else:ifself.dataclose[0]<self.sma[0]:# SELL, SELL, SELL!!! (with all possible default parameters)self.log('SELL CREATE,%.2f'%self.dataclose[0])# Keep track of the created order to avoid a 2nd orderself.order=self.sell()if__name__=='__main__':# Create a cerebro entitycerebro=bt.Cerebro()# Add a strategycerebro.addstrategy(TestStrategy)# Datas are in a subfolder of the samples. Need to find where the script is# because it could have been called from anywheremodpath=os.path.dirname(os.path.abspath(sys.argv[0]))datapath=os.path.join(modpath,'../../datas/orcl-1995-2014.txt')# Create a Data Feeddata=bt.feeds.YahooFinanceCSVData(dataname=datapath,# Do not pass values before this datefromdate=datetime.datetime(2000,1,1),# Do not pass values before this datetodate=datetime.datetime(2000,12,31),# Do not pass values after this datereverse=False)# Add the Data Feed to Cerebrocerebro.adddata(data)# Set our desired cash startcerebro.broker.setcash(1000.0)# Add a FixedSize sizer according to the stakecerebro.addsizer(bt.sizers.FixedSize,stake=10)# 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())# Plot the resultcerebro.plot()
After the execution the output is:
Starting Portfolio Value: 1000.002000-02-18T00:00:00, Close, 27.612000-02-22T00:00:00, Close, 27.972000-02-22T00:00:00, BUY CREATE, 27.972000-02-23T00:00:00, BUY EXECUTED, Size 10, Price: 28.38, Cost: 283.80, Commission 0.002000-02-23T00:00:00, Close, 29.73.........2000-12-21T00:00:00, BUY CREATE, 27.822000-12-22T00:00:00, BUY EXECUTED, Size 10, Price: 28.65, Cost: 286.50, Commission 0.002000-12-22T00:00:00, Close, 30.062000-12-26T00:00:00, Close, 29.172000-12-27T00:00:00, Close, 28.942000-12-28T00:00:00, Close, 29.292000-12-29T00:00:00, Close, 27.412000-12-29T00:00:00, SELL CREATE, 27.41Final Portfolio Value: 981.00
The final result has changed even if the logic hasn’t. This is true but thelogic has not been applied to the same number of bars.
Note
As explained before, the platform will first call next when allindicators are ready to produce a value. In this plotting example(very clear in the chart) the MACD is the last indicator to be fullyready (all 3 lines producing an output). The 1st BUY order is nolonger scheduled during Jan 2000 but close to the end of Feb 2000.
The chart:

Let’s Optimize
Many trading books say each market and each traded stock (or commodity or ..)have different rythms. That there is no such thing as a one size fits all.
Before the plotting sample, when the strategy started using an indicator theperiod default value was 15 bars. It’s a strategy parameter and this can be usedin an optimization to change the value of the parameter and see which one betterfits the market.
Note
There is plenty of literature about Optimization and associated prosand cons. But the advice will always point in the same direction: donot overoptimize. If a trading idea is not sound, optimizing may endproducing a positive result which is only valid for the backtesteddataset.
The sample is modified to optimize the period of the Simple Moving Average. Forthe sake of clarity any output with regards to Buy/Sell orders has been removed
The example now:
from__future__import(absolute_import,division,print_function,unicode_literals)importdatetime# For datetime objectsimportos.path# To manage pathsimportsys# To find out the script name (in argv[0])# Import the backtrader platformimportbacktraderasbt# Create a StrateyclassTestStrategy(bt.Strategy):params=(('maperiod',15),('printlog',False),)deflog(self,txt,dt=None,doprint=False):''' Logging function fot this strategy'''ifself.params.printlogordoprint:dt=dtorself.datas[0].datetime.date(0)print('%s,%s'%(dt.isoformat(),txt))def__init__(self):# Keep a reference to the "close" line in the data[0] dataseriesself.dataclose=self.datas[0].close# To keep track of pending orders and buy price/commissionself.order=Noneself.buyprice=Noneself.buycomm=None# Add a MovingAverageSimple indicatorself.sma=bt.indicators.SimpleMovingAverage(self.datas[0],period=self.params.maperiod)defnotify_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')# Write down: no pending orderself.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.dataclose[0])# Check if an order is pending ... if yes, we cannot send a 2nd oneifself.order:return# Check if we are in the marketifnotself.position:# Not yet ... we MIGHT BUY if ...ifself.dataclose[0]>self.sma[0]:# BUY, BUY, BUY!!! (with all possible default parameters)self.log('BUY CREATE,%.2f'%self.dataclose[0])# Keep track of the created order to avoid a 2nd orderself.order=self.buy()else:ifself.dataclose[0]<self.sma[0]:# SELL, SELL, SELL!!! (with all possible default parameters)self.log('SELL CREATE,%.2f'%self.dataclose[0])# Keep track of the created order to avoid a 2nd orderself.order=self.sell()defstop(self):self.log('(MA Period%2d) Ending Value%.2f'%(self.params.maperiod,self.broker.getvalue()),doprint=True)if__name__=='__main__':# Create a cerebro entitycerebro=bt.Cerebro()# Add a strategystrats=cerebro.optstrategy(TestStrategy,maperiod=range(10,31))# Datas are in a subfolder of the samples. Need to find where the script is# because it could have been called from anywheremodpath=os.path.dirname(os.path.abspath(sys.argv[0]))datapath=os.path.join(modpath,'../../datas/orcl-1995-2014.txt')# Create a Data Feeddata=bt.feeds.YahooFinanceCSVData(dataname=datapath,# Do not pass values before this datefromdate=datetime.datetime(2000,1,1),# Do not pass values before this datetodate=datetime.datetime(2000,12,31),# Do not pass values after this datereverse=False)# Add the Data Feed to Cerebrocerebro.adddata(data)# Set our desired cash startcerebro.broker.setcash(1000.0)# Add a FixedSize sizer according to the stakecerebro.addsizer(bt.sizers.FixedSize,stake=10)# Set the commissioncerebro.broker.setcommission(commission=0.0)# Run over everythingcerebro.run(maxcpus=1)
Instead of callingaddstrategy to add a stratey class to Cerebro, the call ismade tooptstrategy. And instead of passing a value a range of values ispassed.
One of the “Strategy” hooks is added, thestop method, which will be calledwhen the data has been exhausted and backtesting is over. It’s used to print thefinal net value of the portfolio in the broker (it was done in Cerebropreviously)
The system will execute the strategy for each value of the range. The followingwill be output:
2000-12-29, (MA Period 10) Ending Value 880.302000-12-29, (MA Period 11) Ending Value 880.002000-12-29, (MA Period 12) Ending Value 830.302000-12-29, (MA Period 13) Ending Value 893.902000-12-29, (MA Period 14) Ending Value 896.902000-12-29, (MA Period 15) Ending Value 973.902000-12-29, (MA Period 16) Ending Value 959.402000-12-29, (MA Period 17) Ending Value 949.802000-12-29, (MA Period 18) Ending Value 1011.902000-12-29, (MA Period 19) Ending Value 1041.902000-12-29, (MA Period 20) Ending Value 1078.002000-12-29, (MA Period 21) Ending Value 1058.802000-12-29, (MA Period 22) Ending Value 1061.502000-12-29, (MA Period 23) Ending Value 1023.002000-12-29, (MA Period 24) Ending Value 1020.102000-12-29, (MA Period 25) Ending Value 1013.302000-12-29, (MA Period 26) Ending Value 998.302000-12-29, (MA Period 27) Ending Value 982.202000-12-29, (MA Period 28) Ending Value 975.702000-12-29, (MA Period 29) Ending Value 983.302000-12-29, (MA Period 30) Ending Value 979.80
Results:
For periods below 18 the strategy (commissionless) loses money.
For periods between 18 and 26 (both included) the strategy makes money.
Above 26 money is lost again.
And the winning period for this strategy and the given data set is:
- 20 bars, which wins 78.00 units over 1000 $/€ (a 7.8%)
Note
The extra indicators from the plotting example have been removed andthe start of operations is only influenced by the Simple MovingAverage which is being optimized. Hence the slightly different resultsfor period 15
Conclusion
The incremental samples have shown how to go from a barebones script to a fullyworking trading system which even plots the results and can be optimized.
A lot more can be done to try to improve the chances of winning:
Self defined Indicators
Creating an indicator is easy (and even plotting them is easy)
Sizers
Money Management is for many the key to success
Order Types (limit, stop, stoplimit)
Some others
To ensure all the above items can be fully utilized the documentation providesan insight into them (and other topics)
Look in the table of contents and keep on reading … and developing.
Best of luck