Source code for backtrader.analyzers.calmar

#!/usr/bin/env python
"""Calmar Ratio Analyzer Module - Calmar ratio calculation.

This module provides the Calmar analyzer for calculating the Calmar
ratio (annual return divided by maximum drawdown).

Classes:
    Calmar: Analyzer that calculates Calmar ratio.

Example:
    >>> cerebro = bt.Cerebro()
    >>> cerebro.addanalyzer(bt.analyzers.Calmar, _name='calmar')
    >>> results = cerebro.run()
    >>> print(results[0].analyzers.calmar.get_analysis())
"""

import collections
import math

from ..analyzer import TimeFrameAnalyzerBase
from ..dataseries import TimeFrame
from ..metabase import OwnerContext
from .drawdown import TimeDrawDown

__all__ = ["Calmar"]


# Calculate Calmar ratio. Overall, this Calmar calculation is not very successful, or the analyzer/observer series indicators are not very efficient in usage
# Consider creating an analysis module similar to pyfolio
[docs] class Calmar(TimeFrameAnalyzerBase): """This analyzer calculates the CalmarRatio timeframe which can be different from the one used in the underlying data 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 compression is None, then the compression of the first data in the system will be used - *None* - ``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 See also: - https://en.wikipedia.org/wiki/Calmar_ratio Methods: - ``get_analysis`` Returns a OrderedDict with a key for the time period and the corresponding rolling Calmar ratio Attributes: - ``calmar`` the latest calculated calmar ratio """ # Modules used packages = ( "collections", "math", ) # Parameters params = ( ("timeframe", TimeFrame.Months), # default in calmar ("period", 36), ("fund", None), ) # Calculate max drawdown
[docs] def __init__(self, *args, **kwargs): """Initialize the Calmar 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.calmar = None self._fundmode = None self._values = None self._mdd = None # Use OwnerContext so child analyzer can find this as its parent with OwnerContext.set_owner(self): self._maxdd = TimeDrawDown(timeframe=self.p.timeframe, compression=self.p.compression)
# Start
[docs] def start(self): """Initialize the analyzer at the start of the backtest. Sets up the maximum drawdown tracking, value history queue, and fund mode. """ # Max drawdown rate self._mdd = float("-inf") # Double-ended queue, saves period values, default is 36 self._values = collections.deque([float("Nan")] * self.p.period, maxlen=self.p.period) # fundmode if self.p.fund is None: self._fundmode = self.strategy.broker.fundmode else: self._fundmode = self.p.fund # Add different values to self._values based on fundmode if not self._fundmode: self._values.append(self.strategy.broker.getvalue()) else: self._values.append(self.strategy.broker.fundvalue)
[docs] def on_dt_over(self): """Calculate Calmar ratio when timeframe period ends. Updates maximum drawdown and calculates Calmar ratio as annualized return divided by maximum drawdown. """ # Max drawdown rate self._mdd = max(self._mdd, self._maxdd.maxdd) # Add value to self._values if not self._fundmode: self._values.append(self.strategy.broker.getvalue()) else: self._values.append(self.strategy.broker.fundvalue) # Calculate average monthly return by default try: ratio = self._values[-1] / self._values[0] if isinstance(ratio, complex) or not math.isfinite(ratio) or ratio <= 0: raise ValueError(f"invalid calmar ratio input: {ratio}") rann = math.log(ratio) / len(self._values) if not math.isfinite(rann): raise ValueError(f"invalid calmar annualized return: {rann}") except (ZeroDivisionError, TypeError, ValueError): rann = 0.0 # Calculate Calmar indicator try: self.calmar = calmar = rann / (self._mdd or float("Inf")) except (ZeroDivisionError, TypeError, ValueError): self.calmar = calmar = 0.0 if isinstance(calmar, complex) or not math.isfinite(calmar): self.calmar = calmar = 0.0 # Save result self.rets[self.dtkey] = calmar
[docs] def stop(self): """Finalize the analysis when backtest ends. Triggers one final Calmar ratio calculation. """ self.on_dt_over() # update last values