backtrader.analyzers.timereturn 源代码
#!/usr/bin/env python
"""TimeReturn Analyzer Module - Time-based returns calculation.
This module provides the TimeReturn analyzer for calculating returns
over specified time periods.
Classes:
TimeReturn: Analyzer that calculates returns by timeframe.
Example:
>>> cerebro = bt.Cerebro()
>>> cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years)
"""
from ..analyzer import TimeFrameAnalyzerBase
[文档]
class TimeReturn(TimeFrameAnalyzerBase):
"""This analyzer calculates the Returns by looking at the beginning
and end of the timeframe
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
- ``data`` (default: ``None``)
Reference asset to track instead of the portfolio value.
- Note:: this data must have been added to a ``cerebro`` instance with
``addata``, ``resampledata`` or ``replaydata``
- ``firstopen`` (default: ``True``)
When tracking the returns of `data` the following is done when
crossing a timeframe boundary, for example, ``Years``:
- Last ``close`` the previous year is used as the reference price to
see the return in the current year
The problem is the first calculation, because the data has** no
previous** closing price.As such, and when this parameter is `True`,
the *opening* price will be used for the first calculation.
This requires the data feed to have an ``open`` price (for ``close``
the standard [0] notations will be used without a reference to a field
price)
Else the initial close will be used.
# When calculating returns for the first period, whether to use the first opening price. If parameter is False, the first closing price will be used
- ``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
"""
# Parameters
params = (
("data", None),
("firstopen", True),
("fund", None),
)
# __init__ method to support parameter initialization after metaclass removal
[文档]
def __init__(self, *args, **kwargs):
"""Initialize the TimeReturn analyzer.
Args:
*args: Positional arguments.
**kwargs: Keyword arguments for analyzer parameters.
"""
super().__init__(*args, **kwargs)
# Start
[文档]
def start(self):
"""Initialize the analyzer at the start of the backtest.
Sets the fund mode and initializes the starting value for return
calculations.
"""
super().start()
if self.p.fund is None:
self._fundmode = self.strategy.broker.fundmode
else:
self._fundmode = self.p.fund
# PERFORMANCE OPTIMIZATION: Cache p.data reference (called 691K+ times)
self._p_data = self.p.data
# Start value
self._value_start = 0.0
# End value
self._lastvalue = None
# If parameter data is None
if self._p_data is None:
# keep the initial portfolio value if not tracing a data
if not self._fundmode:
self._lastvalue = self.strategy.broker.getvalue()
else:
self._lastvalue = self.strategy.broker.fundvalue
# Notify fund information
[文档]
def notify_fund(self, cash, value, fundvalue, shares):
"""Update current value based on fund notification.
Args:
cash: Current cash amount.
value: Current portfolio value.
fundvalue: Current fund value.
shares: Number of fund shares.
"""
# PERFORMANCE OPTIMIZATION: Use cached _p_data reference
p_data = self._p_data
if not self._fundmode:
# Record current value
if p_data is None:
self._value = value # the portofolio value if tracking no data
else:
self._value = p_data[0] # the data value if tracking data
else:
if p_data is None:
self._value = fundvalue # the fund value if tracking no data
else:
self._value = p_data[0] # the data value if tracking data
# On datetime over
[文档]
def on_dt_over(self):
"""Handle timeframe boundary crossing.
Updates the starting value for return calculation when crossing
into a new timeframe period.
"""
# next is called in a new timeframe period
# PERFORMANCE OPTIMIZATION: Use cached _p_data reference
p_data = self._p_data
if p_data is None or self._lastvalue is not None:
self._value_start = self._lastvalue # update value_start to last
else:
# The 1st tick has no previous reference, use the opening price
if self.p.firstopen:
self._value_start = p_data.open[0]
else:
self._value_start = p_data[0]
# Call next
[文档]
def next(self):
"""Calculate and store the return for the current period.
Calculates the return as (current_value / start_value) - 1 and
stores it in the results dictionary keyed by datetime.
"""
# Calculate the return
super().next()
# self.dtkey is an attribute set in analyzer, usually the end date of a period
self.rets[self.dtkey] = (self._value / self._value_start) - 1.0
# self.rets[self.dtkey] = (float(self._value) / float(self._value_start)) - 1.0
self._lastvalue = self._value # keep last value