lumibot 4.1.3__py3-none-any.whl → 4.2.1__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 (163) 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 +1167 -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 +2 -1
  31. lumibot/strategies/strategy.py +61 -49
  32. lumibot/tools/backtest_cache.py +284 -0
  33. lumibot/tools/databento_helper.py +35 -35
  34. lumibot/tools/databento_helper_polars.py +738 -775
  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.3.dist-info → lumibot-4.2.1.dist-info}/METADATA +9 -1
  40. {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/RECORD +71 -147
  41. tests/backtest/test_databento.py +37 -6
  42. tests/backtest/test_databento_comprehensive_trading.py +8 -4
  43. tests/backtest/test_databento_parity.py +4 -2
  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 +3 -3
  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 +6 -0
  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_live.py +10 -10
  61. tests/test_futures_roll.py +38 -0
  62. tests/test_indicator_subplots.py +101 -0
  63. tests/test_market_infinite_loop_bug.py +77 -3
  64. tests/test_polars_resample.py +67 -0
  65. tests/test_polygon_helper.py +46 -0
  66. tests/test_thetadata_backwards_compat.py +97 -0
  67. tests/test_thetadata_helper.py +222 -23
  68. tests/test_thetadata_pandas_verification.py +186 -0
  69. lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
  70. lumibot/__pycache__/constants.cpython-312.pyc +0 -0
  71. lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
  72. lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
  73. lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
  74. lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
  75. lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
  76. lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
  77. lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
  78. lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
  79. lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
  80. lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
  81. lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
  82. lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
  83. lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
  84. lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
  85. lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
  86. lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
  87. lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
  88. lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
  89. lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
  90. lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
  91. lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
  92. lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
  93. lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
  94. lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
  95. lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
  96. lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
  97. lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
  98. lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
  99. lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
  100. lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
  101. lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
  102. lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
  103. lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
  104. lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
  105. lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
  106. lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
  107. lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
  108. lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
  109. lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
  110. lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
  111. lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
  112. lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
  113. lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
  114. lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
  115. lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
  116. lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
  117. lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
  118. lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
  119. lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
  120. lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
  121. lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
  122. lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
  123. lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
  124. lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
  125. lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
  126. lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
  127. lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
  128. lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  129. lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
  130. lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  131. lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
  132. lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
  133. lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
  134. lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  135. lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
  136. lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
  137. lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
  138. lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
  139. lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
  140. lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
  141. lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
  142. lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
  143. lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
  144. lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
  145. lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
  146. lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
  147. lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
  148. lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
  149. lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
  150. lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
  151. lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
  152. lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
  153. lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
  154. lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
  155. lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
  156. lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
  157. lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
  158. lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
  159. lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
  160. lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
  161. {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/WHEEL +0 -0
  162. {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/licenses/LICENSE +0 -0
  163. {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/top_level.txt +0 -0
@@ -103,6 +103,9 @@ class TestBacktestingDataSourceEnv:
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
@@ -244,6 +247,9 @@ class TestBacktestingDataSourceEnv:
244
247
  # Remove BACKTESTING_DATA_SOURCE from env
245
248
  env_without_datasource = {k: v for k, v in os.environ.items() if k != 'BACKTESTING_DATA_SOURCE'}
246
249
 
250
+ import logging
251
+ caplog.set_level(logging.INFO, logger='lumibot.strategies._strategy')
252
+
247
253
  with patch.dict(os.environ, env_without_datasource, clear=True):
248
254
  # Re-import credentials to pick up env change
249
255
  from importlib import reload
@@ -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