Operating the platform
Line Iterators
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:
-
The
next
methodIt will be called for each iteration. The
datas
array 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:
-
prenext
Called before the minimum period for the line iterator` has been met.
-
nextstart
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.
Minimum Period
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)
Briefly explained:
-
Assuming the data passed to the moving average is a standard data feed its default period is
1
that 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:
-
prenext
24 times -
nextstart
1 time (in turn callingnext
) -
next
n 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
sma1
-
sma2
is receiving a data feed which has a minimum period of 25 which is oursma1
and therefore -
The
sma2
methods are called as indicated:-
prenext
the first 25 + 18 times for a total of 43 times -
25 times to let
sma1
produce its 1st sensible value -
18 times to accumulate extra
sma1
values -
For a total of 19 values (1 after 25 calls and then 18 more)
-
nextstart
then 1 time (in turn callingnext
) -
next
the 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
next
be called (barring the initial hook call tonextstart
)
Note
The same rules apply to preonce
, oncestart
and once
for
the runonce batch operation mode
Note
The minimum period behavior can be manipulated although it’s not
recommended. Should it be wished used the setminperiod(minperiod)
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)
Data Feeds
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)
The optional reversed
parameter for Yahoo is shown, because the CSV files
directly downloaded from Yahoo start with the latest date, rather than with the
oldest.
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
Note
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:
-
__init__
-
next
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.
Note
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
Note
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
replayed.
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')
The start
and 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
in the notify_order
method.
More can be done with other Strategy classes:
-
buy
/sell
/close
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.
close
will get the current market position and close it immediately. -
getposition
(or the property “position”)Returns the current market position
-
setsizer
/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
the strategy.
A Cerebro
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
runonce
mode for full speed. The ones included in the platform do.Custom indicators do not need to implement the runonce functionality.
Cerebro
will 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:
runonce
needs preloading (if not, a batch operation cannot be run) Of course preloading Data Feeds does not enforcerunonce
-
setbroker
/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:
cerebro.run() cerebro.plot()
plot takes some arguments for the customization
-
numfigs=1
If the plot is too dense it may be broken down into several plots
-
plotter=None
A customer plotter instance can be passed and cerebro will not instantiate a default one
-
**kwargs
- standard keyword argumentsWhich 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))
The method
optstrategy
has the same signature asaddstrategy
but does extra housekeeping to ensure optimization runs as expected. A strategy could be expecting a range as a normal parameter for a strategy andaddstrategy
will make no assumptions about the passed parameter.On the other hand,
optstrategy
will 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)
The
optstrategy
method sees factor and creates (a needed) dummy iterable in the background for factor which has a single element (in the example 3.5)Note
Interactive Python shells and some types of frozen executables under Windows have problems with the Python
multiprocessing
modulePlease read the Python documentation about
multiprocessing
.