lumibot 4.2.3__py3-none-any.whl → 4.2.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of lumibot might be problematic. Click here for more details.

@@ -124,6 +124,17 @@ class Vars:
124
124
 
125
125
 
126
126
  class _Strategy:
127
+ @staticmethod
128
+ def _normalize_backtest_datetime(value):
129
+ """Convert backtest boundary datetimes to the LumiBot default timezone."""
130
+ if value is None:
131
+ return None
132
+ aware = to_datetime_aware(value)
133
+ tzinfo = getattr(aware, "tzinfo", None)
134
+ if tzinfo is not None and tzinfo != LUMIBOT_DEFAULT_PYTZ:
135
+ return aware.astimezone(LUMIBOT_DEFAULT_PYTZ)
136
+ return aware
137
+
127
138
  @property
128
139
  def is_backtesting(self) -> bool:
129
140
  """Boolean flag indicating whether the strategy is running in backtesting mode."""
@@ -1388,15 +1399,9 @@ class _Strategy:
1388
1399
  if use_other_option_source and not isinstance(optionsource_class, type):
1389
1400
  raise ValueError(f"`optionsource_class` must be a class. You passed in {optionsource_class}")
1390
1401
 
1391
- self.verify_backtest_inputs(backtesting_start, backtesting_end)
1392
-
1393
- if not self.IS_BACKTESTABLE:
1394
- get_logger(__name__).warning(f"Strategy {name + ' ' if name is not None else ''}cannot be " f"backtested at the moment")
1395
- return None
1396
-
1397
1402
  try:
1398
- backtesting_start = to_datetime_aware(backtesting_start)
1399
- backtesting_end = to_datetime_aware(backtesting_end)
1403
+ backtesting_start = self._normalize_backtest_datetime(backtesting_start)
1404
+ backtesting_end = self._normalize_backtest_datetime(backtesting_end)
1400
1405
  except AttributeError:
1401
1406
  get_logger(__name__).error(
1402
1407
  "`backtesting_start` and `backtesting_end` must be datetime objects. \n"
@@ -1405,6 +1410,15 @@ class _Strategy:
1405
1410
  )
1406
1411
  return None
1407
1412
 
1413
+ get_logger(__name__).info("Backtest start = %s", backtesting_start)
1414
+ get_logger(__name__).info("Backtest end = %s", backtesting_end)
1415
+
1416
+ self.verify_backtest_inputs(backtesting_start, backtesting_end)
1417
+
1418
+ if not self.IS_BACKTESTABLE:
1419
+ get_logger(__name__).warning(f"Strategy {name + ' ' if name is not None else ''}cannot be " f"backtested at the moment")
1420
+ return None
1421
+
1408
1422
  if BACKTESTING_QUIET_LOGS is not None:
1409
1423
  quiet_logs = BACKTESTING_QUIET_LOGS
1410
1424
 
@@ -1628,18 +1642,21 @@ class _Strategy:
1628
1642
  if not isinstance(backtesting_end, datetime.datetime):
1629
1643
  raise ValueError(f"`backtesting_end` must be a datetime object. You passed in {backtesting_end}")
1630
1644
 
1645
+ start_dt = cls._normalize_backtest_datetime(backtesting_start)
1646
+ end_dt = cls._normalize_backtest_datetime(backtesting_end)
1647
+
1631
1648
  # Check that backtesting end is after backtesting start
1632
- if backtesting_end <= backtesting_start:
1649
+ if end_dt <= start_dt:
1633
1650
  raise ValueError(
1634
1651
  f"`backtesting_end` must be after `backtesting_start`. You passed in "
1635
- f"{backtesting_end} and {backtesting_start}"
1652
+ f"{end_dt} and {start_dt}"
1636
1653
  )
1637
1654
 
1638
1655
  # Check that backtesting_end is not in the future
1639
- now = datetime.datetime.now(backtesting_end.tzinfo) if backtesting_end.tzinfo else datetime.datetime.now()
1640
- if backtesting_end > now:
1656
+ now = datetime.datetime.now(end_dt.tzinfo) if end_dt.tzinfo else datetime.datetime.now()
1657
+ if end_dt > now:
1641
1658
  raise ValueError(
1642
- f"`backtesting_end` cannot be in the future. You passed in {backtesting_end}, now is {now}"
1659
+ f"`backtesting_end` cannot be in the future. You passed in {end_dt}, now is {now}"
1643
1660
  )
