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 calendar
25from collections import OrderedDict
26import datetime
27import pprint as pp
28
29import backtrader as bt
30from backtrader import TimeFrame
31from backtrader.utils.py3 import MAXINT, with_metaclass
32
33
34class MetaAnalyzer(bt.MetaParams):
35    def donew(cls, *args, **kwargs):
36        '''
37        Intercept the strategy parameter
38        '''
39        # Create the object and set the params in place
40        _obj, args, kwargs = super(MetaAnalyzer, cls).donew(*args, **kwargs)
41
42        _obj._children = list()
43
44        _obj.strategy = strategy = bt.metabase.findowner(_obj, bt.Strategy)
45        _obj._parent = bt.metabase.findowner(_obj, Analyzer)
46
47        # Register with a master observer if created inside one
48        masterobs = bt.metabase.findowner(_obj, bt.Observer)
49        if masterobs is not None:
50            masterobs._register_analyzer(_obj)
51
52        _obj.datas = strategy.datas
53
54        # For each data add aliases: for first data: data and data0
55        if _obj.datas:
56            _obj.data = data = _obj.datas[0]
57
58            for l, line in enumerate(data.lines):
59                linealias = data._getlinealias(l)
60                if linealias:
61                    setattr(_obj, 'data_%s' % linealias, line)
62                setattr(_obj, 'data_%d' % l, line)
63
64            for d, data in enumerate(_obj.datas):
65                setattr(_obj, 'data%d' % d, data)
66
67                for l, line in enumerate(data.lines):
68                    linealias = data._getlinealias(l)
69                    if linealias:
70                        setattr(_obj, 'data%d_%s' % (d, linealias), line)
71                    setattr(_obj, 'data%d_%d' % (d, l), line)
72
73        _obj.create_analysis()
74
75        # Return to the normal chain
76        return _obj, args, kwargs
77
78    def dopostinit(cls, _obj, *args, **kwargs):
79        _obj, args, kwargs = \
80            super(MetaAnalyzer, cls).dopostinit(_obj, *args, **kwargs)
81
82        if _obj._parent is not None:
83            _obj._parent._register(_obj)
84
85        # Return to the normal chain
86        return _obj, args, kwargs
87
88
89class Analyzer(with_metaclass(MetaAnalyzer, object)):
90    '''Analyzer base class. All analyzers are subclass of this one
91
92    An Analyzer instance operates in the frame of a strategy and provides an
93    analysis for that strategy.
94
95    Automagically set member attributes:
96
97      - ``self.strategy`` (giving access to the *strategy* and anything
98        accessible from it)
99
100      - ``self.datas[x]`` giving access to the array of data feeds present in
101        the the system, which could also be accessed via the strategy reference
102
103      - ``self.data``, giving access to ``self.datas[0]``
104
105      - ``self.dataX`` -> ``self.datas[X]``
106
107      - ``self.dataX_Y`` -> ``self.datas[X].lines[Y]``
108
109      - ``self.dataX_name`` -> ``self.datas[X].name``
110
111      - ``self.data_name`` -> ``self.datas[0].name``
112
113      - ``self.data_Y`` -> ``self.datas[0].lines[Y]``
114
115    This is not a *Lines* object, but the methods and operation follow the same
116    design
117
118      - ``__init__`` during instantiation and initial setup
119
120      - ``start`` / ``stop`` to signal the begin and end of operations
121
122      - ``prenext`` / ``nextstart`` / ``next`` family of methods that follow
123        the calls made to the same methods in the strategy
124
125      - ``notify_trade`` / ``notify_order`` / ``notify_cashvalue`` /
126        ``notify_fund`` which receive the same notifications as the equivalent
127        methods of the strategy
128
129    The mode of operation is open and no pattern is preferred. As such the
130    analysis can be generated with the ``next`` calls, at the end of operations
131    during ``stop`` and even with a single method like ``notify_trade``
132
133    The important thing is to override ``get_analysis`` to return a *dict-like*
134    object containing the results of the analysis (the actual format is
135    implementation dependent)
136
137    '''
138    csv = True
139
140    def __len__(self):
141        '''Support for invoking ``len`` on analyzers by actually returning the
142        current length of the strategy the analyzer operates on'''
143        return len(self.strategy)
144
145    def _register(self, child):
146        self._children.append(child)
147
148    def _prenext(self):
149        for child in self._children:
150            child._prenext()
151
152        self.prenext()
153
154    def _notify_cashvalue(self, cash, value):
155        for child in self._children:
156            child._notify_cashvalue(cash, value)
157
158        self.notify_cashvalue(cash, value)
159
160    def _notify_fund(self, cash, value, fundvalue, shares):
161        for child in self._children:
162            child._notify_fund(cash, value, fundvalue, shares)
163
164        self.notify_fund(cash, value, fundvalue, shares)
165
166    def _notify_trade(self, trade):
167        for child in self._children:
168            child._notify_trade(trade)
169
170        self.notify_trade(trade)
171
172    def _notify_order(self, order):
173        for child in self._children:
174            child._notify_order(order)
175
176        self.notify_order(order)
177
178    def _nextstart(self):
179        for child in self._children:
180            child._nextstart()
181
182        self.nextstart()
183
184    def _next(self):
185        for child in self._children:
186            child._next()
187
188        self.next()
189
190    def _start(self):
191        for child in self._children:
192            child._start()
193
194        self.start()
195
196    def _stop(self):
197        for child in self._children:
198            child._stop()
199
200        self.stop()
201
202    def notify_cashvalue(self, cash, value):
203        '''Receives the cash/value notification before each next cycle'''
204        pass
205
206    def notify_fund(self, cash, value, fundvalue, shares):
207        '''Receives the current cash, value, fundvalue and fund shares'''
208        pass
209
210    def notify_order(self, order):
211        '''Receives order notifications before each next cycle'''
212        pass
213
214    def notify_trade(self, trade):
215        '''Receives trade notifications before each next cycle'''
216        pass
217
218    def next(self):
219        '''Invoked for each next invocation of the strategy, once the minum
220        preiod of the strategy has been reached'''
221        pass
222
223    def prenext(self):
224        '''Invoked for each prenext invocation of the strategy, until the minimum
225        period of the strategy has been reached
226
227        The default behavior for an analyzer is to invoke ``next``
228        '''
229        self.next()
230
231    def nextstart(self):
232        '''Invoked exactly once for the nextstart invocation of the strategy,
233        when the minimum period has been first reached
234        '''
235        self.next()
236
237    def start(self):
238        '''Invoked to indicate the start of operations, giving the analyzer
239        time to setup up needed things'''
240        pass
241
242    def stop(self):
243        '''Invoked to indicate the end of operations, giving the analyzer
244        time to shut down needed things'''
245        pass
246
247    def create_analysis(self):
248        '''Meant to be overriden by subclasses. Gives a chance to create the
249        structures that hold the analysis.
250
251        The default behaviour is to create a ``OrderedDict`` named ``rets``
252        '''
253        self.rets = OrderedDict()
254
255    def get_analysis(self):
256        '''Returns a *dict-like* object with the results of the analysis
257
258        The keys and format of analysis results in the dictionary is
259        implementation dependent.
260
261        It is not even enforced that the result is a *dict-like object*, just
262        the convention
263
264        The default implementation returns the default OrderedDict ``rets``
265        created by the default ``create_analysis`` method
266
267        '''
268        return self.rets
269
270    def print(self, *args, **kwargs):
271        '''Prints the results returned by ``get_analysis`` via a standard
272        ``Writerfile`` object, which defaults to writing things to standard
273        output
274        '''
275        writer = bt.WriterFile(*args, **kwargs)
276        writer.start()
277        pdct = dict()
278        pdct[self.__class__.__name__] = self.get_analysis()
279        writer.writedict(pdct)
280        writer.stop()
281
282    def pprint(self, *args, **kwargs):
283        '''Prints the results returned by ``get_analysis`` using the pretty
284        print Python module (*pprint*)
285        '''
286        pp.pprint(self.get_analysis(), *args, **kwargs)
287
288
289class MetaTimeFrameAnalyzerBase(Analyzer.__class__):
290    def __new__(meta, name, bases, dct):
291        # Hack to support original method name
292        if '_on_dt_over' in dct:
293            dct['on_dt_over'] = dct.pop('_on_dt_over')  # rename method
294
295        return super(MetaTimeFrameAnalyzerBase, meta).__new__(meta, name,
296                                                              bases, dct)
297
298
299class TimeFrameAnalyzerBase(with_metaclass(MetaTimeFrameAnalyzerBase,
300                                           Analyzer)):
301    params = (
302        ('timeframe', None),
303        ('compression', None),
304        ('_doprenext', True),
305    )
306
307    def _start(self):
308        # Override to add specific attributes
309        self.timeframe = self.p.timeframe or self.data._timeframe
310        self.compression = self.p.compression or self.data._compression
311
312        self.dtcmp, self.dtkey = self._get_dt_cmpkey(datetime.datetime.min)
313        super(TimeFrameAnalyzerBase, self)._start()
314
315    def _prenext(self):
316        for child in self._children:
317            child._prenext()
318
319        if self._dt_over():
320            self.on_dt_over()
321
322        if self.p._doprenext:
323            self.prenext()
324
325    def _nextstart(self):
326        for child in self._children:
327            child._nextstart()
328
329        if self._dt_over() or not self.p._doprenext:  # exec if no prenext
330            self.on_dt_over()
331
332        self.nextstart()
333
334    def _next(self):
335        for child in self._children:
336            child._next()
337
338        if self._dt_over():
339            self.on_dt_over()
340
341        self.next()
342
343    def on_dt_over(self):
344        pass
345
346    def _dt_over(self):
347        if self.timeframe == TimeFrame.NoTimeFrame:
348            dtcmp, dtkey = MAXINT, datetime.datetime.max
349        else:
350            # With >= 1.9.x the system datetime is in the strategy
351            dt = self.strategy.datetime.datetime()
352            dtcmp, dtkey = self._get_dt_cmpkey(dt)
353
354        if self.dtcmp is None or dtcmp > self.dtcmp:
355            self.dtkey, self.dtkey1 = dtkey, self.dtkey
356            self.dtcmp, self.dtcmp1 = dtcmp, self.dtcmp
357            return True
358
359        return False
360
361    def _get_dt_cmpkey(self, dt):
362        if self.timeframe == TimeFrame.NoTimeFrame:
363            return None, None
364
365        if self.timeframe == TimeFrame.Years:
366            dtcmp = dt.year
367            dtkey = datetime.date(dt.year, 12, 31)
368
369        elif self.timeframe == TimeFrame.Months:
370            dtcmp = dt.year * 100 + dt.month
371            _, lastday = calendar.monthrange(dt.year, dt.month)
372            dtkey = datetime.datetime(dt.year, dt.month, lastday)
373
374        elif self.timeframe == TimeFrame.Weeks:
375            isoyear, isoweek, isoweekday = dt.isocalendar()
376            dtcmp = isoyear * 100 + isoweek
377            sunday = dt + datetime.timedelta(days=7 - isoweekday)
378            dtkey = datetime.datetime(sunday.year, sunday.month, sunday.day)
379
380        elif self.timeframe == TimeFrame.Days:
381            dtcmp = dt.year * 10000 + dt.month * 100 + dt.day
382            dtkey = datetime.datetime(dt.year, dt.month, dt.day)
383
384        else:
385            dtcmp, dtkey = self._get_subday_cmpkey(dt)
386
387        return dtcmp, dtkey
388
389    def _get_subday_cmpkey(self, dt):
390        # Calculate intraday position
391        point = dt.hour * 60 + dt.minute
392
393        if self.timeframe < TimeFrame.Minutes:
394            point = point * 60 + dt.second
395
396        if self.timeframe < TimeFrame.Seconds:
397            point = point * 1e6 + dt.microsecond
398
399        # Apply compression to update point position (comp 5 -> 200 // 5)
400        point = point // self.compression
401
402        # Move to next boundary
403        point += 1
404
405        # Restore point to the timeframe units by de-applying compression
406        point *= self.compression
407
408        # Get hours, minutes, seconds and microseconds
409        if self.timeframe == TimeFrame.Minutes:
410            ph, pm = divmod(point, 60)
411            ps = 0
412            pus = 0
413        elif self.timeframe == TimeFrame.Seconds:
414            ph, pm = divmod(point, 60 * 60)
415            pm, ps = divmod(pm, 60)
416            pus = 0
417        elif self.timeframe == TimeFrame.MicroSeconds:
418            ph, pm = divmod(point, 60 * 60 * 1e6)
419            pm, psec = divmod(pm, 60 * 1e6)
420            ps, pus = divmod(psec, 1e6)
421
422        extradays = 0
423        if ph > 23:  # went over midnight:
424            extradays = ph // 24
425            ph %= 24
426
427        # moving 1 minor unit to the left to be in the boundary
428        # pm -= self.timeframe == TimeFrame.Minutes
429        # ps -= self.timeframe == TimeFrame.Seconds
430        # pus -= self.timeframe == TimeFrame.MicroSeconds
431
432        tadjust = datetime.timedelta(
433            minutes=self.timeframe == TimeFrame.Minutes,
434            seconds=self.timeframe == TimeFrame.Seconds,
435            microseconds=self.timeframe == TimeFrame.MicroSeconds)
436
437        # Replace intraday parts with the calculated ones and update it
438        dtcmp = dt.replace(hour=ph, minute=pm, second=ps, microsecond=pus)
439        dtcmp -= tadjust
440        if extradays:
441            dt += datetime.timedelta(days=extradays)
442        dtkey = dtcmp
443
444        return dtcmp, dtkey
445