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