Module backtrader.timer

Expand source code
#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
#
# Copyright (C) 2015-2023 Daniel Rodriguez
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)


import bisect
import collections
from datetime import date, datetime, timedelta
from itertools import islice

from .feed import AbstractDataBase
from .metabase import MetaParams
from .utils import date2num, num2date
from .utils.py3 import integer_types, range, with_metaclass
from .utils import TIME_MAX


__all__ = ['SESSION_TIME', 'SESSION_START', 'SESSION_END', 'Timer']

SESSION_TIME, SESSION_START, SESSION_END = range(3)


class Timer(with_metaclass(MetaParams, object)):
    params = (
        ('tid', None),
        ('owner', None),
        ('strats', False),
        ('when', None),
        ('offset', timedelta()),
        ('repeat', timedelta()),
        ('weekdays', []),
        ('weekcarry', False),
        ('monthdays', []),
        ('monthcarry', True),
        ('allow', None),  # callable that allows a timer to take place
        ('tzdata', None),
        ('cheat', False),
    )

    SESSION_TIME, SESSION_START, SESSION_END = range(3)

    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

    def start(self, data):
        # write down the 'reset when' value
        if not isinstance(self.p.when, integer_types):  # expect time/datetime
            self._rstwhen = self.p.when
            self._tzdata = self.p.tzdata
        else:
            self._tzdata = data if self.p.tzdata is None else self.p.tzdata

            if self.p.when == SESSION_START:
                self._rstwhen = self._tzdata.p.sessionstart
            elif self.p.when == SESSION_END:
                self._rstwhen = self._tzdata.p.sessionend

        self._isdata = isinstance(self._tzdata, AbstractDataBase)
        self._reset_when()

        self._nexteos = datetime.min
        self._curdate = date.min

        self._curmonth = -1  # non-existent month
        self._monthmask = collections.deque()

        self._curweek = -1  # non-existent week
        self._weekmask = collections.deque()

    def _reset_when(self, ddate=datetime.min):
        self._when = self._rstwhen
        self._dtwhen = self._dwhen = None

        self._lastcall = ddate

    def _check_month(self, ddate):
        if not self.p.monthdays:
            return True

        mask = self._monthmask
        daycarry = False
        dmonth = ddate.month
        if dmonth != self._curmonth:
            self._curmonth = dmonth  # write down new month
            daycarry = self.p.monthcarry and bool(mask)
            self._monthmask = mask = collections.deque(self.p.monthdays)

        dday = ddate.day
        dc = bisect.bisect_left(mask, dday)  # "left" for days before dday
        daycarry = daycarry or (self.p.monthcarry and dc > 0)
        if dc < len(mask):
            curday = bisect.bisect_right(mask, dday, lo=dc) > 0  # check dday
            dc += curday
        else:
            curday = False

        while dc:
            mask.popleft()
            dc -= 1

        return daycarry or curday

    def _check_week(self, ddate=date.min):
        if not self.p.weekdays:
            return True

        _, dweek, dwkday = ddate.isocalendar()

        mask = self._weekmask
        daycarry = False
        if dweek != self._curweek:
            self._curweek = dweek  # write down new month
            daycarry = self.p.weekcarry and bool(mask)
            self._weekmask = mask = collections.deque(self.p.weekdays)

        dc = bisect.bisect_left(mask, dwkday)  # "left" for days before dday
        daycarry = daycarry or (self.p.weekcarry and dc > 0)
        if dc < len(mask):
            curday = bisect.bisect_right(mask, dwkday, lo=dc) > 0  # check dday
            dc += curday
        else:
            curday = False

        while dc:
            mask.popleft()
            dc -= 1

        return daycarry or curday

    def check(self, dt):
        d = num2date(dt)
        ddate = d.date()
        if self._lastcall == ddate:  # not repeating, awaiting date change
            return False

        if d > self._nexteos:
            if self._isdata:  # eos provided by data
                nexteos, _ = self._tzdata._getnexteos()
            else:  # generic eos
                nexteos = datetime.combine(ddate, TIME_MAX)
            self._nexteos = nexteos
            self._reset_when()

        if ddate > self._curdate:  # day change
            self._curdate = ddate
            ret = self._check_month(ddate)
            if ret:
                ret = self._check_week(ddate)
            if ret and self.p.allow is not None:
                ret = self.p.allow(ddate)

            if not ret:
                self._reset_when(ddate)  # this day won't make it
                return False  # timer target not met

        # no day change or passed month, week and allow filters on date change
        dwhen = self._dwhen
        dtwhen = self._dtwhen
        if dtwhen is None:
            dwhen = datetime.combine(ddate, self._when)
            if self.p.offset:
                dwhen += self.p.offset

            self._dwhen = dwhen

            if self._isdata:
                self._dtwhen = dtwhen = self._tzdata.date2num(dwhen)
            else:
                self._dtwhen = dtwhen = date2num(dwhen, tz=self._tzdata)

        if dt < dtwhen:
            return False  # timer target not met

        self.lastwhen = dwhen  # record when the last timer "when" happened

        if not self.p.repeat:  # cannot repeat
            self._reset_when(ddate)  # reset and mark as called on ddate
        else:
            if d > self._nexteos:
                if self._isdata:  # eos provided by data
                    nexteos, _ = self._tzdata._getnexteos()
                else:  # generic eos
                    nexteos = datetime.combine(ddate, TIME_MAX)

                self._nexteos = nexteos
            else:
                nexteos = self._nexteos

            while True:
                dwhen += self.p.repeat
                if dwhen > nexteos:  # new schedule is beyone session
                    self._reset_when(ddate)  # reset to original point
                    break

                if dwhen > d:  # gone over current datetime
                    self._dtwhen = dtwhen = date2num(dwhen)  # float timestamp
                    # Get the localized expected next time
                    if self._isdata:
                        self._dwhen = self._tzdata.num2date(dtwhen)
                    else:  # assume pytz compatible or None
                        self._dwhen = num2date(dtwhen, tz=self._tzdata)

                    break

        return True  # timer target was met

