Skip to content

Commit 940a61b

Browse files
authored
Merge pull request #888 from Lumiwealth/fix/backtest-datetime-normalization
Fix/backtest datetime normalization
2 parents b644da9 + d23df7e commit 940a61b

File tree

3 files changed

+27
-5
lines changed

3 files changed

+27
-5
lines changed

lumibot/strategies/_strategy.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,21 @@ def all(self):
124124

125125

126126
class _Strategy:
127+
@staticmethod
128+
def _normalize_backtest_datetime(value):
129+
"""Ensure backtest boundary datetimes are timezone-aware.
130+
131+
Naive datetimes are localized to the LumiBot default timezone; timezone-aware
132+
inputs are returned unchanged so their original offsets are preserved.
133+
"""
134+
if value is None:
135+
return None
136+
if isinstance(value, datetime.datetime) and (
137+
value.tzinfo is None or value.tzinfo.utcoffset(value) is None
138+
):
139+
return to_datetime_aware(value)
140+
return value
141+
127142
@property
128143
def is_backtesting(self) -> bool:
129144
"""Boolean flag indicating whether the strategy is running in backtesting mode."""
@@ -1389,8 +1404,8 @@ def run_backtest(
13891404
raise ValueError(f"`optionsource_class` must be a class. You passed in {optionsource_class}")
13901405

13911406
try:
1392-
backtesting_start = to_datetime_aware(backtesting_start)
1393-
backtesting_end = to_datetime_aware(backtesting_end)
1407+
backtesting_start = self._normalize_backtest_datetime(backtesting_start)
1408+
backtesting_end = self._normalize_backtest_datetime(backtesting_end)
13941409
except AttributeError:
13951410
get_logger(__name__).error(
13961411
"`backtesting_start` and `backtesting_end` must be datetime objects. \n"
@@ -1399,6 +1414,9 @@ def run_backtest(
13991414
)
14001415
return None
14011416

1417+
get_logger(__name__).info("Backtest start = %s", backtesting_start)
1418+
get_logger(__name__).info("Backtest end = %s", backtesting_end)
1419+
14021420
self.verify_backtest_inputs(backtesting_start, backtesting_end)
14031421

14041422
if not self.IS_BACKTESTABLE:
@@ -1628,8 +1646,8 @@ def verify_backtest_inputs(cls, backtesting_start, backtesting_end):
16281646
if not isinstance(backtesting_end, datetime.datetime):
16291647
raise ValueError(f"`backtesting_end` must be a datetime object. You passed in {backtesting_end}")
16301648

1631-
start_dt = to_datetime_aware(backtesting_start)
1632-
end_dt = to_datetime_aware(backtesting_end)
1649+
start_dt = cls._normalize_backtest_datetime(backtesting_start)
1650+
end_dt = cls._normalize_backtest_datetime(backtesting_end)
16331651

16341652
# Check that backtesting end is after backtesting start
16351653
if end_dt <= start_dt:

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="lumibot",
8-
version="4.2.4",
8+
version="4.2.5",
99
author="Robert Grzesik",
1010
author_email="rob@lumiwealth.com",
1111
description="Backtesting and Trading Library, Made by Lumiwealth",

tests/test_backtesting_datetime_normalization.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import pytest
55

6+
from lumibot.constants import LUMIBOT_DEFAULT_PYTZ
67
from lumibot.strategies import Strategy
78
from lumibot.strategies._strategy import _Strategy
89

@@ -88,3 +89,6 @@ def broker_factory(data_source, *args, **kwargs):
8889

8990
assert "start" in captured and captured["start"].tzinfo is not None
9091
assert "end" in captured and captured["end"].tzinfo is not None
92+
assert captured["start"].tzinfo.zone == LUMIBOT_DEFAULT_PYTZ.zone
93+
assert captured["end"].tzinfo.zone == LUMIBOT_DEFAULT_PYTZ.zone
94+
assert captured["start"].tzinfo.zone == captured["end"].tzinfo.zone

0 commit comments

Comments
 (0)