|
| 1 | +import datetime |
| 2 | +from unittest.mock import patch |
| 3 | + |
| 4 | +import pytest |
| 5 | + |
| 6 | +from lumibot.strategies import Strategy |
| 7 | +from lumibot.strategies._strategy import _Strategy |
| 8 | + |
| 9 | + |
| 10 | +class MinimalStrategy(Strategy): |
| 11 | + """No-op strategy used for backtest scaffolding.""" |
| 12 | + |
| 13 | + def initialize(self): |
| 14 | + self.sleeptime = "1D" |
| 15 | + |
| 16 | + def on_trading_iteration(self): |
| 17 | + pass |
| 18 | + |
| 19 | + |
| 20 | +class DummyDataSource: |
| 21 | + """Lightweight datasource stub capturing the start/end datetimes.""" |
| 22 | + |
| 23 | + SOURCE = "dummy" |
| 24 | + |
| 25 | + def __init__(self, datetime_start=None, datetime_end=None, **kwargs): |
| 26 | + self.datetime_start = datetime_start |
| 27 | + self.datetime_end = datetime_end |
| 28 | + self._data_store = {} |
| 29 | + |
| 30 | + |
| 31 | +class DummyTrader: |
| 32 | + """Trader stub that records strategies and returns canned results.""" |
| 33 | + |
| 34 | + def __init__(self, *args, **kwargs): |
| 35 | + self._strategies = [] |
| 36 | + |
| 37 | + def add_strategy(self, strategy): |
| 38 | + self._strategies.append(strategy) |
| 39 | + |
| 40 | + def run_all(self, **_kwargs): |
| 41 | + return {strategy.name: {"dummy": True} for strategy in self._strategies} |
| 42 | + |
| 43 | + |
| 44 | +class _EarlyExit(Exception): |
| 45 | + """Signal to stop run_backtest after the datasource is constructed.""" |
| 46 | + |
| 47 | + |
| 48 | +def test_verify_backtest_inputs_accepts_mixed_timezones(): |
| 49 | + """Regression: verify_backtest_inputs must not crash on naive vs aware inputs.""" |
| 50 | + naive_start = datetime.datetime(2025, 1, 1) |
| 51 | + aware_end = datetime.datetime(2025, 9, 30, tzinfo=datetime.timezone.utc) |
| 52 | + |
| 53 | + # Should not raise |
| 54 | + _Strategy.verify_backtest_inputs(naive_start, aware_end) |
| 55 | + |
| 56 | + |
| 57 | +def test_run_backtest_normalizes_mixed_timezones(): |
| 58 | + """Strategy.run_backtest should normalize naive/aware datetimes before validation.""" |
| 59 | + naive_start = datetime.datetime(2025, 1, 1) |
| 60 | + aware_end = datetime.datetime(2025, 9, 30, tzinfo=datetime.timezone.utc) |
| 61 | + |
| 62 | + captured = {} |
| 63 | + |
| 64 | + class CapturingDataSource(DummyDataSource): |
| 65 | + def __init__(self, datetime_start=None, datetime_end=None, **kwargs): |
| 66 | + super().__init__(datetime_start=datetime_start, datetime_end=datetime_end, **kwargs) |
| 67 | + captured["start"] = self.datetime_start |
| 68 | + captured["end"] = self.datetime_end |
| 69 | + |
| 70 | + def broker_factory(data_source, *args, **kwargs): |
| 71 | + captured["data_source"] = data_source |
| 72 | + raise _EarlyExit |
| 73 | + |
| 74 | + with patch("lumibot.strategies._strategy.BacktestingBroker", side_effect=broker_factory), \ |
| 75 | + patch("lumibot.strategies._strategy.Trader", DummyTrader): |
| 76 | + with pytest.raises(_EarlyExit): |
| 77 | + MinimalStrategy.run_backtest( |
| 78 | + CapturingDataSource, |
| 79 | + backtesting_start=naive_start, |
| 80 | + backtesting_end=aware_end, |
| 81 | + show_plot=False, |
| 82 | + show_tearsheet=False, |
| 83 | + show_indicators=False, |
| 84 | + show_progress_bar=False, |
| 85 | + save_logfile=False, |
| 86 | + save_stats_file=False, |
| 87 | + ) |
| 88 | + |
| 89 | + assert "start" in captured and captured["start"].tzinfo is not None |
| 90 | + assert "end" in captured and captured["end"].tzinfo is not None |
0 commit comments