Classes

class Timer (*args, **kwargs)
Expand source code
class Timer(with_metaclass(MetaParams, object)):
    params = (
        ('tid', None),
        ('owner', None),
        ('strats', False),
        ('when', None),
        ('offset', timedelta()),
        ('repeat', timedelta()),
        ('weekdays', []),
        ('weekcarry', False),
        ('monthdays', []),
        ('monthcarry', True),
        ('allow', None),  # callable that allows a timer to take place
        ('tzdata', None),
        ('cheat', False),
    )

    SESSION_TIME, SESSION_START, SESSION_END = range(3)

    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

    def start(self, data):
        # write down the 'reset when' value
        if not isinstance(self.p.when, integer_types):  # expect time/datetime
            self._rstwhen = self.p.when
            self._tzdata = self.p.tzdata
        else:
            self._tzdata = data if self.p.tzdata is None else self.p.tzdata

            if self.p.when == SESSION_START:
                self._rstwhen = self._tzdata.p.sessionstart
            elif self.p.when == SESSION_END:
                self._rstwhen = self._tzdata.p.sessionend

        self._isdata = isinstance(self._tzdata, AbstractDataBase)
        self._reset_when()

        self._nexteos = datetime.min
        self._curdate = date.min

        self._curmonth = -1  # non-existent month
        self._monthmask = collections.deque()

        self._curweek = -1  # non-existent week
        self._weekmask = collections.deque()

    def _reset_when(self, ddate=datetime.min):
        self._when = self._rstwhen
        self._dtwhen = self._dwhen = None

        self._lastcall = ddate

    def _check_month(self, ddate):
        if not self.p.monthdays:
            return True

        mask = self._monthmask
        daycarry = False
        dmonth = ddate.month
        if dmonth != self._curmonth:
            self._curmonth = dmonth  # write down new month
            daycarry = self.p.monthcarry and bool(mask)
            self._monthmask = mask = collections.deque(self.p.monthdays)

        dday = ddate.day
        dc = bisect.bisect_left(mask, dday)  # "left" for days before dday
        daycarry = daycarry or (self.p.monthcarry and dc > 0)
        if dc < len(mask):
            curday = bisect.bisect_right(mask, dday, lo=dc) > 0  # check dday
            dc += curday
        else:
            curday = False

        while dc:
            mask.popleft()
            dc -= 1

        return daycarry or curday

    def _check_week(self, ddate=date.min):
        if not self.p.weekdays:
            return True

        _, dweek, dwkday = ddate.isocalendar()

        mask = self._weekmask
        daycarry = False
        if dweek != self._curweek:
            self._curweek = dweek  # write down new month
            daycarry = self.p.weekcarry and bool(mask)
            self._weekmask = mask = collections.deque(self.p.weekdays)

        dc = bisect.bisect_left(mask, dwkday)  # "left" for days before dday
        daycarry = daycarry or (self.p.weekcarry and dc > 0)
        if dc < len(mask):
            curday = bisect.bisect_right(mask, dwkday, lo=dc) > 0  # check dday
            dc += curday
        else:
            curday = False

        while dc:
            mask.popleft()
            dc -= 1

        return daycarry or curday

    def check(self, dt):
        d = num2date(dt)
        ddate = d.date()
        if self._lastcall == ddate:  # not repeating, awaiting date change
            return False

        if d > self._nexteos:
            if self._isdata:  # eos provided by data
                nexteos, _ = self._tzdata._getnexteos()
            else:  # generic eos
                nexteos = datetime.combine(ddate, TIME_MAX)
            self._nexteos = nexteos
            self._reset_when()

        if ddate > self._curdate:  # day change
            self._curdate = ddate
            ret = self._check_month(ddate)
            if ret:
                ret = self._check_week(ddate)
            if ret and self.p.allow is not None:
                ret = self.p.allow(ddate)

            if not ret:
                self._reset_when(ddate)  # this day won't make it
                return False  # timer target not met

        # no day change or passed month, week and allow filters on date change
        dwhen = self._dwhen
        dtwhen = self._dtwhen
        if dtwhen is None:
            dwhen = datetime.combine(ddate, self._when)
            if self.p.offset:
                dwhen += self.p.offset

            self._dwhen = dwhen

            if self._isdata:
                self._dtwhen = dtwhen = self._tzdata.date2num(dwhen)
            else:
                self._dtwhen = dtwhen = date2num(dwhen, tz=self._tzdata)

        if dt < dtwhen:
            return False  # timer target not met

        self.lastwhen = dwhen  # record when the last timer "when" happened

        if not self.p.repeat:  # cannot repeat
            self._reset_when(ddate)  # reset and mark as called on ddate
        else:
            if d > self._nexteos:
                if self._isdata:  # eos provided by data
                    nexteos, _ = self._tzdata._getnexteos()
                else:  # generic eos
                    nexteos = datetime.combine(ddate, TIME_MAX)

                self._nexteos = nexteos
            else:
                nexteos = self._nexteos

            while True:
                dwhen += self.p.repeat
                if dwhen > nexteos:  # new schedule is beyone session
                    self._reset_when(ddate)  # reset to original point
                    break

                if dwhen > d:  # gone over current datetime
                    self._dtwhen = dtwhen = date2num(dwhen)  # float timestamp
                    # Get the localized expected next time
                    if self._isdata:
                        self._dwhen = self._tzdata.num2date(dtwhen)
                    else:  # assume pytz compatible or None
                        self._dwhen = num2date(dtwhen, tz=self._tzdata)

                    break

        return True  # timer target was met

