backtrader2 / backtrader

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

Modify ibstore.py to fix #41: IBStore.dt_plus_duration calculates month offset incorrectly #55

Closed FGU1 closed 3 years ago

FGU1 commented 3 years ago

Hi, Just a small modification of ibstore.py to fix https://github.com/backtrader2/backtrader/issues/41 Month offset was done by changing the month number, which caused problem when for ex. the 31th of January was offset to February.

neilsmurphy commented 3 years ago

I've tested this by finding all of the bad dates in one year, and then running them with the new code.

from datetime import timedelta, datetime

def dt_plus_duration(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)
        return dt.replace(year=dt.year + years, month=month + 1, day=1) + timedelta(dt.day - 1)

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

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

if __name__ == '__main__':

    dates = [
        [datetime(2020, 7, 31), datetime(2020,  10, 1)],
        [datetime(2020, 12, 29), datetime(2021, 3, 1)],
        [datetime(2020, 12, 30), datetime(2021, 3, 2)],
        [datetime(2020, 12, 31), datetime(2021, 3, 3)],
    ]

    for date in dates:
        calulate_date = dt_plus_duration(date[0], "2 M")
        assert calulate_date == date[1]
        print(f"Input date: {date[0].date()} Calculated date: "
              f"{dt_plus_duration(date[0], '2 M').date()} Output date:{date[1].date()}")

Output:

Input date: 2020-07-31 Calculated date: 2020-10-01 Output date:2020-10-01
Input date: 2020-12-29 Calculated date: 2021-03-01 Output date:2021-03-01
Input date: 2020-12-30 Calculated date: 2021-03-02 Output date:2021-03-02
Input date: 2020-12-31 Calculated date: 2021-03-03 Output date:2021-03-03

This looks good to me. @vladisld Do you think we need a more integrated test?

vladisld commented 3 years ago

I would add an additional problematic dates:

    [datetime(2019, 12, 29), datetime(2020, 2, 29)],
    [datetime(2019, 12, 30), datetime(2020, 3, 1)],
    [datetime(2019, 12, 31), datetime(2020, 3, 2)],

Otherwise LGTM.

neilsmurphy commented 3 years ago

OK, dates then are:

dates = [
    [datetime(2020, 7, 31), datetime(2020,  10, 1)],
    [datetime(2020, 12, 29), datetime(2021, 3, 1)],
    [datetime(2020, 12, 30), datetime(2021, 3, 2)],
    [datetime(2020, 12, 31), datetime(2021, 3, 3)],
    [datetime(2019, 12, 29), datetime(2020, 2, 29)],
    [datetime(2019, 12, 30), datetime(2020, 3, 1)],
    [datetime(2019, 12, 31), datetime(2020, 3, 2)],
]

And results are:

Input date: 2020-07-31 Calculated date: 2020-10-01 Output date:2020-10-01
Input date: 2020-12-29 Calculated date: 2021-03-01 Output date:2021-03-01
Input date: 2020-12-30 Calculated date: 2021-03-02 Output date:2021-03-02
Input date: 2020-12-31 Calculated date: 2021-03-03 Output date:2021-03-03
Input date: 2019-12-29 Calculated date: 2020-02-29 Output date:2020-02-29
Input date: 2019-12-30 Calculated date: 2020-03-01 Output date:2020-03-01
Input date: 2019-12-31 Calculated date: 2020-03-02 Output date:2020-03-02

I will go ahead and merge.

vladisld commented 3 years ago

I don't see the above test committed as part of the fix and running as part of Travis CI - am I missing something ?

neilsmurphy commented 3 years ago

No you didn't miss it. I was asking above if the local test was good enough. I'm not sure how to test IBStores in Travis. Could you assist? Thanks.

vladisld commented 3 years ago

Sure, np, I'll try to integrate it.

FGU1 commented 3 years ago

I guess this test would be ok ?

import datetime as dt

store = bt.stores.IBStore()

test_cases = [
            (dt.datetime(2020, 7, 31), '2 M', dt.datetime(2020,  10, 1)),
            (dt.datetime(2020, 12, 29), '2 M', dt.datetime(2021, 3, 1)),
            (dt.datetime(2020, 12, 30), '2 M', dt.datetime(2021, 3, 2)),
            (dt.datetime(2020, 12, 31), '2 M', dt.datetime(2021, 3, 3)),
            (dt.datetime(2019, 12, 29), '2 M', dt.datetime(2020, 2, 29)),
            (dt.datetime(2019, 12, 30), '2 M', dt.datetime(2020, 3, 1)),
            (dt.datetime(2019, 12, 31), '2 M', dt.datetime(2020, 3, 2)),
            (dt.datetime(1999, 12, 31), '2 M', dt.datetime(2000, 3, 2)),
            (dt.datetime(2099, 12, 31), '2 M', dt.datetime(2100, 3, 3))
            ]

def test_run():
    for src_dt, duration_str, trg_dt in test_cases:
        calculated_dt = store.dt_plus_duration(src_dt, duration_str)
        assert calculated_dt == trg_dt
vladisld commented 3 years ago

yep, the only small issue I see is to run it in Travis - for that you need to instruct Travis CI to import the ibpy package first - otherwise IBStore class will not be available for you.