1#!/usr/bin/env python
2# -*- coding: utf-8; py-indent-offset:4 -*-
3###############################################################################
4#
5# Copyright (C) 2015, 2016, 2017 Daniel Rodriguez
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program.  If not, see <http://www.gnu.org/licenses/>.
19#
20###############################################################################
21from __future__ import (absolute_import, division, print_function,
22                        unicode_literals)
23
24import math
25
26from backtrader.utils.py3 import itervalues
27
28from backtrader import Analyzer, TimeFrame
29from backtrader.mathsupport import average, standarddev
30from backtrader.analyzers import TimeReturn, AnnualReturn
31
32
33class SharpeRatio(Analyzer):
34    '''This analyzer calculates the SharpeRatio of a strategy using a risk free
35    asset which is simply an interest rate
36
37    See also:
38
39      - https://en.wikipedia.org/wiki/Sharpe_ratio
40
41    Params:
42
43      - ``timeframe``: (default: ``TimeFrame.Years``)
44
45      - ``compression`` (default: ``1``)
46
47        Only used for sub-day timeframes to for example work on an hourly
48        timeframe by specifying "TimeFrame.Minutes" and 60 as compression
49
50      - ``riskfreerate`` (default: 0.01 -> 1%)
51
52        Expressed in annual terms (see ``convertrate`` below)
53
54      - ``convertrate`` (default: ``True``)
55
56        Convert the ``riskfreerate`` from annual to monthly, weekly or daily
57        rate. Sub-day conversions are not supported
58
59      - ``factor`` (default: ``None``)
60
61        If ``None``, the conversion factor for the riskfree rate from *annual*
62        to the chosen timeframe will be chosen from a predefined table
63
64          Days: 252, Weeks: 52, Months: 12, Years: 1
65
66        Else the specified value will be used
67
68      - ``annualize`` (default: ``False``)
69
70        If ``convertrate`` is ``True``, the *SharpeRatio* will be delivered in
71        the ``timeframe`` of choice.
72
73        In most occasions the SharpeRatio is delivered in annualized form.
74        Convert the ``riskfreerate`` from annual to monthly, weekly or daily
75        rate. Sub-day conversions are not supported
76
77      - ``stddev_sample`` (default: ``False``)
78
79        If this is set to ``True`` the *standard deviation* will be calculated
80        decreasing the denominator in the mean by ``1``. This is used when
81        calculating the *standard deviation* if it's considered that not all
82        samples are used for the calculation. This is known as the *Bessels'
83        correction*
84
85      - ``daysfactor`` (default: ``None``)
86
87        Old naming for ``factor``. If set to anything else than ``None`` and
88        the ``timeframe`` is ``TimeFrame.Days`` it will be assumed this is old
89        code and the value will be used
90
91      - ``legacyannual`` (default: ``False``)
92
93        Use the ``AnnualReturn`` return analyzer, which as the name implies
94        only works on years
95
96      - ``fund`` (default: ``None``)
97
98        If ``None`` the actual mode of the broker (fundmode - True/False) will
99        be autodetected to decide if the returns are based on the total net
100        asset value or on the fund value. See ``set_fundmode`` in the broker
101        documentation
102
103        Set it to ``True`` or ``False`` for a specific behavior
104
105    Methods:
106
107      - get_analysis
108
109        Returns a dictionary with key "sharperatio" holding the ratio
110
111    '''
112    params = (
113        ('timeframe', TimeFrame.Years),
114        ('compression', 1),
115        ('riskfreerate', 0.01),
116        ('factor', None),
117        ('convertrate', True),
118        ('annualize', False),
119        ('stddev_sample', False),
120
121        # old behavior
122        ('daysfactor', None),
123        ('legacyannual', False),
124        ('fund', None),
125    )
126
127    RATEFACTORS = {
128        TimeFrame.Days: 252,
129        TimeFrame.Weeks: 52,
130        TimeFrame.Months: 12,
131        TimeFrame.Years: 1,
132    }
133
134    def __init__(self):
135        if self.p.legacyannual:
136            self.anret = AnnualReturn()
137        else:
138            self.timereturn = TimeReturn(
139                timeframe=self.p.timeframe,
140                compression=self.p.compression,
141                fund=self.p.fund)
142
143    def stop(self):
144        super(SharpeRatio, self).stop()
145        if self.p.legacyannual:
146            rate = self.p.riskfreerate
147            retavg = average([r - rate for r in self.anret.rets])
148            retdev = standarddev(self.anret.rets)
149
150            self.ratio = retavg / retdev
151        else:
152            # Get the returns from the subanalyzer
153            returns = list(itervalues(self.timereturn.get_analysis()))
154
155            rate = self.p.riskfreerate  #
156
157            factor = None
158
159            # Hack to identify old code
160            if self.p.timeframe == TimeFrame.Days and \
161               self.p.daysfactor is not None:
162
163                factor = self.p.daysfactor
164
165            else:
166                if self.p.factor is not None:
167                    factor = self.p.factor  # user specified factor
168                elif self.p.timeframe in self.RATEFACTORS:
169                    # Get the conversion factor from the default table
170                    factor = self.RATEFACTORS[self.p.timeframe]
171
172            if factor is not None:
173                # A factor was found
174
175                if self.p.convertrate:
176                    # Standard: downgrade annual returns to timeframe factor
177                    rate = pow(1.0 + rate, 1.0 / factor) - 1.0
178                else:
179                    # Else upgrade returns to yearly returns
180                    returns = [pow(1.0 + x, factor) - 1.0 for x in returns]
181
182            lrets = len(returns) - self.p.stddev_sample
183            # Check if the ratio can be calculated
184            if lrets:
185                # Get the excess returns - arithmetic mean - original sharpe
186                ret_free = [r - rate for r in returns]
187                ret_free_avg = average(ret_free)
188                retdev = standarddev(ret_free, avgx=ret_free_avg,
189                                     bessel=self.p.stddev_sample)
190
191                try:
192                    ratio = ret_free_avg / retdev
193
194                    if factor is not None and \
195                       self.p.convertrate and self.p.annualize:
196
197                        ratio = math.sqrt(factor) * ratio
198                except (ValueError, TypeError, ZeroDivisionError):
199                    ratio = None
200            else:
201                # no returns or stddev_sample was active and 1 return
202                ratio = None
203
204            self.ratio = ratio
205
206        self.rets['sharperatio'] = self.ratio
207
208
209class SharpeRatio_A(SharpeRatio):
210    '''Extension of the SharpeRatio which returns the Sharpe Ratio directly in
211    annualized form
212
213    The following param has been changed from ``SharpeRatio``
214
215      - ``annualize`` (default: ``True``)
216
217    '''
218
219    params = (
220        ('annualize', True),
221    )
222