backtrader2 / backtrader

Python Backtesting library for trading strategies
https://www.backtrader.com
GNU General Public License v3.0
238 stars 54 forks source link

IBStore.dt_plus_duration calculates the date offset incorrectly for month durations #41

Closed vladisld closed 3 years ago

vladisld commented 4 years ago

Community discussion: https://community.backtrader.com/topic/3030/exception-has-occurred-valueerror-day-is-out-of-range-for-month-file-f-ibbacktradelivedataex-py-line-165-in-module-cerebro-run

Problem description:

The following ValueError exception may be raised in case the specific fromdate parameter is used to initialize the IBStore:

Traceback (most recent call last):
  File "C:/Users/Vlad/PycharmProjects/test/test_ib_error.py", line 35, in <module>
    cerebro.run()
  File "W:\backtrader\backtrader\cerebro.py", line 1177, in run
    runstrat = self.runstrategies(iterstrat)
  File "W:\backtrader\backtrader\cerebro.py", line 1263, in runstrategies
    data._start()
  File "W:\backtrader\backtrader\feed.py", line 203, in _start
    self.start()
  File "W:\backtrader\backtrader\feeds\ibdata.py", line 406, in start
    self._st_start()
  File "W:\backtrader\backtrader\feeds\ibdata.py", line 650, in _st_start
    sessionend=self.p.sessionend)
  File "W:\backtrader\backtrader\stores\ibstore.py", line 720, in reqHistoricalDataEx
    intdate = self.dt_plus_duration(begindate, dur)
  File "W:\backtrader\backtrader\stores\ibstore.py", line 1191, in dt_plus_duration
    return dt.replace(year=dt.year + years, month=month + 1)
ValueError: day is out of range for month

Sample user code:

import backtrader as bt
import datetime

class IntraTrendStrategy(bt.Strategy):
    def next(self):
        pass

cerebro = bt.Cerebro()
store = bt.stores.IBStore(host="127.0.0.1", port=7497, clientId= 4)
cerebro.addstrategy(IntraTrendStrategy)
stockkwargs = dict(
timeframe=bt.TimeFrame.Minutes,
compression=5,
rtbar=False, # use RealTime 5 seconds bars
historical=True, # only historical download
qcheck=0.5, # timeout in seconds (float) to check for events
fromdate=datetime.datetime(2020, 1, 1), # get data from..
todate=datetime.datetime(2020, 9, 20), # get data from..
latethrough=False, # let late samples through
tradename=None, # use a different asset as order target
tz="Asia/Kolkata"
)
data0 = store.getdata(dataname="TCS-STK-NSE-INR", **stockkwargs)
cerebro.replaydata(data0, timeframe=bt.TimeFrame.Days, compression=1)
cerebro.broker.setcash(100000.0)
cerebro.broker.setcommission(commission=0.001)
cerebro.run()

Analysis:

The problem is with the following code in IBStore.py:

    def dt_plus_duration(self, dt, duration):
        size, dim = duration.split()
        size = int(size)
        if dim == 'S':
            return dt + timedelta(seconds=size)

        if dim == 'D':
            return dt + timedelta(days=size)

        if dim == 'W':
            return dt + timedelta(days=size * 7)

        if dim == 'M':
            month = dt.month - 1 + size  # -1 to make it 0 based, readd below
            years, month = divmod(month, 12)
            return dt.replace(year=dt.year + years, month=month + 1) # <=---- the bug is here

        if dim == 'Y':
            return dt.replace(year=dt.year + size)

        return dt  # could do nothing with it ... return it intact

It's plain wrong to replace the month of the date without taking care of the day of the month. In our case the date '2019.12.31' was replaced with '2020.02.31' which is definitely wrong.

Reduced test case:

import backtrader as bt
import datetime

store = bt.IBStore()
store.dt_plus_duration(datetime(2019,12,31,), '2M')