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 24 25from datetime import datetime 26 27import backtrader as bt 28 29 30class MetaRollOver(bt.DataBase.__class__): 31 def __init__(cls, name, bases, dct): 32 '''Class has already been created ... register''' 33 # Initialize the class 34 super(MetaRollOver, cls).__init__(name, bases, dct) 35 36 def donew(cls, *args, **kwargs): 37 '''Intercept const. to copy timeframe/compression from 1st data''' 38 # Create the object and set the params in place 39 _obj, args, kwargs = super(MetaRollOver, cls).donew(*args, **kwargs) 40 41 if args: 42 _obj.p.timeframe = args[0]._timeframe 43 _obj.p.compression = args[0]._compression 44 45 return _obj, args, kwargs 46 47 48class RollOver(bt.with_metaclass(MetaRollOver, bt.DataBase)): 49 '''Class that rolls over to the next future when a condition is met 50 51 Params: 52 53 - ``checkdate`` (default: ``None``) 54 55 This must be a *callable* with the following signature:: 56 57 checkdate(dt, d): 58 59 Where: 60 61 - ``dt`` is a ``datetime.datetime`` object 62 - ``d`` is the current data feed for the active future 63 64 Expected Return Values: 65 66 - ``True``: as long as the callable returns this, a switchover can 67 happen to the next future 68 69 If a commodity expires on the 3rd Friday of March, ``checkdate`` could 70 return ``True`` for the entire week in which the expiration takes 71 place. 72 73 - ``False``: the expiration cannot take place 74 75 - ``checkcondition`` (default: ``None``) 76 77 **Note**: This will only be called if ``checkdate`` has returned 78 ``True`` 79 80 If ``None`` this will evaluate to ``True`` (execute roll over) 81 internally 82 83 Else this must be a *callable* with this signature:: 84 85 checkcondition(d0, d1) 86 87 Where: 88 89 - ``d0`` is the current data feed for the active future 90 - ``d1`` is the data feed for the next expiration 91 92 Expected Return Values: 93 94 - ``True``: roll-over to the next future 95 96 Following with the example from ``checkdate``, this could say that the 97 roll-over can only happend if the *volume* from ``d0`` is already less 98 than the volume from ``d1`` 99 100 - ``False``: the expiration cannot take place 101 ''' 102 103 params = ( 104 # ('rolls', []), # array of futures to roll over 105 ('checkdate', None), # callable 106 ('checkcondition', None), # callable 107 ) 108 109 def islive(self): 110 '''Returns ``True`` to notify ``Cerebro`` that preloading and runonce 111 should be deactivated''' 112 return True 113 114 def __init__(self, *args): 115 self._rolls = args 116 117 def start(self): 118 super(RollOver, self).start() 119 for d in self._rolls: 120 d.setenvironment(self._env) 121 d._start() 122 123 # put the references in a separate list to have pops 124 self._ds = list(self._rolls) 125 self._d = self._ds.pop(0) if self._ds else None 126 self._dexp = None 127 self._dts = [datetime.min for xx in self._ds] 128 129 def stop(self): 130 super(RollOver, self).stop() 131 for d in self._rolls: 132 d.stop() 133 134 def _gettz(self): 135 '''To be overriden by subclasses which may auto-calculate the 136 timezone''' 137 if self._rolls: 138 return self._rolls[0]._gettz() 139 return bt.utils.date.Localizer(self.p.tz) 140 141 def _checkdate(self, dt, d): 142 if self.p.checkdate is not None: 143 return self.p.checkdate(dt, d) 144 145 return False 146 147 def _checkcondition(self, d0, d1): 148 if self.p.checkcondition is not None: 149 return self.p.checkcondition(d0, d1) 150 151 return True 152 153 def _load(self): 154 while self._d is not None: 155 _next = self._d.next() 156 if _next is None: # no values yet, more will come 157 continue 158 if _next is False: # no values from current data src 159 if self._ds: 160 self._d = self._ds.pop(0) 161 self._dts.pop(0) 162 else: 163 self._d = None 164 continue 165 166 dt0 = self._d.datetime.datetime() # current dt for active data 167 168 # Synchronize other datas using dt0 169 for i, d_dt in enumerate(zip(self._ds, self._dts)): 170 d, dt = d_dt 171 while dt < dt0: 172 if d.next() is None: 173 continue 174 self._dts[i] = dt = d.datetime.datetime() 175 176 # Move expired future as much as needed 177 while self._dexp is not None: 178 if not self._dexp.next(): 179 self._dexp = None 180 break 181 182 if self._dexp.datetime.datetime() < dt0: 183 continue 184 185 if self._dexp is None and self._checkdate(dt0, self._d): 186 # rule has been met ... check other factors only if 2 datas 187 # still there 188 if self._ds and self._checkcondition(self._d, self._ds[0]): 189 # Time to switch to next data 190 self._dexp = self._d 191 self._d = self._ds.pop(0) 192 self._dts.pop(0) 193 194 # Fill the line and tell we die 195 self.lines.datetime[0] = self._d.lines.datetime[0] 196 self.lines.open[0] = self._d.lines.open[0] 197 self.lines.high[0] = self._d.lines.high[0] 198 self.lines.low[0] = self._d.lines.low[0] 199 self.lines.close[0] = self._d.lines.close[0] 200 self.lines.volume[0] = self._d.lines.volume[0] 201 self.lines.openinterest[0] = self._d.lines.openinterest[0] 202 return True 203 204 # Out of the loop -> self._d is None, no data feed to return from 205 return False 206