lumibot 4.2.2__py3-none-any.whl → 4.2.4__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.

@@ -288,7 +288,8 @@ class ThetaDataBacktestingPandas(PandasData):
288
288
  )
289
289
 
290
290
  expected_last_dt = self.to_default_timezone(current_dt).replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=1)
291
- target_index = pd.date_range(end=expected_last_dt, periods=requested_length, freq="D", tz=self.tzinfo)
291
+ expected_last_dt_utc = expected_last_dt.astimezone(pytz.UTC)
292
+ target_index = pd.date_range(end=expected_last_dt_utc, periods=requested_length, freq="D", tz=pytz.UTC).tz_convert(self.tzinfo)
292
293
 
293
294
  # DEBUG-LOG: Target index details
294
295
  logger.debug(
@@ -1388,12 +1388,6 @@ class _Strategy:
1388
1388
  if use_other_option_source and not isinstance(optionsource_class, type):
1389
1389
  raise ValueError(f"`optionsource_class` must be a class. You passed in {optionsource_class}")
1390
1390
 
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
1391
  try:
1398
1392
  backtesting_start = to_datetime_aware(backtesting_start)
1399
1393
  backtesting_end = to_datetime_aware(backtesting_end)
@@ -1405,6 +1399,12 @@ class _Strategy:
1405
1399
  )
1406
1400
  return None
1407
1401
 
1402
+ self.verify_backtest_inputs(backtesting_start, backtesting_end)
1403
+
1404
+ if not self.IS_BACKTESTABLE:
1405
+ get_logger(__name__).warning(f"Strategy {name + ' ' if name is not None else ''}cannot be " f"backtested at the moment")
1406
+ return None
1407
+
1408
1408
  if BACKTESTING_QUIET_LOGS is not None:
1409
1409
  quiet_logs = BACKTESTING_QUIET_LOGS
1410
1410
 
@@ -1628,18 +1628,21 @@ class _Strategy:
1628
1628
  if not isinstance(backtesting_end, datetime.datetime):
1629
1629
  raise ValueError(f"`backtesting_end` must be a datetime object. You passed in {backtesting_end}")
1630
1630
 
1631
+ start_dt = to_datetime_aware(backtesting_start)
1632
+ end_dt = to_datetime_aware(backtesting_end)
1633
+
1631
1634
  # Check that backtesting end is after backtesting start
1632
- if backtesting_end <= backtesting_start:
1635
+ if end_dt <= start_dt:
1633
1636
  raise ValueError(
1634
1637
  f"`backtesting_end` must be after `backtesting_start`. You passed in "
1635
- f"{backtesting_end} and {backtesting_start}"
1638
+ f"{end_dt} and {start_dt}"
1636
1639
  )
1637
1640
 
1638
1641
  # 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:
1642
+ now = datetime.datetime.now(end_dt.tzinfo) if end_dt.tzinfo else datetime.datetime.now()
1643
+ if end_dt > now:
1641
1644
  raise ValueError(
1642
- f"`backtesting_end` cannot be in the future. You passed in {backtesting_end}, now is {now}"
1645
+ f"`backtesting_end` cannot be in the future. You passed in {end_dt}, now is {now}"
1643
1646
  )
1644
1647
 
