Operating the platform
To engage into operations, the plaftorm uses the notion of line iterators. They have been loosely modeled after Python’s iterators but have actually nothing to do with them.
Strategies and Indicators are line iterators.
The line iterator concept tries to describe the following:
A Line Iterator kicks slave line iterators telling them to iterate
A Line Iterator then iterates over its own declared named lines setting values
The key to iteration, just like with regular Python iterators, is:
It will be called for each iteration. The
datasarray which the line iterator has and serve as basis for logic/calculations will have already been moved to the next index by the platform (barring data replay)
Called when the minimum period for the line iterator has been met. A bit more on this below.
But because they are not regular iterators, two additional methods exist:
Called before the minimum period for the line iterator` has been met.
Called exactly ONCE when the minimum period for the line iterator` has been met.
The default behavior is to forward the call to
next, but can of course be overriden if needed.
Extra methods for Indicators
To speed up operations, Indicators support a batch operation mode which has
been termed as runonce. It is not strictly needed (a
next method suffices)
but it greatly reduces time.
The runonce methods rules void the get/set point with index 0 and relies on direct access to the underlying arrays holding the data and being passed the right indices for each state.
The defined methods follow the naming of the next family:
once(self, start, end)
Called when the minimum period has been met. The internal array must be processed between start and end which are zero based from the start of the internal array
preonce(self, start, end)
Called before the minimum period has been met.
oncestart(self, start, end)
Called exactly ONCE when the minimum period has been met.
The default behavior is to forward the call to
once, but can of course be overriden if needed.
A picture is worth a thousand words and in this case possibly an example too. A SimpleMovingAverage is capable of explaining it:
class SimpleMovingAverage(Indicator): lines = ('sma',) params = dict(period=20) def __init__(self): ... # Not relevant for the explanation def prenext(self): print('prenext:: current period:', len(self)) def nextstart(self): print('nextstart:: current period:', len(self)) # emulate default behavior ... call next self.next() def next(self): print('next:: current period:', len(self))
And the instantiation could look like:
sma = btind.SimpleMovingAverage(self.data, period=25)
Assuming the data passed to the moving average is a standard data feed its default period is
1that is: the data feed produces a bar with no initial delay.
Then the “period=25” instantiated moving average would have its methods called as follows:
nextstart1 time (in turn calling
nextn additional times until the data feed has been exhausted
Let’s go for the killer indicator: a SimpleMovingAverage over another SimpleMovingAverage. The instantiation could look like:
sma1 = btind.SimpleMovingAverage(self.data, period=25) sma2 = btind.SimpleMovingAverage(sma1, period=20)
What now goes on:
The same as above for
sma2is receiving a data feed which has a minimum period of 25 which is our
sma2methods are called as indicated:
prenextthe first 25 + 18 times for a total of 43 times
25 times to let
sma1produce its 1st sensible value
18 times to accumulate extra
For a total of 19 values (1 after 25 calls and then 18 more)
nextstartthen 1 time (in turn calling
nextthe n additional times until the data feed has been exhausted
The platform is calling
next when the system has already processed 44 bars.
The minimum period has been automatically adjusted to the incoming data.
Strategies and Indicators adhere to this behavior:
- Only when the automatically calculated minimum period has been reached will
nextbe called (barring the initial hook call to
The same rules apply to
the runonce batch operation mode
The minimum period behavior can be manipulated although it’s not
recommended. Should it be wished used the
method in either Strategies or Indicators
Up and Running
Getting up and running involves at least 3 Lines objects:
A Data feed
A Strategy (actually a class derived from Strategy)
A Cerebro (brain in Spanish)
These objects, obviously, provide the data which will be backtested by applying calculations (direct and/or with Indicators)
The platform provides several data feeds:
Several CSV Format and a Generic CSV reader
Yahoo online fetcher
Support for receiving Pandas DataFrames and blaze objects
Live Data Feeds with Interacive Brokers, Visual Chart and Oanda
The platform makes no assumption about the content of the data feed such as timeframe and compression. Those values, together with a name, can be supplied for informational purposes and advance operations like Data Feed Resampling (turning a for example a 5 minute Data Feed into a Daily Data Feed)
Example of setting up a Yahoo Finance Data Feed:
import backtrader as bt import backtrader.feeds as btfeeds ... datapath = 'path/to/your/yahoo/data.csv' data = btfeeds.YahooFinanceCSVData( dataname=datapath, reversed=True)
reversed parameter for Yahoo is shown, because the CSV files
directly downloaded from Yahoo start with the latest date, rather than with the
If your data spans a large time range, the actual loaded data can be limited as follows:
data = btfeeds.YahooFinanceCSVData( dataname=datapath, reversed=True fromdate=datetime.datetime(2014, 1, 1), todate=datetime.datetime(2014, 12, 31))
Both the fromdate and the todate will be included if present in the data feed.
As already mentioned timeframe, compression and name can be added:
data = btfeeds.YahooFinanceCSVData( dataname=datapath, reversed=True fromdate=datetime.datetime(2014, 1, 1), todate=datetime.datetime(2014, 12, 31) timeframe=bt.TimeFrame.Days, compression=1, name='Yahoo' )
If the data is plotted, those values will be used.
A Strategy (derived) class
Before going on and for a more simplified approach, please check the Signals section of the documentation if subclassing a strategy is not wished.
The goal of anyone using the platform is backtesting the data and this is done inside a Strategy (derived class).
There are 2 methods which at least need customization:
During initialization indicators on data and other calculations are created prepared to later apply the logic.
The next method is later called to apply the logic for each and every bar of the data.
If data feeds of different timeframes (and thus different bar counts)
are passed the
next method will be called for the master data
(the 1st one passed to cerebro, see below) which must be the the data
with the smaller timeframe
If the Data Replay functionality is used, the
next method will be
called several time for the same bar as the development of the bar is
A basic Strategy derived class:
class MyStrategy(bt.Strategy): def __init__(self): self.sma = btind.SimpleMovingAverage(self.data, period=20) def next(self): if self.sma > self.data.close: self.buy() elif self.sma < self.data.close: self.sell()
Strategies have other methods (or hook points) which can be overriden:
class MyStrategy(bt.Strategy): def __init__(self): self.sma = btind.SimpleMovingAverage(self.data, period=20) def next(self): if self.sma > self.data.close: submitted_order = self.buy() elif self.sma < self.data.close: submitted_order = self.sell() def start(self): print('Backtesting is about to start') def stop(self): print('Backtesting is finished') def notify_order(self, order): print('An order new/changed/executed/canceled has been received')
stop methods should be self-explanatory. As expected and
following the text in the print function, the
notify_order method will be
called when the strategy needs a notification. Use case:
A buy or sell is requested (as seen in next)
buy/sell will return an order which is submitted to the broker. Keeping a reference to this submitted order is up to the caller.
It can for example be used to ensure that no new orders are submitted if an order is still pending.
If the order is Accepted/Executed/Canceled/Changed the broker will notify the status change (and for example execution size) back to the strategy via the notify method
The QuickStart guide has a complete and functional example of order management
More can be done with other Strategy classes:
Use the underlying broker and sizer to send the broker a buy/sell order
The same could be done by manually creating an Order and passing it over to the broker. But the platform is about making it easy for those using it.
closewill get the current market position and close it immediately.
getposition(or the property “position”)
Returns the current market position
getsizer(or the property “sizer”)
These allow setting/getting the underlying stake Sizer. The same logic can be checked against Sizers which provide different stakes for the same situation (fixed size, proportional to capital, exponential)
There is plenty of literature but Van K. Tharp has excellent books on the subject.
A Strategy is a Lines object and these support parameters, which are collected using the standard Python kwargs argument:
class MyStrategy(bt.Strategy): params = (('period', 20),) def __init__(self): self.sma = btind.SimpleMovingAverage(self.data, period=self.params.period) ... ...
Notice how the
SimpleMovingAverage is no longer instantiated with a fixed
value of 20, but rather with the parameter “period” which has been defined for
Once Data Feeds are available and the Strategy has been defined, a Cerebro instance is what brings everything together and execute the actions. Instantiating one is easy:
cerebro = bt.Cerebro()
Defaults are taking care of if nothing special is wished.
A default broker is created
No commission for the operations
Data Feeds will be preloaded
The default execution mode will be runonce (batch operation) which is the faster
All indicators must support the
runoncemode for full speed. The ones included in the platform do.
Custom indicators do not need to implement the runonce functionality.
Cerebrowill simulate it, which means those non-runonce compatible indicators will run slower. But still most of the system will run in batch mode.
Since a Data feed is already available and a Strategy too (created earlier) the standard way to put it all together and get it up and running is:
cerebro.adddata(data) cerebro.addstrategy(MyStrategy, period=25) cerebro.run()
Notice the following:
The Data Feed “instance” is added
The MyStrategy “class” is added along with parameters (kwargs) that will be passed to it.
The instantiation of MyStrategy will be done by cerebro in the background and any kwargs in “addstrategy” will be passed to it
The user may add as many Strategies and Data Feeds as wished. How Strategies communicate with each other to achieve coordination (if wished be) is not enforced/restricted by the platform.
Of course a Cerebro offers additional possibilities:
Decide about preloading and operation mode:
cerebro = bt.Cerebro(runonce=True, preload=True)
There is a constraint here:
runonceneeds preloading (if not, a batch operation cannot be run) Of course preloading Data Feeds does not enforce
getbroker(and the broker property)
A custom broker can be set if wished. The actual broker instance can also be accesed
Plotting. In a regular case as easy as:
plot takes some arguments for the customization
If the plot is too dense it may be broken down into several plots
A customer plotter instance can be passed and cerebro will not instantiate a default one
**kwargs- standard keyword arguments
Which will get passed to the plotter.
Please see the plotting section for more information.
Optimization of strategies.
As mentioned above, Cerebro gets a Strategy derived class (not an instance) and the keyword arguments that will be passed to it upon instantiation, which will happen when “run” is called.
This is so to enable optimization. The same Strategy class will be instantiated as many times as needed with new parameters. If an instance had been passed to cerebro … this would not be possible.
Optimization is requested as follows:
cerebro.optstrategy(MyStrategy, period=xrange(10, 20))
optstrategyhas the same signature as
addstrategybut does extra housekeeping to ensure optimization runs as expected. A strategy could be expecting a range as a normal parameter for a strategy and
addstrategywill make no assumptions about the passed parameter.
On the other hand,
optstrategywill understand that an iterable is a set of values that has to be passed in sequence to each instantiation of the Strategy class.
Notice that instead of a single value a range of values is passed. In this simple case 10 values 10 -> 19 (20 is the upper limit) will be tried for this strategy.
If a more complex strategy is developed with extra parameters they can all be passed to optstrategy. Parameters which must not undergo optimization can be passed directly without the end user having to create a dummy iterable of just one value. Example:
cerebro.optstrategy(MyStrategy, period=xrange(10, 20), factor=3.5)
optstrategymethod sees factor and creates (a needed) dummy iterable in the background for factor which has a single element (in the example 3.5)
Interactive Python shells and some types of frozen executables under Windows have problems with the Python
Please read the Python documentation about