1644
1661
 
1645
1662
  def send_update_to_cloud(self):
@@ -13,8 +13,6 @@ import pandas_market_calendars as mcal
13
13
  from apscheduler.jobstores.memory import MemoryJobStore
14
14
  from apscheduler.schedulers.background import BackgroundScheduler
15
15
  from apscheduler.triggers.cron import CronTrigger
16
- from termcolor import colored
17
-
18
16
  from lumibot.constants import LUMIBOT_DEFAULT_PYTZ
19
17
  from lumibot.entities import Asset, Order
20
18
  from lumibot.entities import Asset
@@ -1166,7 +1164,7 @@ class StrategyExecutor(Thread):
1166
1164
  # For live trading, stop when market closes
1167
1165
  return False
1168
1166
 
1169
- self.strategy.log_message(colored(f"Sleeping for {strategy_sleeptime} seconds", color="blue"))
1167
+ self.strategy.logger.debug("Sleeping for %s seconds", strategy_sleeptime)
1170
1168
 
1171
1169
  # Run process orders at the market close time first (if not continuous market)
1172
1170
  if not is_continuous_market:
@@ -311,7 +311,13 @@ def get_price_data(
311
311
  )
312
312
 
313
313
  if cache_file.exists():
314
- logger.info(f"\nLoading '{datastyle}' pricing data for {asset} / {quote_asset} with '{timespan}' timespan from cache file...")
314
+ logger.debug(
315
+ "\nLoading '%s' pricing data for %s / %s with '%s' timespan from cache file...",
316
+ datastyle,
317
+ asset,
318
+ quote_asset,
319
+ timespan,
320
+ )
315
321
  df_cached = load_cache(cache_file)
316
322
  if df_cached is not None and not df_cached.empty:
317
323
  df_all = df_cached.copy() # Make a copy so we can check the original later for differences
@@ -372,7 +378,7 @@ def get_price_data(
372
378
  )
373
379
  if not missing_dates:
374
380
  if df_all is not None and not df_all.empty:
375
- logger.info("ThetaData cache HIT for %s %s %s (%d rows).", asset, timespan, datastyle, len(df_all))
381
+ logger.debug("ThetaData cache HIT for %s %s %s (%d rows).", asset, timespan, datastyle, len(df_all))
376
382
  # DEBUG-LOG: Cache hit