1645
1648
  def send_update_to_cloud(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumibot
3
- Version: 4.2.2
3
+ Version: 4.2.4
4
4
  Summary: Backtesting and Trading Library, Made by Lumiwealth
5
5
  Home-page: https://github.com/Lumiwealth/lumibot
6
6
  Author: Robert Grzesik
@@ -14,7 +14,7 @@ lumibot/backtesting/interactive_brokers_rest_backtesting.py,sha256=5HJ_sPX0uOUg-
14
14
  lumibot/backtesting/pandas_backtesting.py,sha256=m-NvT4o-wFQjaZft6TXULzeZBrskO_7Z-jfy9AIkyAY,388
15
15
  lumibot/backtesting/polygon_backtesting.py,sha256=u9kif_2_7k0P4-KDvbHhaMfSoBVejUUX7fh9H3PCVE0,12350
16
16
  lumibot/backtesting/thetadata_backtesting.py,sha256=Xcz5f-4zTkKgWWcktNzItH2vrr8CysIMQWKKqLwugbA,345
17
- lumibot/backtesting/thetadata_backtesting_pandas.py,sha256=14XMsbQCa3uE_iS2nvTlGUXkn9kvI0cDSE8mqdKnDEg,51750
17
+ lumibot/backtesting/thetadata_backtesting_pandas.py,sha256=3ltwnxqAw4xXG2VOk14G_vpVHAVdnGl10SsveoMoJF0,51844
18
18
  lumibot/backtesting/yahoo_backtesting.py,sha256=LT2524mGlrUSq1YSRnUqGW4-Xcq4USgRv2EhnV_zfs4,502
19
19
  lumibot/brokers/__init__.py,sha256=MGWKHeH3mqseYRL7u-KX1Jp2x9EaFO4Ol8sfNSxzu1M,404
20
20
  lumibot/brokers/alpaca.py,sha256=VQ17idfqiEFb2JCqqdMGmbvF789L7_PpsCbudiFRzmg,61595
@@ -104,7 +104,7 @@ 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=jfKU4NpYiGJexTEeHz-pi-8NtSdcX1BXBTcjr7wsUuY,110825
108
108
  lumibot/strategies/session_manager.py,sha256=Nze6UYNSPlCsf-tyHvtFqUeL44WSNHjwsKrIepvsyCY,12956
109
109
  lumibot/strategies/strategy.py,sha256=toPeL5oIVWmCxBNcfXqIuTCF_EeCfIVj425PrSYImCo,170021
110
110
  lumibot/strategies/strategy_executor.py,sha256=IrHwDOu5s3gG65dz7FL-0kllWy7COci7JFyB4iiPUrg,70801
@@ -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.2.dist-info/licenses/LICENSE,sha256=fYhGIyxjyNXACgpNQS3xxpxDOaVOWRVxZMCRbsDv8k0,35130
145
+ lumibot-4.2.4.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=8NjxiZbSIOzE4I2qDwEqkN3M7jnIdTfjPPE2Qp_0ySE,2984
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
@@ -234,7 +235,7 @@ tests/test_quiet_logs_requirements.py,sha256=YoUooSVLrFL8TlWPfxEiqxvSj4d8z6-qg58
234
235
  tests/test_session_manager.py,sha256=1qygN3aQ2Xe2uh4BMPm0E3V8KXLFNGq5qdL8KkZjef4,11632
235
236
  tests/test_strategy_methods.py,sha256=j9Mhr6nnG1fkiVQXnx7gLjzGbeQmwt0UbJr_4plD36o,12539
236
237
  tests/test_thetadata_backwards_compat.py,sha256=RzNLhNZNJZ2hPkEDyG-T_4mRRXh5XqavK6r-OjfRASQ,3306
237
- tests/test_thetadata_helper.py,sha256=pcEPu-9kQYp4cn5xmhU1-28DfT-GRu_nUuUMb1xi7nA,58088
238
+ tests/test_thetadata_helper.py,sha256=l77ksu70knZBudfrMAOwT9zJ91AxX5UgazqvYSrotqM,59346
238
239
  tests/test_thetadata_pandas_verification.py,sha256=MWUecqBY6FGFslWLRo_C5blGbom_unmXCZikAfZXLks,6553
239
240
  tests/test_tradier.py,sha256=iCEM2FTxJSzJ2oLNaRqSx05XaX_DCiMzLx1aEYPANko,33280
240
241
  tests/test_tradier_data.py,sha256=1jTxDzQtzaC42CQJVXMRMElBwExy1mVci3NFfKjjVH0,13363
@@ -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.2.dist-info/METADATA,sha256=yjZcnAmbXlQQj4ZEDPZNoWndZHusYMdY8nluNGVQP-0,12092
284
- lumibot-4.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
285
- lumibot-4.2.2.dist-info/top_level.txt,sha256=otUnUjDFVASauEDiTiAzNgMyqQ1B6jjS3QqqP-WSx38,14
286
- lumibot-4.2.2.dist-info/RECORD,,
284
+ lumibot-4.2.4.dist-info/METADATA,sha256=tTX4FY9DKXc8JjeCMImHK3NgzlR1h_U9dSvQqyOnQ28,12092
285
+ lumibot-4.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
286
+ lumibot-4.2.4.dist-info/top_level.txt,sha256=otUnUjDFVASauEDiTiAzNgMyqQ1B6jjS3QqqP-WSx38,14
287
+ lumibot-4.2.4.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,90 @@
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
@@ -1408,6 +1408,46 @@ class TestThetaDataChainsCaching:
1408
1408
  assert time2 < time1 * 0.1, f"Cache not working: time1={time1:.2f}s, time2={time2:.2f}s (should be 10x faster)"
1409
1409
  print(f"✓ Cache speedup: {time1/time2:.1f}x faster ({time1:.2f}s -> {time2:.4f}s)")
1410
1410
 
1411
+
1412
+ def test_finalize_day_frame_handles_dst_fallback():
1413
+ tz = pytz.timezone("America/New_York")
1414
+ utc = pytz.UTC
1415
+ frame_index = pd.date_range(
1416
+ end=tz.localize(datetime.datetime(2024, 10, 31, 16, 0)),
1417
+ periods=5,
1418
+ freq="D",
1419
+ )
1420
+ frame = pd.DataFrame(
1421
+ {
1422
+ "open": [100 + i for i in range(len(frame_index))],
1423
+ "high": [101 + i for i in range(len(frame_index))],
1424
+ "low": [99 + i for i in range(len(frame_index))],
1425
+ "close": [100.5 + i for i in range(len(frame_index))],
1426
+ "volume": [1000 + i for i in range(len(frame_index))],
1427
+ },
1428
+ index=frame_index,
1429
+ )
1430
+
1431
+ data_source = ThetaDataBacktestingPandas(
1432
+ datetime_start=utc.localize(datetime.datetime(2024, 10, 1)),
1433
+ datetime_end=utc.localize(datetime.datetime(2024, 11, 5)),
1434
+ username="user",
1435
+ password="pass",
1436
+ use_quote_data=False,
1437
+ )
1438
+
1439
+ current_dt = utc.localize(datetime.datetime(2024, 11, 4, 13, 30))
1440
+ result = data_source._finalize_day_frame(
1441
+ frame,
1442
+ current_dt,
1443
+ requested_length=len(frame_index),
1444
+ timeshift=None,
1445
+ asset=Asset("TSLA"),
1446
+ )
1447
+
1448
+ assert result is not None
1449
+ assert len(result) == len(frame_index)
1450
+
1411
1451
  def test_chains_strike_format(self):
1412
1452
  """Test strikes are floats (not integers) and properly converted."""
1413
1453
  username = os.environ.get("THETADATA_USERNAME")