Source code for backtrader.analyzers.returns
#!/usr/bin/env python
"""Returns Analyzer Module - Return statistics calculation.
This module provides the Returns analyzer for calculating total, average,
compound, and annualized returns using a logarithmic approach.
Classes:
Returns: Analyzer that calculates return statistics.
Example:
>>> cerebro = bt.Cerebro()
>>> cerebro.addanalyzer(bt.analyzers.Returns, _name='ret')
>>> results = cerebro.run()
>>> print(results[0].analyzers.ret.get_analysis())
"""
import math
from ..analyzer import TimeFrameAnalyzerBase
from ..dataseries import TimeFrame
# Calculate total, average, compound and annualized returns using logarithmic method
[docs]
class Returns(TimeFrameAnalyzerBase):
"""
Total, Average, Compound and Annualized Returns calculated using a
logarithmic approach
See:
- https://www.crystalbull.com/sharpe-ratio-better-with-log-returns/
Params:
- ``timeframe`` (default: ``None``)
If ``None`` the `timeframe` of the first data in the system will be
used
Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no
time constraints
- ``compression`` (default: ``None``)
Only used for sub-day timeframes to, for example, work on an hourly
timeframe by specifying "TimeFrame.Minutes" and 60 as compression
If `None`, then the compression of the first data in the system will be
used
- ``tann`` (default: ``None``)
Number of periods to use for the annualization (normalization)
namely:
- ``days: 252``
- ``weeks: 52``
- ``months: 12``
- ``years: 1``
- ``fund`` (default: ``None``)
If `None`, the actual mode of the broker (fundmode - True/False) will
be autodetected to decide if the returns are based on the total net
asset value or on the fund value. See ``set_fundmode`` in the broker
documentation
Set it to ``True`` or ``False`` for a specific behavior
Methods:
- get_analysis
Returns a dictionary with returns as values and the datetime points for
each return as keys
The returned dict the following keys:
- ``rtot``: Total compound return
- ``ravg``: Average return for the entire period (timeframe specific)
- ``rnorm``: Annualized/Normalized return
- ``rnorm100``: Annualized/Normalized return expressed in 100%
"""
# Parameters
params = (
("tann", None),
("fund", None),
)
# Days etc. for calculating annualization
_TANN = {
TimeFrame.Days: 252.0,
TimeFrame.Weeks: 52.0,
TimeFrame.Months: 12.0,
TimeFrame.Years: 1.0,
}
# Start
[docs]
def __init__(self, *args, **kwargs):
"""Initialize the Returns analyzer.
Args:
*args: Positional arguments.
**kwargs: Keyword arguments for analyzer parameters.
"""
# Call parent class __init__ method to support timeframe and compression parameters
super().__init__(*args, **kwargs)
self._value_end = None
self._tcount = None
self._value_start = None
self._fundmode = None
[docs]
def start(self):
"""Initialize the analyzer at the start of the backtest.
Records the initial portfolio value and sets the fund mode.
"""
super().start()
# If fund is None, _fundmode is broker's fundmode, otherwise equals fund
if self.p.fund is None:
self._fundmode = self.strategy.broker.fundmode
else:
self._fundmode = self.p.fund
# If fundmode is False, get value, otherwise get fundvalue
if not self._fundmode:
self._value_start = self.strategy.broker.getvalue()
else:
self._value_start = self.strategy.broker.fundvalue
# Count subperiods
self._tcount = 0
# When stopping
[docs]
def stop(self):
"""Calculate and store return statistics at the end of the backtest.
Calculates:
- rtot: Total compound return
- ravg: Average return for the period
- rnorm: Annualized return
- rnorm100: Annualized return in percentage form
"""
super().stop()
# If fundmode is False, get value, otherwise get fundvalue
if not self._fundmode:
self._value_end = self.strategy.broker.getvalue()
else:
self._value_end = self.strategy.broker.fundvalue
# Compound return
# rtot calculates total log returns
try:
nlrtot = self._value_end / self._value_start
if isinstance(nlrtot, complex) or not math.isfinite(nlrtot) or nlrtot <= 0.0:
rtot = float("-inf")
else:
rtot = math.log(nlrtot)
if not math.isfinite(rtot):
rtot = float("-inf")
except (ZeroDivisionError, TypeError, ValueError):
rtot = float("-inf")
self.rets["rtot"] = rtot
# Average return
# Calculate average return, first calculate log returns, then calculate average log returns
if self._tcount > 0:
self.rets["ravg"] = ravg = rtot / self._tcount
else:
self.rets["ravg"] = ravg = 0.0
# Annualized normalized return
# Calculate annualized return
tann = self.p.tann or self._TANN.get(self.timeframe, None)
if tann is None:
tann = self._TANN.get(self.data._timeframe, 1.0) # assign default
if ravg > float("-inf"):
self.rets["rnorm"] = rnorm = math.expm1(ravg * tann)
else:
self.rets["rnorm"] = rnorm = ravg
# Annualized return in percentage form
self.rets["rnorm100"] = rnorm * 100.0 # human-readable %
[docs]
def on_dt_over(self):
"""Called when a datetime period is over.
Increments the subperiod counter.
"""
self._tcount += 1 # count the subperiod