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

Files changed (164) hide show
  1. lumibot/backtesting/__init__.py +19 -5
  2. lumibot/backtesting/backtesting_broker.py +98 -18
  3. lumibot/backtesting/databento_backtesting.py +5 -686
  4. lumibot/backtesting/databento_backtesting_pandas.py +738 -0
  5. lumibot/backtesting/databento_backtesting_polars.py +860 -546
  6. lumibot/backtesting/fix_debug.py +37 -0
  7. lumibot/backtesting/thetadata_backtesting.py +9 -355
  8. lumibot/backtesting/thetadata_backtesting_pandas.py +1178 -0
  9. lumibot/brokers/alpaca.py +8 -1
  10. lumibot/brokers/schwab.py +12 -2
  11. lumibot/credentials.py +13 -0
  12. lumibot/data_sources/__init__.py +5 -8
  13. lumibot/data_sources/data_source.py +6 -2
  14. lumibot/data_sources/data_source_backtesting.py +30 -0
  15. lumibot/data_sources/databento_data.py +5 -390
  16. lumibot/data_sources/databento_data_pandas.py +440 -0
  17. lumibot/data_sources/databento_data_polars.py +15 -9
  18. lumibot/data_sources/pandas_data.py +30 -17
  19. lumibot/data_sources/polars_data.py +986 -0
  20. lumibot/data_sources/polars_mixin.py +472 -96
  21. lumibot/data_sources/polygon_data_polars.py +5 -0
  22. lumibot/data_sources/yahoo_data.py +9 -2
  23. lumibot/data_sources/yahoo_data_polars.py +5 -0
  24. lumibot/entities/__init__.py +15 -0
  25. lumibot/entities/asset.py +5 -28
  26. lumibot/entities/bars.py +89 -20
  27. lumibot/entities/data.py +29 -6
  28. lumibot/entities/data_polars.py +668 -0
  29. lumibot/entities/position.py +38 -4
  30. lumibot/strategies/_strategy.py +31 -9
  31. lumibot/strategies/strategy.py +61 -49
  32. lumibot/tools/backtest_cache.py +284 -0
  33. lumibot/tools/databento_helper.py +65 -42
  34. lumibot/tools/databento_helper_polars.py +748 -778
  35. lumibot/tools/futures_roll.py +251 -0
  36. lumibot/tools/indicators.py +135 -104
  37. lumibot/tools/polars_utils.py +142 -0
  38. lumibot/tools/thetadata_helper.py +1068 -134
  39. {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/METADATA +9 -1
  40. {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/RECORD +72 -148
  41. tests/backtest/test_databento.py +37 -6
  42. tests/backtest/test_databento_comprehensive_trading.py +70 -87
  43. tests/backtest/test_databento_parity.py +31 -7
  44. tests/backtest/test_debug_avg_fill_price.py +1 -1
  45. tests/backtest/test_example_strategies.py +11 -1
  46. tests/backtest/test_futures_edge_cases.py +96 -63
  47. tests/backtest/test_futures_single_trade.py +2 -2
  48. tests/backtest/test_futures_ultra_simple.py +2 -2
  49. tests/backtest/test_polars_lru_eviction.py +470 -0
  50. tests/backtest/test_yahoo.py +42 -0
  51. tests/test_asset.py +4 -4
  52. tests/test_backtest_cache_manager.py +149 -0
  53. tests/test_backtesting_data_source_env.py +50 -10
  54. tests/test_continuous_futures_resolution.py +60 -48
  55. tests/test_data_polars_parity.py +160 -0
  56. tests/test_databento_asset_validation.py +23 -5
  57. tests/test_databento_backtesting.py +1 -1
  58. tests/test_databento_backtesting_polars.py +312 -192
  59. tests/test_databento_data.py +220 -463
  60. tests/test_databento_helper.py +6 -1
  61. tests/test_databento_live.py +10 -10
  62. tests/test_futures_roll.py +38 -0
  63. tests/test_indicator_subplots.py +101 -0
  64. tests/test_market_infinite_loop_bug.py +77 -3
  65. tests/test_polars_resample.py +67 -0
  66. tests/test_polygon_helper.py +46 -0
  67. tests/test_thetadata_backwards_compat.py +97 -0
  68. tests/test_thetadata_helper.py +222 -23
  69. tests/test_thetadata_pandas_verification.py +186 -0
  70. lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
  71. lumibot/__pycache__/constants.cpython-312.pyc +0 -0
  72. lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
  73. lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
  74. lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
  75. lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
  76. lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
  77. lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
  78. lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
  79. lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
  80. lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
  81. lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
  82. lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
  83. lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
  84. lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
  85. lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
  86. lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
  87. lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
  88. lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
  89. lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
  90. lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
  91. lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
  92. lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
  93. lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
  94. lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
  95. lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
  96. lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
  97. lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
  98. lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
  99. lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
  100. lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
  101. lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
  102. lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
  103. lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
  104. lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
  105. lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
  106. lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
  107. lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
  108. lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
  109. lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
  110. lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
  111. lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
  112. lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
  113. lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
  114. lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
  115. lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
  116. lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
  117. lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
  118. lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
  119. lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
  120. lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
  121. lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
  122. lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
  123. lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
  124. lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
  125. lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
  126. lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
  127. lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
  128. lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
  129. lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  130. lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
  131. lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  132. lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
  133. lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
  134. lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
  135. lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  136. lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
  137. lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
  138. lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
  139. lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
  140. lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
  141. lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
  142. lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
  143. lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
  144. lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
  145. lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
  146. lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
  147. lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
  148. lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
  149. lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
  150. lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
  151. lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
  152. lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
  153. lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
  154. lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
  155. lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
  156. lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
  157. lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
  158. lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
  159. lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
  160. lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
  161. lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
  162. {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/WHEEL +0 -0
  163. {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/licenses/LICENSE +0 -0
  164. {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/top_level.txt +0 -0
@@ -98,11 +98,14 @@ class TestBacktestingDataSourceEnv:
98
98
  )
99
99
 
100
100
  # Verify the log message shows polygon was selected
101
- assert any("Auto-selected backtesting data source from BACKTESTING_DATA_SOURCE env var: polygon" in record.message
101
+ assert any("Using BACKTESTING_DATA_SOURCE setting for backtest data: polygon" in record.message
102
102
  for record in caplog.records)
103
103
 
104
104
  def test_auto_select_thetadata_case_insensitive(self, clean_environment, restore_theta_credentials, caplog):
105
105
  """Test that BACKTESTING_DATA_SOURCE=THETADATA (uppercase) selects ThetaDataBacktesting."""
106
+ import logging
107
+ caplog.set_level(logging.INFO, logger='lumibot.strategies._strategy')
108
+
106
109
  with patch.dict(os.environ, {'BACKTESTING_DATA_SOURCE': 'THETADATA'}):
107
110
  # Re-import credentials to pick up env change
108
111
  from importlib import reload
@@ -130,7 +133,7 @@ class TestBacktestingDataSourceEnv:
130
133
  pass
131
134
 
132
135
  # Verify the log message shows thetadata was selected OR check for ThetaData error
133
- thetadata_selected = any("Auto-selected backtesting data source from BACKTESTING_DATA_SOURCE env var: THETADATA" in record.message
136
+ thetadata_selected = any("Using BACKTESTING_DATA_SOURCE setting for backtest data: THETADATA" in record.message
134
137
  for record in caplog.records)
135
138
  thetadata_attempted = any("Cannot connect to Theta Data" in record.message or "ThetaData" in record.message
136
139
  for record in caplog.records)
@@ -183,8 +186,11 @@ class TestBacktestingDataSourceEnv:
183
186
  show_indicators=False,
184
187
  )
185
188
 
186
- def test_explicit_datasource_overrides_env(self, clean_environment, restore_theta_credentials, caplog):
187
- """Test that explicit datasource_class overrides BACKTESTING_DATA_SOURCE env var."""
189
+ def test_env_override_wins_over_explicit_datasource(self, clean_environment, restore_theta_credentials, caplog):
190
+ """Test that BACKTESTING_DATA_SOURCE env var takes precedence over explicit datasource_class."""
191
+ import logging
192
+ caplog.set_level(logging.INFO, logger='lumibot.strategies._strategy')
193
+
188
194
  with patch.dict(os.environ, {'BACKTESTING_DATA_SOURCE': 'polygon'}):
189
195
  # Re-import credentials to pick up env change
190
196
  from importlib import reload
@@ -205,15 +211,45 @@ class TestBacktestingDataSourceEnv:
205
211
  show_progress_bar=False,
206
212
  )
207
213
 
208
- # Verify the auto-select message was NOT logged (explicit datasource was used)
209
- assert not any("Auto-selected backtesting data source" in record.message
210
- for record in caplog.records)
214
+ # Verify the env override message was logged (env var wins)
215
+ assert any("Using BACKTESTING_DATA_SOURCE setting for backtest data: polygon" in record.message
216
+ for record in caplog.records)
217
+
218
+ def test_explicit_datasource_used_when_env_none(self, clean_environment, restore_theta_credentials, caplog):
219
+ """Test that setting BACKTESTING_DATA_SOURCE to 'none' defers to the explicit datasource_class."""
220
+ import logging
221
+ caplog.set_level(logging.INFO, logger='lumibot.strategies._strategy')
222
+
223
+ with patch.dict(os.environ, {'BACKTESTING_DATA_SOURCE': 'none'}):
224
+ from importlib import reload
225
+ import lumibot.credentials
226
+ reload(lumibot.credentials)
227
+
228
+ backtesting_start = datetime(2023, 1, 1)
229
+ backtesting_end = datetime(2023, 1, 10) # Shorter backtest for speed
230
+
231
+ SimpleTestStrategy.run_backtest(
232
+ YahooDataBacktesting,
233
+ backtesting_start=backtesting_start,
234
+ backtesting_end=backtesting_end,
235
+ show_plot=False,
236
+ show_tearsheet=False,
237
+ show_indicators=False,
238
+ show_progress_bar=False,
239
+ )
240
+
241
+ # Confirm no override occurred
242
+ assert not any("Using BACKTESTING_DATA_SOURCE setting for backtest data" in record.message
243
+ for record in caplog.records)
211
244
 
212
245
  def test_default_thetadata_when_no_env_set(self, clean_environment, restore_theta_credentials, caplog):
213
246
  """Test that ThetaData is the default when BACKTESTING_DATA_SOURCE is not set."""
214
247
  # Remove BACKTESTING_DATA_SOURCE from env
215
248
  env_without_datasource = {k: v for k, v in os.environ.items() if k != 'BACKTESTING_DATA_SOURCE'}
216
249
 
250
+ import logging
251
+ caplog.set_level(logging.INFO, logger='lumibot.strategies._strategy')
252
+
217
253
  with patch.dict(os.environ, env_without_datasource, clear=True):
218
254
  # Re-import credentials to pick up env change
219
255
  from importlib import reload
@@ -240,9 +276,13 @@ class TestBacktestingDataSourceEnv:
240
276
  # Expected to fail with test credentials - that's okay
241
277
  pass
242
278
 
243
- # Verify ThetaData was attempted (no auto-select message since it's the default)
244
- assert any("Cannot connect to Theta Data" in record.message or "ThetaData" in record.message
245
- for record in caplog.records), "ThetaData was not used as default"
279
+ # Verify ThetaData was attempted (look for override message or Theta-specific logs)
280
+ assert any(
281
+ "Using BACKTESTING_DATA_SOURCE setting for backtest data: ThetaData" in record.message
282
+ or "Cannot connect to Theta Data" in record.message
283
+ or "ThetaData" in record.message
284
+ for record in caplog.records
285
+ ), "ThetaData was not used as default"
246
286
 
247
287
 
248
288
  if __name__ == "__main__":
@@ -9,6 +9,7 @@ from lumibot.tools.databento_helper import (
9
9
  _format_futures_symbol_for_databento,
10
10
  )
11
11
  from lumibot.entities import Asset
12
+ from lumibot.entities.asset import FUTURES_MONTH_CODES
12
13
 
13
14
 
14
15
  class TestContinuousFuturesResolution(unittest.TestCase):
@@ -107,14 +108,26 @@ class TestContinuousFuturesResolution(unittest.TestCase):
107
108
  """Test contract generation around year boundaries with expiration-aware logic."""
108
109
  asset = Asset("ES", asset_type=Asset.AssetType.CONT_FUTURE)
109
110
 
111
+ from lumibot.tools import futures_roll
112
+
110
113
  contract = asset.resolve_continuous_futures_contract(reference_date=datetime(2025, 12, 31))
111
114
  self.assertEqual(contract, 'ESH26')
112
115
 
113
116
  contract = asset.resolve_continuous_futures_contract(reference_date=datetime(2026, 1, 1))
114
117
  self.assertEqual(contract, 'ESH26')
115
118
 
116
- contract = asset.resolve_continuous_futures_contract(reference_date=datetime(2025, 12, 14))
117
- self.assertEqual(contract, 'ESZ25')
119
+ pre_trigger = datetime(2025, 12, 8)
120
+ post_trigger = datetime(2025, 12, 9)
121
+
122
+ year_pre, month_pre = futures_roll.determine_contract_year_month("ES", pre_trigger)
123
+ expected_pre = asset._build_contract_variants(f"ES{FUTURES_MONTH_CODES[month_pre]}", year_pre)[2]
124
+ contract = asset.resolve_continuous_futures_contract(reference_date=pre_trigger)
125
+ self.assertEqual(contract, expected_pre)
126
+
127
+ year_post, month_post = futures_roll.determine_contract_year_month("ES", post_trigger)
128
+ expected_post = asset._build_contract_variants(f"ES{FUTURES_MONTH_CODES[month_post]}", year_post)[2]
129
+ contract = asset.resolve_continuous_futures_contract(reference_date=post_trigger)
130
+ self.assertEqual(contract, expected_post)
118
131
 
119
132
  def test_different_symbol_formats(self):
120
133
  """Test continuous futures resolution with different symbol formats."""
@@ -229,34 +242,32 @@ class TestContinuousFuturesResolution(unittest.TestCase):
229
242
  """
230
243
  asset = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
231
244
 
232
- # Test that contract resolution properly accounts for 3rd Friday expiration
233
- # Rollover happens on 15th of expiry month to avoid expired contracts
234
- quarterly_tests = [
235
- # Q1: Jan-Feb should resolve to March (H), Mar 15+ should roll to June (M)
236
- (datetime(2024, 1, 15), 'H24'),
237
- (datetime(2024, 2, 15), 'H24'),
238
- (datetime(2024, 3, 14), 'H24'), # Before rollover
239
- (datetime(2024, 3, 15), 'M24'), # After rollover (Mar expires ~21st)
240
- # Q2: Apr-May should resolve to June (M), Jun 15+ should roll to Sep (U)
241
- (datetime(2024, 4, 15), 'M24'),
242
- (datetime(2024, 5, 15), 'M24'),
243
- (datetime(2024, 6, 14), 'M24'), # Before rollover
244
- (datetime(2024, 6, 15), 'U24'), # After rollover (Jun expires ~20th)
245
- # Q3: Jul-Aug should resolve to September (U), Sep 15+ should roll to Dec (Z)
246
- (datetime(2024, 7, 15), 'U24'),
247
- (datetime(2024, 8, 15), 'U24'),
248
- (datetime(2024, 9, 14), 'U24'), # Before rollover
249
- (datetime(2024, 9, 15), 'Z24'), # After rollover (Sep expires ~19th)
250
- # Q4: Oct-Nov should resolve to December (Z), Dec 15+ should roll to Mar next year (H)
251
- (datetime(2024, 10, 15), 'Z24'),
252
- (datetime(2024, 11, 15), 'Z24'),
253
- (datetime(2024, 12, 14), 'Z24'), # Before rollover
254
- (datetime(2024, 12, 15), 'H25'), # After rollover (Dec expires ~19th)
245
+ from lumibot.tools import futures_roll
246
+
247
+ quarterly_dates = [
248
+ datetime(2024, 1, 15),
249
+ datetime(2024, 2, 15),
250
+ datetime(2024, 3, 4),
251
+ datetime(2024, 3, 5),
252
+ datetime(2024, 4, 15),
253
+ datetime(2024, 5, 15),
254
+ datetime(2024, 6, 10),
255
+ datetime(2024, 6, 11),
256
+ datetime(2024, 7, 15),
257
+ datetime(2024, 8, 15),
258
+ datetime(2024, 9, 9),
259
+ datetime(2024, 9, 10),
260
+ datetime(2024, 10, 15),
261
+ datetime(2024, 11, 15),
262
+ datetime(2024, 12, 9),
263
+ datetime(2024, 12, 10),
255
264
  ]
256
-
257
- for test_date, expected_suffix in quarterly_tests:
265
+
266
+ for test_date in quarterly_dates:
267
+ year, month = futures_roll.determine_contract_year_month("MES", test_date)
268
+ month_code = FUTURES_MONTH_CODES[month]
269
+ expected_contract = asset._build_contract_variants(f"MES{month_code}", year)[2]
258
270
  contract = asset.resolve_continuous_futures_contract(reference_date=test_date)
259
- expected_contract = f"MES{expected_suffix}"
260
271
  self.assertEqual(
261
272
  contract,
262
273
  expected_contract,
@@ -270,30 +281,31 @@ class TestContinuousFuturesResolution(unittest.TestCase):
270
281
  """
271
282
  asset = Asset("ES", asset_type=Asset.AssetType.CONT_FUTURE)
272
283
 
273
- # Test around March 2025 expiration (3rd Friday is March 21st)
274
- test_cases = [
275
- (datetime(2025, 3, 14), 'ESH25'), # Before rollover - still March
276
- (datetime(2025, 3, 15), 'ESM25'), # Rollover day - move to June
277
- (datetime(2025, 3, 21), 'ESM25'), # Actual expiry day - already rolled
278
- (datetime(2025, 3, 22), 'ESM25'), # After expiry - definitely rolled
279
-
280
- # Test around June 2025 expiration (3rd Friday is June 20th)
281
- (datetime(2025, 6, 14), 'ESM25'), # Before rollover - still June
282
- (datetime(2025, 6, 15), 'ESU25'), # Rollover day - move to September
283
- (datetime(2025, 6, 20), 'ESU25'), # Actual expiry day - already rolled
284
-
285
- # Test around December 2025 expiration (3rd Friday is December 19th)
286
- (datetime(2025, 12, 14), 'ESZ25'), # Before rollover - still December
287
- (datetime(2025, 12, 15), 'ESH26'), # Rollover day - move to March next year
288
- (datetime(2025, 12, 19), 'ESH26'), # Actual expiry day - already rolled
284
+ from lumibot.tools import futures_roll
285
+
286
+ check_dates = [
287
+ datetime(2025, 3, 10),
288
+ datetime(2025, 3, 11),
289
+ datetime(2025, 3, 21),
290
+ datetime(2025, 3, 22),
291
+ datetime(2025, 6, 9),
292
+ datetime(2025, 6, 10),
293
+ datetime(2025, 6, 20),
294
+ datetime(2025, 12, 8),
295
+ datetime(2025, 12, 9),
296
+ datetime(2025, 12, 19),
289
297
  ]
290
-
291
- for test_date, expected_contract in test_cases:
298
+
299
+ for test_date in check_dates:
300
+ year, month = futures_roll.determine_contract_year_month("ES", test_date)
301
+ month_code = FUTURES_MONTH_CODES[month]
302
+ expected = asset._build_contract_variants(f"ES{month_code}", year)[2]
303
+
292
304
  contract = asset.resolve_continuous_futures_contract(reference_date=test_date)
293
305
  self.assertEqual(
294
306
  contract,
295
- expected_contract,
296
- f"Date {test_date.strftime('%Y-%m-%d')} should resolve to {expected_contract}, got {contract}",
307
+ expected,
308
+ f"Date {test_date.strftime('%Y-%m-%d')} should resolve to {expected}, got {contract}",
297
309
  )
298
310
 
299
311
 
@@ -0,0 +1,160 @@
1
+ """
2
+ Regression test for Data vs DataPolars parity bug.
3
+
4
+ This test isolates the issue where DataPolars returns 234 rows when asked for 2 rows
5
+ with timeshift=-2 parameter.
6
+ """
7
+
8
+ from datetime import datetime, timedelta, timezone
9
+ import pandas as pd
10
+ import polars as pl
11
+ import pytest
12
+
13
+ from lumibot.entities import Data, DataPolars, Asset
14
+
15
+
16
+ def _create_mock_ohlc_data(start: datetime, periods: int = 300) -> pd.DataFrame:
17
+ """Create mock OHLC data for testing.
18
+
19
+ Args:
20
+ start: Starting datetime (must be timezone-aware)
21
+ periods: Number of minute bars to generate
22
+
23
+ Returns:
24
+ DataFrame with OHLC data indexed by timestamp
25
+ """
26
+ index = pd.date_range(start=start, periods=periods, freq="1min", tz=timezone.utc)
27
+ data = {
28
+ "open": [200 + i * 0.1 for i in range(periods)],
29
+ "high": [201 + i * 0.1 for i in range(periods)],
30
+ "low": [199 + i * 0.1 for i in range(periods)],
31
+ "close": [200.5 + i * 0.1 for i in range(periods)],
32
+ "volume": [10000 + i * 100 for i in range(periods)],
33
+ }
34
+ return pd.DataFrame(data, index=index)
35
+
36
+
37
+ def test_data_polars_row_count_parity():
38
+ """
39
+ Test that Data and DataPolars return the same number of rows for identical requests.
40
+
41
+ This reproduces the bug where:
42
+ - Data.get_bars(length=2, timeshift=-2) returns 2 rows
43
+ - DataPolars.get_bars(length=2, timeshift=-2) returns 234 rows
44
+ """
45
+ # Create mock data starting at market open
46
+ start = datetime(2024, 7, 18, 9, 30, tzinfo=timezone.utc)
47
+ mock_df = _create_mock_ohlc_data(start, periods=300)
48
+
49
+ # Create asset
50
+ asset = Asset("HIMS", asset_type=Asset.AssetType.STOCK)
51
+
52
+ # Create Data instance (pandas mode)
53
+ data_pandas = Data(
54
+ asset=asset,
55
+ df=mock_df.copy(),
56
+ timestep="minute",
57
+ quote=asset,
58
+ )
59
+
60
+ # Create DataPolars instance (polars mode)
61
+ # Convert to polars format with datetime column
62
+ mock_df_reset = mock_df.reset_index()
63
+ mock_df_reset.columns = ["datetime", "open", "high", "low", "close", "volume"]
64
+ mock_polars = pl.from_pandas(mock_df_reset)
65
+
66
+ data_polars = DataPolars(
67
+ asset=asset,
68
+ df=mock_polars,
69
+ timestep="minute",
70
+ quote=asset,
71
+ )
72
+
73
+ # Test at a specific datetime (10:00 AM = 30 minutes after market open)
74
+ test_dt = datetime(2024, 7, 18, 10, 0, tzinfo=timezone.utc)
75
+
76
+ # Request 2 bars with timeshift=-2
77
+ # This should return bars at 09:58 and 09:59
78
+ # get_bars() returns DataFrames directly
79
+ df_pandas = data_pandas.get_bars(
80
+ dt=test_dt,
81
+ length=2,
82
+ timestep="minute",
83
+ timeshift=-2
84
+ )
85
+
86
+ df_polars = data_polars.get_bars(
87
+ dt=test_dt,
88
+ length=2,
89
+ timestep="minute",
90
+ timeshift=-2
91
+ )
92
+
93
+ # CRITICAL ASSERTIONS
94
+ assert len(df_pandas) == 2, f"Pandas should return 2 rows, got {len(df_pandas)}"
95
+ assert len(df_polars) == 2, f"Polars should return 2 rows, got {len(df_polars)}"
96
+ assert len(df_pandas) == len(df_polars), (
97
+ f"Row count mismatch! Pandas returned {len(df_pandas)} rows, "
98
+ f"Polars returned {len(df_polars)} rows"
99
+ )
100
+
101
+
102
+ def test_data_polars_timeshift_timedelta():
103
+ """
104
+ Test timeshift parameter handling when passed as timedelta.
105
+
106
+ Tests the conversion of timedelta(minutes=-2) to integer offset.
107
+ """
108
+ start = datetime(2024, 7, 18, 9, 30, tzinfo=timezone.utc)
109
+ mock_df = _create_mock_ohlc_data(start, periods=300)
110
+
111
+ asset = Asset("HIMS", asset_type=Asset.AssetType.STOCK)
112
+
113
+ # Create Data instance
114
+ data_pandas = Data(
115
+ asset=asset,
116
+ df=mock_df.copy(),
117
+ timestep="minute",
118
+ quote=asset,
119
+ )
120
+
121
+ # Create DataPolars instance
122
+ mock_df_reset = mock_df.reset_index()
123
+ mock_df_reset.columns = ["datetime", "open", "high", "low", "close", "volume"]
124
+ mock_polars = pl.from_pandas(mock_df_reset)
125
+
126
+ data_polars = DataPolars(
127
+ asset=asset,
128
+ df=mock_polars,
129
+ timestep="minute",
130
+ quote=asset,
131
+ )
132
+
133
+ test_dt = datetime(2024, 7, 18, 10, 0, tzinfo=timezone.utc)
134
+
135
+ # Test with timedelta parameter (this is what the backtest engine uses)
136
+ timeshift_td = timedelta(minutes=-2)
137
+
138
+ # get_bars() returns DataFrames directly
139
+ df_pandas = data_pandas.get_bars(
140
+ dt=test_dt,
141
+ length=2,
142
+ timestep="minute",
143
+ timeshift=timeshift_td
144
+ )
145
+
146
+ df_polars = data_polars.get_bars(
147
+ dt=test_dt,
148
+ length=2,
149
+ timestep="minute",
150
+ timeshift=timeshift_td
151
+ )
152
+
153
+ assert len(df_pandas) == 2, f"Pandas should return 2 rows with timedelta timeshift"
154
+ assert len(df_polars) == 2, f"Polars should return 2 rows with timedelta timeshift"
155
+ assert len(df_pandas) == len(df_polars), "Row count mismatch with timedelta timeshift"
156
+
157
+
158
+ if __name__ == "__main__":
159
+ # Run tests with verbose output
160
+ pytest.main([__file__, "-v", "-s"])
@@ -2,6 +2,8 @@
2
2
  Tests for DataBento asset type validation
3
3
  """
4
4
  import pytest
5
+ import pandas as pd
6
+ import polars as pl
5
7
  from datetime import datetime, timedelta
6
8
  from unittest.mock import Mock, patch
7
9
 
@@ -26,8 +28,19 @@ class TestDataBentoAssetValidation:
26
28
  for asset in future_assets:
27
29
  # Should not raise an exception during validation
28
30
  # (We'll mock the actual API call)
29
- with patch('lumibot.data_sources.databento_data.databento_helper.get_price_data_from_databento') as mock_get_data:
30
- mock_get_data.return_value = Mock()
31
+ with patch(
32
+ 'lumibot.data_sources.databento_data_pandas.databento_helper_polars.get_price_data_from_databento_polars'
33
+ ) as mock_get_data:
34
+ mock_get_data.return_value = pl.DataFrame(
35
+ {
36
+ "datetime": [datetime.now()],
37
+ "open": [100.0],
38
+ "high": [101.0],
39
+ "low": [99.0],
40
+ "close": [100.5],
41
+ "volume": [1000],
42
+ }
43
+ )
31
44
  try:
32
45
  data_source.get_historical_prices(asset, 10, "minute")
33
46
  # If we get here, validation passed
@@ -49,9 +62,14 @@ class TestDataBentoAssetValidation:
49
62
  Asset("SPY", "stock"), # string format
50
63
  ]
51
64
 
52
- for asset in equity_assets:
53
- with pytest.raises(ValueError, match="only supports futures assets"):
54
- data_source.get_historical_prices(asset, 10, "minute")
65
+ with patch(
66
+ 'lumibot.data_sources.databento_data_pandas.databento_helper_polars.get_price_data_from_databento_polars'
67
+ ) as mock_get_data:
68
+ for asset in equity_assets:
69
+ result = data_source.get_historical_prices(asset, 10, "minute")
70
+ assert result is None
71
+
72
+ mock_get_data.assert_not_called()
55
73
 
56
74
  def test_helper_function_allows_all_assets(self):
57
75
  """Test that helper function allows all asset types (validation is only in live data source)"""
@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
4
4
  import pandas as pd
5
5
  import pytz
6
6
 
7
- from lumibot.backtesting.databento_backtesting import DataBentoDataBacktesting
7
+ from lumibot.backtesting.databento_backtesting_pandas import DataBentoDataBacktestingPandas as DataBentoDataBacktesting
8
8
  from lumibot.entities import Asset, Data
9
9
 
10
10