Analyzers
Be it backtesting or trading, being able to analyze the performance of the trading system is key to understanding if not only profit has been attained, but also if it has been achieved with too much risk or if it was really worth the effort when compared with a reference asset (or a risk-free asset)
That’s where the family of Analyzer
objects comes in: provide an analysis
of what’s happened or even of what’s actually happening.
Nature of analyzers
The interface is modeled after that of Lines objects, feature for example a
next
method but there is a major difference:
-
Analyzers
do not hold lines.That means they are not expensive in terms of memory because even after having analyzed thousands of price bars they may still simply hold a single result in memory.
Location in the ecosystem
Analyzer
objects are (like strategies, observers and datas) added to
the system through a cerebro
instance:
addanalyzer(ancls, *args, **kwargs)
But when it comes to operation during cerebro.run
the following will happen
for each strategy present in the system
-
ancls
will be instantiated with*args
and**kwargs
during acerebro.run
-
The
ancls
instance will be attached to the strategy
That means:
- If the backtesting run contains for example 3 strategies then 3
instances of
ancls
will be created and each of them will be attached to a different strategy.
Bottomline: an analyzer analyzes the performance of a single strategy and not the performance of an entires system
Additional Location
Some Analyzer
objects may actually use other analyzers to complete its
work. For example: SharpeRatio
uses the output of TimeReturn
for the
calculations.
These sub-analyzers or slave-analyzers will also be inserted into the same strategy as the one creating them. But they are completely invisible to the user.
Attributes
To carry out the intended work, Analyzer
objects are provided with some
default attributes which are automagically passed and set in the instance for
ease of use:
-
self.strategy
: reference to the strategy subclass in which the analyzer object is operating. Anything accessible by the strategy can also be accessd by the analyzer -
self.datas[x]
: the array of data feeds present in the strategy. Although this could be accesed over the strategy reference, the shortcut makes work more comfortable. -
self.data
: shortcut toself.datas[0]
for extra comfort. -
self.dataX
: shortcuts to the differentself.datas[x]
Some other aliases are available although they are probably an overkill:
* `self.dataX_Y` where X is a reference to `self.datas[X]` and `Y`
refers to the line, finally pointing to: `self.datas[X].lines[Y]`
If the line has a name, the following is also available:
* `self.dataX_Name` which resolves to `self.datas[X].Name` returning
the line by name rather than by index
For the first data, the last two shortcuts are available without the initial
X
numeric reference. For example:
* `self.data_2` refers to `self.datas[0].lines[2]`
And
* `self.data_close` refers to `self.datas[0].close`
Returning the analysis
The Analyzer base class creates a self.rets
(of type
collections.OrderedDict
) member attribute to return the analysis. This is
done in the method create_analysis
which can be overriden by subclasses if
creating custom analyzers.
Modus operandi
Although Analyzer
objects are not Lines objects and therefore do not
iterate over lines, they have been designed to follow the same operation
pattern.
-
Instantiated before the system is put into motion (therefore calling
__init__
) -
Signaled the begin of operations with
start
-
prenext
/nextstart
/next
will be invoked following the calculated minimum period of the strategy the indicator is working in.The default behaviour of
prenext
andnextstart
is to invoke next, because an analyzer may be analyzing from the very first moment the system is alive.It may be customary to call
len(self)
in Lines objects to check the actual amount of bars. This also works inAnalyzers
by returning the value forself.strategy
-
Orders and trades will be notified just like they are to the strategy via
notify_order
andnotify_trade
-
Cash and value will also be notified like it is done with the strategy over the
notify_cashvalue
method -
Cash, value and fundvalue and fund shares will also be notified like it is done with the strategy over the
notify_fund
method -
stop
will be invoked to signal the end of operations
Once the regular operations cycle has been completed, the analyzers featuring additional methods for extracting/outputting information
-
get_analysis
: which ideally (not enforced) returnes adict
-like object containing the analysis results. -
print
uses a standardbacktrader.WriterFile
(unless overriden) to write the analysis result fromget_analysis
. -
pprint
(pretty print) uses the Pythonpprint
module to print theget_analysis
resutls.
And finally:
-
get_analysis
creates a member attributeself.ret
(of typecollections.OrderedDict
) to which analyzers write the analysis results.Subclasses of Analyzer can override this method to change this behavior
Analyzer Patterns
Development of Analyzer objects in the backtrader
platform have revealed
2 different usage patterns for the generation of the analysis:
-
During execution by gathering information in the
notify_xxx
andnext
methods, and generating the current information of the analysis innext
The
TradeAnalyzer
, for example, uses just thenotify_trade
method to generate the statistics. -
Gather (or not) the information as above, but generate the analysis in a single pass during the
stop
methodThe
SQN
(System Quality Number) gathers trade information duringnotify_trade
but generates the statistic during thestop
method
A quick example
As easy as it can be:
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime
import backtrader as bt
import backtrader.analyzers as btanalyzers
import backtrader.feeds as btfeeds
import backtrader.strategies as btstrats
cerebro = bt.Cerebro()
# data
dataname = '../datas/sample/2005-2006-day-001.txt'
data = btfeeds.BacktraderCSVData(dataname=dataname)
cerebro.adddata(data)
# strategy
cerebro.addstrategy(btstrats.SMA_CrossOver)
# Analyzer
cerebro.addanalyzer(btanalyzers.SharpeRatio, _name='mysharpe')
thestrats = cerebro.run()
thestrat = thestrats[0]
print('Sharpe Ratio:', thestrat.analyzers.mysharpe.get_analysis())
Executing it (having stored it in analyzer-test.py
:
$ ./analyzer-test.py
Sharpe Ratio: {'sharperatio': 11.647332609673256}
There is no plotting, because the SharpeRatio
is a single value at the end
of the calculation.
Forensic Analysis of an Analyzer
Let’s repeat that Analyzers
are not Lines objects, but to seamlessly
integrate them into the backtrader
ecosystem, the internal API conventions
of several Lines object are followed (actually a mixture of them)
Note
The code for the SharpeRatio
has evolved to take for example into
account annualization and the version here should only be a
reference.
Please check the Analyzers Reference
There is additionally a SharpeRatio_A
which provides the value
directly in annualized form regardless of the sought timeframe
Code for SharpeRatio
to serve as a basis (a simplified version)
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import operator
from backtrader.utils.py3 import map
from backtrader import Analyzer, TimeFrame
from backtrader.mathsupport import average, standarddev
from backtrader.analyzers import AnnualReturn
class SharpeRatio(Analyzer):
params = (('timeframe', TimeFrame.Years), ('riskfreerate', 0.01),)
def __init__(self):
super(SharpeRatio, self).__init__()
self.anret = AnnualReturn()
def start(self):
# Not needed ... but could be used
pass
def next(self):
# Not needed ... but could be used
pass
def stop(self):
retfree = [self.p.riskfreerate] * len(self.anret.rets)
retavg = average(list(map(operator.sub, self.anret.rets, retfree)))
retdev = standarddev(self.anret.rets)
self.ratio = retavg / retdev
def get_analysis(self):
return dict(sharperatio=self.ratio)
The code can be broken down into:
-
params
declarationAlthough the declared ones are not used (meant as an example), Analyzers like most other objects in
backtrader
support parameters -
__init__
methodJust like Strategies declare Indicators in
__init__
, the same do analyzers with support objects.In this case: the
SharpeRatio
is calculated using Annual Returns. The calculation will be automatic and will be available toSharpeRatio
for its own calculations.Note
The actual implementation of
SharpeRatio
uses the more generic and later developedTimeReturn
analyzer -
next
methodSharpeRatio
doesn’t need it, but this method will be called after each invocation of the parent strategynext
-
start
methodCalled right before the backtesting starts. Can be used for extra initialization tasks. Sharperatio doesn’t need it
-
stop
methodCalled right after the backtesting ends. Like
SharpeRatio
does, it can be used to finish/make the calculation -
get_analysis
method (returns a dictionary)Access for external callers to the produced analysis
Returns: a dictionary with the analysis.
Reference
class backtrader.Analyzer()
Analyzer base class. All analyzers are subclass of this one
An Analyzer instance operates in the frame of a strategy and provides an analysis for that strategy.
Automagically set member attributes:
-
self.strategy
(giving access to the strategy and anything accessible from it) -
self.datas[x]
giving access to the array of data feeds present in the the system, which could also be accessed via the strategy reference -
self.data
, giving access toself.datas[0]
-
self.dataX
->self.datas[X]
-
self.dataX_Y
->self.datas[X].lines[Y]
-
self.dataX_name
->self.datas[X].name
-
self.data_name
->self.datas[0].name
-
self.data_Y
->self.datas[0].lines[Y]
This is not a Lines object, but the methods and operation follow the same design
-
__init__
during instantiation and initial setup -
start
/stop
to signal the begin and end of operations -
prenext
/nextstart
/next
family of methods that follow the calls made to the same methods in the strategy -
notify_trade
/notify_order
/notify_cashvalue
/notify_fund
which receive the same notifications as the equivalent methods of the strategy
The mode of operation is open and no pattern is preferred. As such the
analysis can be generated with the next
calls, at the end of operations
during stop
and even with a single method like notify_trade
The important thing is to override get_analysis
to return a dict-like
object containing the results of the analysis (the actual format is
implementation dependent)
start()
Invoked to indicate the start of operations, giving the analyzer time to setup up needed things
stop()
Invoked to indicate the end of operations, giving the analyzer time to shut down needed things
prenext()
Invoked for each prenext invocation of the strategy, until the minimum period of the strategy has been reached
The default behavior for an analyzer is to invoke next
nextstart()
Invoked exactly once for the nextstart invocation of the strategy, when the minimum period has been first reached
next()
Invoked for each next invocation of the strategy, once the minum preiod of the strategy has been reached
notify_cashvalue(cash, value)
Receives the cash/value notification before each next cycle
notify_fund(cash, value, fundvalue, shares)
Receives the current cash, value, fundvalue and fund shares
notify_order(order)
Receives order notifications before each next cycle
notify_trade(trade)
Receives trade notifications before each next cycle
get_analysis()
Returns a dict-like object with the results of the analysis
The keys and format of analysis results in the dictionary is implementation dependent.
It is not even enforced that the result is a dict-like object, just the convention
The default implementation returns the default OrderedDict rets
created by the default create_analysis
method
create_analysis()
Meant to be overriden by subclasses. Gives a chance to create the structures that hold the analysis.
The default behaviour is to create a OrderedDict
named rets
print(*args, **kwargs)
Prints the results returned by get_analysis
via a standard
Writerfile
object, which defaults to writing things to standard
output
pprint(*args, **kwargs)
Prints the results returned by get_analysis
using the pretty
print Python module (pprint)
len()
Support for invoking len
on analyzers by actually returning the
current length of the strategy the analyzer operates on