Class variables

var SESSION_END
var SESSION_START
var SESSION_TIME
var frompackages
var packages
var params

Methods

def check(self, dt)
Expand source code
def check(self, dt):
    d = num2date(dt)
    ddate = d.date()
    if self._lastcall == ddate:  # not repeating, awaiting date change
        return False

    if d > self._nexteos:
        if self._isdata:  # eos provided by data
            nexteos, _ = self._tzdata._getnexteos()
        else:  # generic eos
            nexteos = datetime.combine(ddate, TIME_MAX)
        self._nexteos = nexteos
        self._reset_when()

    if ddate > self._curdate:  # day change
        self._curdate = ddate
        ret = self._check_month(ddate)
        if ret:
            ret = self._check_week(ddate)
        if ret and self.p.allow is not None:
            ret = self.p.allow(ddate)

        if not ret:
            self._reset_when(ddate)  # this day won't make it
            return False  # timer target not met

    # no day change or passed month, week and allow filters on date change
    dwhen = self._dwhen
    dtwhen = self._dtwhen
    if dtwhen is None:
        dwhen = datetime.combine(ddate, self._when)
        if self.p.offset:
            dwhen += self.p.offset

        self._dwhen = dwhen

        if self._isdata:
            self._dtwhen = dtwhen = self._tzdata.date2num(dwhen)
        else:
            self._dtwhen = dtwhen = date2num(dwhen, tz=self._tzdata)

    if dt < dtwhen:
        return False  # timer target not met

    self.lastwhen = dwhen  # record when the last timer "when" happened

    if not self.p.repeat:  # cannot repeat
        self._reset_when(ddate)  # reset and mark as called on ddate
    else:
        if d > self._nexteos:
            if self._isdata:  # eos provided by data
                nexteos, _ = self._tzdata._getnexteos()
            else:  # generic eos
                nexteos = datetime.combine(ddate, TIME_MAX)

            self._nexteos = nexteos
        else:
            nexteos = self._nexteos

        while True:
            dwhen += self.p.repeat
            if dwhen > nexteos:  # new schedule is beyone session
                self._reset_when(ddate)  # reset to original point
                break

            if dwhen > d:  # gone over current datetime
                self._dtwhen = dtwhen = date2num(dwhen)  # float timestamp
                # Get the localized expected next time
                if self._isdata:
                    self._dwhen = self._tzdata.num2date(dtwhen)
                else:  # assume pytz compatible or None
                    self._dwhen = num2date(dtwhen, tz=self._tzdata)

                break

    return True  # timer target was met
def start(self, data)
Expand source code
def start(self, data):
    # write down the 'reset when' value
    if not isinstance(self.p.when, integer_types):  # expect time/datetime
        self._rstwhen = self.p.when
        self._tzdata = self.p.tzdata
    else:
        self._tzdata = data if self.p.tzdata is None else self.p.tzdata

        if self.p.when == SESSION_START:
            self._rstwhen = self._tzdata.p.sessionstart
        elif self.p.when == SESSION_END:
            self._rstwhen = self._tzdata.p.sessionend

    self._isdata = isinstance(self._tzdata, AbstractDataBase)
    self._reset_when()

    self._nexteos = datetime.min
    self._curdate = date.min

    self._curmonth = -1  # non-existent month
    self._monthmask = collections.deque()

    self._curweek = -1  # non-existent week
    self._weekmask = collections.deque()