Source code for backtrader.analyzers.annualreturn
#!/usr/bin/env python
"""Annual Return Analyzer Module - Annual return calculation.
This module provides the AnnualReturn analyzer for calculating
year-by-year returns of a strategy.
Classes:
AnnualReturn: Analyzer that calculates annual returns.
Example:
>>> cerebro = bt.Cerebro()
>>> cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='annret')
>>> results = cerebro.run()
>>> print(results[0].analyzers.annret.get_analysis())
"""
import math
from collections import OrderedDict
from ..analyzer import Analyzer
from ..utils.date import num2date
from ..utils.log_message import get_logger
from ..utils.py3 import range
logger = get_logger(__name__)
# Calculate annual returns. The algorithm implementation is somewhat complex, so a pandas-based version MyAnnualReturn was written later with much simpler logic
[docs]
class AnnualReturn(Analyzer):
"""
This analyzer calculates the AnnualReturns by looking at the beginning
and end of the year
Params:
- (None)
Member Attributes:
- ``rets``: list of calculated annual returns
- ``ret``: dictionary (key: year) of annual returns
**get_analysis**:
- Returns a dictionary of annual returns (key: year)
"""
[docs]
def __init__(self):
"""Initialize the AnnualReturn analyzer.
Initializes cache lists for storing dates and values during backtesting.
"""
super().__init__()
# Cache data
self._dt_cache = []
self._value_cache = []
[docs]
def next(self):
"""Cache current date and account value on each bar.
Stores the current datetime and portfolio value for later
annual return calculation.
"""
# Cache current date and account value each time next is called
dt_val = self.data.datetime[0]
value_val = self.strategy.broker.getvalue()
self._dt_cache.append(dt_val)
self._value_cache.append(value_val)
[docs]
def stop(self):
"""Calculate annual returns from cached data.
Iterates through cached date-value pairs to calculate returns
for each calendar year. Stores results in self.rets (list) and
self.ret (dictionary keyed by year).
"""
# Must have stats.broker
# Current year
cur_year = -1
# Start value
value_start = 0.0
# End value
value_end = 0.0
# Save return data
setattr(self, "rets", [])
setattr(self, "ret", OrderedDict())
# Calculate using cached data
for i in range(len(self._dt_cache)):
dt_val = self._dt_cache[i]
value_cur = self._value_cache[i]
# Convert date
try:
dt = num2date(dt_val)
except (ValueError, TypeError, OverflowError) as e:
logger.debug("Failed to convert date value %s: %s", dt_val, e)
continue
# If the year at index i is greater than current year, if current year > 0, calculate return and save to self.ret, and start value equals end value
# When years are not equal, it indicates current i is a new year
if dt.year > cur_year:
if cur_year >= 0:
annual_ret = self._safe_annual_return(value_start, value_end)
self.rets.append(annual_ret)
self.ret[cur_year] = annual_ret
# changing between real years, use last value as new start
value_start = value_end
else:
# No value set whatsoever, use the currently loaded value
value_start = value_cur
cur_year = dt.year
# No matter what, the last value is always the last loaded value
value_end = value_cur
# If current year hasn't ended and return hasn't been calculated, calculate at the end even if less than a full year
if cur_year >= 0 and cur_year not in self.ret:
# finish calculating pending data
annual_ret = self._safe_annual_return(value_start, value_end)
self.rets.append(annual_ret)
self.ret[cur_year] = annual_ret
@staticmethod
def _safe_annual_return(value_start, value_end):
"""Compute (value_end/value_start - 1), returning 0.0 for any invalid
(zero/NaN/inf/complex) inputs or result. Shared by the year-boundary
and final-pending paths in stop()."""
try:
valid_values = (
value_start != 0 and math.isfinite(value_start) and math.isfinite(value_end)
)
except TypeError:
valid_values = False
if not valid_values:
return 0.0
try:
annual_ret = (value_end / value_start) - 1.0
if isinstance(annual_ret, complex) or not math.isfinite(annual_ret):
return 0.0
return annual_ret
except (ZeroDivisionError, TypeError, ValueError):
return 0.0
[docs]
def get_analysis(self):
"""Return the annual return analysis results.
Returns:
OrderedDict: Dictionary mapping years to their annual returns.
"""
return self.ret
[docs]
class MyAnnualReturn(Analyzer):
"""
This analyzer calculates the AnnualReturns by looking at the beginning
and end of the year
Params:
- (None)
Member Attributes:
- ``rets``: list of calculated annual returns
- ``ret``: dictionary (key: year) of annual returns
**get_analysis**:
- Returns a dictionary of annual returns (key: year)
"""
[docs]
def stop(self):
"""Calculate annual returns using pandas.
Uses pandas DataFrame operations to group data by year and
calculate annual returns based on beginning and ending values
for each year.
Note:
This method requires pandas to be installed.
"""
# Container for saving data - dictionary
if not hasattr(self, "ret"):
setattr(self, "ret", OrderedDict())
# Get data time and convert to date
dt_list = self.data.datetime.get(0, size=len(self.data))
dt_list = [num2date(i) for i in dt_list]
# Get account assets
value_list = self.strategy.stats.broker.value.get(0, size=len(self.data))
# Convert to pandas format
import pandas as pd
df = pd.DataFrame([dt_list, value_list]).T
df.columns = ["datetime", "value"]
df["pre_value"] = df["value"].shift(1)
# Calculate simple returns for each year
df["year"] = [i.year for i in df["datetime"]]
for year, data in df.groupby("year"):
begin_value = list(data["pre_value"])[0]
end_value = list(data["value"])[-1]
try:
valid_values = math.isfinite(begin_value) and math.isfinite(end_value)
except TypeError:
valid_values = False
if not valid_values or begin_value == 0:
annual_return = 0.0
else:
try:
annual_return = (end_value / begin_value) - 1
if isinstance(annual_return, complex) or not math.isfinite(annual_return):
annual_return = 0.0
except (ZeroDivisionError, TypeError, ValueError):
annual_return = 0.0
self.ret[year] = annual_return
[docs]
def get_analysis(self):
"""Return the annual return analysis results.
Returns:
OrderedDict: Dictionary mapping years to their annual returns.
"""
return self.ret