377
383
  logger.debug(
378
384
  "[THETA][DEBUG][CACHE][HIT] asset=%s timespan=%s datastyle=%s rows=%d start=%s end=%s",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumibot
3
- Version: 4.2.3
3
+ Version: 4.2.5
4
4
  Summary: Backtesting and Trading Library, Made by Lumiwealth
5
5
  Home-page: https://github.com/Lumiwealth/lumibot
6
6
  Author: Robert Grzesik
@@ -104,10 +104,10 @@ lumibot/example_strategies/test_broker_functions.py,sha256=wnVS-M_OtzMgaXVBgshVE
104
104
  lumibot/resources/ThetaTerminal.jar,sha256=K6GeeFcN8-gvyL2x5iq5pzD79KfPJvMK8iiezi3TmNQ,11834389
105
105
  lumibot/resources/conf.yaml,sha256=rjB9-10JP7saZ_edjX5bQDGfuc3amOQTUUUr-UiMpNA,597
106
106
  lumibot/strategies/__init__.py,sha256=jEZ95K5hG0f595EXYKWwL2_UsnWWk5Pug361PK2My2E,79
107
- lumibot/strategies/_strategy.py,sha256=2SWG-VP-MRcZz3ABbP2BHsBWMw78hx4f0hvftpKJRvY,110788
107
+ lumibot/strategies/_strategy.py,sha256=wWx2te98kwMTuusioKR24KsKW8Djs3yhOb2nNmvgbiU,111467
108
108
  lumibot/strategies/session_manager.py,sha256=Nze6UYNSPlCsf-tyHvtFqUeL44WSNHjwsKrIepvsyCY,12956
109
109
  lumibot/strategies/strategy.py,sha256=toPeL5oIVWmCxBNcfXqIuTCF_EeCfIVj425PrSYImCo,170021
110
- lumibot/strategies/strategy_executor.py,sha256=IrHwDOu5s3gG65dz7FL-0kllWy7COci7JFyB4iiPUrg,70801
110
+ lumibot/strategies/strategy_executor.py,sha256=AnmXlKD2eMgKXs3TrD1u8T_Zsn_8GnG5KRcM_Pq-JBQ,70749
111
111
  lumibot/tools/__init__.py,sha256=oRRoK2NBkfnc0kueAfY0HrWVKgzRBO1hlglVMR4jr5M,1501
112
112
  lumibot/tools/alpaca_helpers.py,sha256=nhBS-sv28lZfIQ85szC9El8VHLrCw5a5KbsGOOEjm6w,3147
113
113
  lumibot/tools/backtest_cache.py,sha256=A-Juzu0swZI_FP4U7cd7ruYTgJYgV8BPf_WJDI-NtrM,9556
@@ -132,7 +132,7 @@ lumibot/tools/polygon_helper_async.py,sha256=YHDXa9kmkkn8jh7hToY6GP5etyXS9Tj-uky
132
132
  lumibot/tools/polygon_helper_polars_optimized.py,sha256=NaIZ-5Av-G2McPEKHyJ-x65W72W_Agnz4lRgvXfQp8c,30415
133
133
  lumibot/tools/projectx_helpers.py,sha256=EIemLfbG923T_RBV_i6s6A9xgs7dt0et0oCnhFwdWfA,58299
134
134
  lumibot/tools/schwab_helper.py,sha256=CXnYhgsXOIb5MgmIYOp86aLxsBF9oeVrMGrjwl_GEv0,11768
135
- lumibot/tools/thetadata_helper.py,sha256=-FJm_NXSBJoyYLcdNQXGytMbmr-wx7F1gItnRnBUWf0,80072
135
+ lumibot/tools/thetadata_helper.py,sha256=Guutp_2QAZe22_r6pftCojpTb3DpDp4Ul_szf6N7X1I,80152
136
136
  lumibot/tools/types.py,sha256=x-aQBeC6ZTN2-pUyxyo69Q0j5e0c_swdfe06kfrWSVc,1978
137
137
  lumibot/tools/yahoo_helper.py,sha256=htcKKkuktatIckVKfLc_ms0X75mXColysQhrZW244z8,19497
138
138
  lumibot/tools/yahoo_helper_polars_optimized.py,sha256=g9xBN-ReHSW4Aj9EMU_OncBXVS1HpfL8LTHit9ZxFY4,7417
@@ -142,7 +142,7 @@ lumibot/traders/trader.py,sha256=KMif3WoZtnSxA0BzoK3kvkTITNELrDFIortx1BYBv8s,967
142
142
  lumibot/trading_builtins/__init__.py,sha256=vH2QL5zLjL3slfEV1YW-BvQHtEYLCFkIWTZDfh3y8LE,87
143
143
  lumibot/trading_builtins/custom_stream.py,sha256=8_XiPT0JzyXrgnXCXoovGGUrWEfnG4ohIYMPfB_Nook,5264
144
144
  lumibot/trading_builtins/safe_list.py,sha256=IIjZOHSiZYK25A4WBts0oJaZNOJDsjZL65MOSHhE3Ig,1975
145
- lumibot-4.2.3.dist-info/licenses/LICENSE,sha256=fYhGIyxjyNXACgpNQS3xxpxDOaVOWRVxZMCRbsDv8k0,35130
145
+ lumibot-4.2.5.dist-info/licenses/LICENSE,sha256=fYhGIyxjyNXACgpNQS3xxpxDOaVOWRVxZMCRbsDv8k0,35130
146
146
  tests/__init__.py,sha256=3-VoT-nAuqMfwufd4ceN6fXaHl_zCfDCSXJOTp1ywYQ,393
147
147
  tests/conftest.py,sha256=UBw_2fx7r6TZPKus2b1Qxrzmd4bg8EEBnX1vCHUuSVA,3311
148
148
  tests/fixtures.py,sha256=wOHQsh1SGHnXe_PGi6kDWI30CS_Righi7Ig7vwSEKT4,9082
@@ -162,7 +162,8 @@ tests/test_backtesting_broker.py,sha256=rxZGH5cgiWLmNGdI3k9fti3Fp9IOSohq8xD2E3L2
162
162
  tests/test_backtesting_broker_await_close.py,sha256=WbehY7E4Qet3_Mo7lpfgjmhtI9pnJPIt9mkFI15Dzho,7545
163
163
  tests/test_backtesting_broker_time_advance.py,sha256=FCv0nKG8BQlEjNft7kmQYm9M2CsLIZ0b7mWCllOHQxc,6378
164
164
  tests/test_backtesting_crypto_cash_unit.py,sha256=4EO9jVajdZNV0M7zSyp4gpR_msZFoM4x5tb-6g-mHO8,11399
165
- tests/test_backtesting_data_source_env.py,sha256=w1qren0mKXXFcLHaGo111z5BpqtBj_A-bMZZjhYkVq8,12318
165
+ tests/test_backtesting_data_source_env.py,sha256=ZzpF42-tMc8qqETuy_nf43UsSZMbHtS_ivH93ZqV5P0,12460
166
+ tests/test_backtesting_datetime_normalization.py,sha256=n1ObwObTI_V4uQOQw_WIii7Ph_OgPZYIZGbx5Jv_19U,3245
166
167
  tests/test_backtesting_flow_control.py,sha256=pBqW-fa-HnZq0apUBltalGMM-vNJ_2A5W2SoJzMK8Mg,7208
167
168
  tests/test_backtesting_multileg_unit.py,sha256=h1DPfVuYXXx-uq6KtUjr6_nasZuXPm_5gFat1XxCKIo,6456
168
169
  tests/test_backtesting_quiet_logs_complete.py,sha256=x-GfOiqkiUu8pYKCzB0UUacn13Nx_cPRth7_jmPY2Y8,14155
@@ -280,7 +281,7 @@ tests/backtest/test_thetadata.py,sha256=xWYfC9C4EhbMDb29qyZWHO3sSWaLIPzzvcMbHCt5
280
281
  tests/backtest/test_thetadata_comprehensive.py,sha256=-gN3xLJcJtlB-k4vlaK82DCZDGDmr0LNZZDzn-aN3l4,26120
281
282
  tests/backtest/test_thetadata_vs_polygon.py,sha256=dZqsrOx3u3cz-1onIO6o5BDRjI1ey7U9vIkZupfXoig,22831
282
283
  tests/backtest/test_yahoo.py,sha256=2FguUTUMC9_A20eqxnZ17rN3tT9n6hyvJHaL98QKpqY,3443
283
- lumibot-4.2.3.dist-info/METADATA,sha256=6a_FNEVEmL6ZbCA_h71_vSc1HAThL2gMk1aG1vuZv9Q,12092
284
- lumibot-4.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
285
- lumibot-4.2.3.dist-info/top_level.txt,sha256=otUnUjDFVASauEDiTiAzNgMyqQ1B6jjS3QqqP-WSx38,14
286
- lumibot-4.2.3.dist-info/RECORD,,
284
+ lumibot-4.2.5.dist-info/METADATA,sha256=lK1f_O0TcWv79IUVPehgpO2s_dQE1jrsj69_LCqeKM8,12092
285
+ lumibot-4.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
286
+ lumibot-4.2.5.dist-info/top_level.txt,sha256=otUnUjDFVASauEDiTiAzNgMyqQ1B6jjS3QqqP-WSx38,14
287
+ lumibot-4.2.5.dist-info/RECORD,,
@@ -75,6 +75,9 @@ class TestBacktestingDataSourceEnv:
75
75
  # Configure caplog to capture INFO level logs from lumibot.strategies._strategy
76
76
  import logging
77
77
  caplog.set_level(logging.INFO, logger='lumibot.strategies._strategy')
78
+ polygon_key = os.environ.get("POLYGON_API_KEY")
79
+ if not polygon_key:
80
+ pytest.skip("Polygon API key not configured")
78
81
 
79
82
  with patch.dict(os.environ, {'BACKTESTING_DATA_SOURCE': 'polygon'}):
80
83
  # Re-import credentials to pick up env change
@@ -0,0 +1,94 @@
1
+ import datetime
2
+ from unittest.mock import patch
3
+
4
+ import pytest
5
+
6
+ from lumibot.constants import LUMIBOT_DEFAULT_PYTZ
7
+ from lumibot.strategies import Strategy
8
+ from lumibot.strategies._strategy import _Strategy
9
+
10
+
11
+ class MinimalStrategy(Strategy):
12
+ """No-op strategy used for backtest scaffolding."""
13
+
14
+ def initialize(self):
15
+ self.sleeptime = "1D"
16
+
17
+ def on_trading_iteration(self):
18
+ pass
19
+
20
+
21
+ class DummyDataSource:
22
+ """Lightweight datasource stub capturing the start/end datetimes."""
23
+
24
+ SOURCE = "dummy"
25
+
26
+ def __init__(self, datetime_start=None, datetime_end=None, **kwargs):
27
+ self.datetime_start = datetime_start
28
+ self.datetime_end = datetime_end
29
+ self._data_store = {}
30
+
31
+
32
+ class DummyTrader:
33
+ """Trader stub that records strategies and returns canned results."""
34
+
35
+ def __init__(self, *args, **kwargs):
36
+ self._strategies = []
37
+
38
+ def add_strategy(self, strategy):
39
+ self._strategies.append(strategy)
40
+
41
+ def run_all(self, **_kwargs):
42
+ return {strategy.name: {"dummy": True} for strategy in self._strategies}
43
+
44
+
45
+ class _EarlyExit(Exception):
46
+ """Signal to stop run_backtest after the datasource is constructed."""
47
+
48
+
49
+ def test_verify_backtest_inputs_accepts_mixed_timezones():
50
+ """Regression: verify_backtest_inputs must not crash on naive vs aware inputs."""
51
+ naive_start = datetime.datetime(2025, 1, 1)
52
+ aware_end = datetime.datetime(2025, 9, 30, tzinfo=datetime.timezone.utc)
53
+
54
+ # Should not raise
55
+ _Strategy.verify_backtest_inputs(naive_start, aware_end)
56
+
57
+
58
+ def test_run_backtest_normalizes_mixed_timezones():
59
+ """Strategy.run_backtest should normalize naive/aware datetimes before validation."""
60
+ naive_start = datetime.datetime(2025, 1, 1)
61
+ aware_end = datetime.datetime(2025, 9, 30, tzinfo=datetime.timezone.utc)
62
+
63
+ captured = {}
64
+
65
+ class CapturingDataSource(DummyDataSource):
66
+ def __init__(self, datetime_start=None, datetime_end=None, **kwargs):
67
+ super().__init__(datetime_start=datetime_start, datetime_end=datetime_end, **kwargs)
68
+ captured["start"] = self.datetime_start
69
+ captured["end"] = self.datetime_end
70
+
71
+ def broker_factory(data_source, *args, **kwargs):
72
+ captured["data_source"] = data_source
73
+ raise _EarlyExit
74
+
75
+ with patch("lumibot.strategies._strategy.BacktestingBroker", side_effect=broker_factory), \
76
+ patch("lumibot.strategies._strategy.Trader", DummyTrader):
77
+ with pytest.raises(_EarlyExit):
78
+ MinimalStrategy.run_backtest(
79
+ CapturingDataSource,
80
+ backtesting_start=naive_start,
81
+ backtesting_end=aware_end,
82
+ show_plot=False,
83
+ show_tearsheet=False,
84
+ show_indicators=False,
85
+ show_progress_bar=False,
86
+ save_logfile=False,
87
+ save_stats_file=False,
88
+ )
89
+
90
+ assert "start" in captured and captured["start"].tzinfo is not None
91
+ 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