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
@@ -49,7 +49,12 @@ class TestDataBentoHelper(unittest.TestCase):
49
49
  self.assertRegex(result, r"ES[FGHJKMNQUVXZ]\d", "Should auto-resolve to contract format like ESZ5")
50
50
 
51
51
  # Test specific contract with expiration (March 2025 = H25)
52
- result = databento_helper._format_futures_symbol_for_databento(self.test_asset_future)
52
+ specific_future = Asset(
53
+ symbol="ES",
54
+ asset_type="future",
55
+ expiration=datetime(2025, 3, 15).date()
56
+ )
57
+ result = databento_helper._format_futures_symbol_for_databento(specific_future)
53
58
  self.assertEqual(result, "ESH25") # March 2025 = H25
54
59
 
55
60
  # Test another month (December 2024 = Z24)
@@ -27,13 +27,13 @@ load_dotenv()
27
27
  def test_symbol_resolution():
28
28
  """Test that symbols are properly resolved to contract codes"""
29
29
  from lumibot.entities import Asset
30
- from lumibot.data_sources.databento_data_polars_live import DataBentoDataPolarsLive
30
+ from lumibot.data_sources.databento_data_polars import DataBentoDataPolars
31
31
 
32
32
  print("\n" + "="*60)
33
33
  print("TEST 1: Symbol Resolution")
34
34
  print("="*60)
35
35
 
36
- data_source = DataBentoDataPolarsLive(
36
+ data_source = DataBentoDataPolars(
37
37
  api_key=os.getenv('DATABENTO_API_KEY'),
38
38
  has_paid_subscription=True,
39
39
  enable_live_stream=False # Don't need streaming for this test
@@ -97,14 +97,14 @@ def test_live_api_connection():
97
97
  def test_minute_bar_aggregation():
98
98
  """Test minute bar aggregation with <1 minute lag"""
99
99
  from lumibot.entities import Asset
100
- from lumibot.data_sources.databento_data_polars_live import DataBentoDataPolarsLive
100
+ from lumibot.data_sources.databento_data_polars import DataBentoDataPolars
101
101
 
102
102
  print("\n" + "="*60)
103
103
  print("TEST 3: Minute Bar Aggregation & Latency")
104
104
  print("="*60)
105
105
 
106
106
  # Initialize with Live API
107
- data_source = DataBentoDataPolarsLive(
107
+ data_source = DataBentoDataPolars(
108
108
  api_key=os.getenv('DATABENTO_API_KEY'),
109
109
  has_paid_subscription=True,
110
110
  enable_live_stream=True
@@ -175,13 +175,13 @@ def test_minute_bar_aggregation():
175
175
  )
176
176
  def test_api_routing():
177
177
  """Test that correct API is used based on time range"""
178
- from lumibot.data_sources.databento_data_polars_live import DataBentoDataPolarsLive
178
+ from lumibot.data_sources.databento_data_polars import DataBentoDataPolars
179
179
 
180
180
  print("\n" + "="*60)
181
181
  print("TEST 4: API Routing (Live vs Historical)")
182
182
  print("="*60)
183
183
 
184
- data_source = DataBentoDataPolarsLive(
184
+ data_source = DataBentoDataPolars(
185
185
  api_key=os.getenv('DATABENTO_API_KEY'),
186
186
  has_paid_subscription=True,
187
187
  enable_live_stream=True
@@ -219,13 +219,13 @@ def test_api_routing():
219
219
  def test_long_time_periods():
220
220
  """Test different time periods including long periods (500+ bars)"""
221
221
  from lumibot.entities import Asset
222
- from lumibot.data_sources.databento_data_polars_live import DataBentoDataPolarsLive
222
+ from lumibot.data_sources.databento_data_polars import DataBentoDataPolars
223
223
 
224
224
  print("\n" + "="*60)
225
225
  print("TEST 5: Long Time Period Handling (500+ bars)")
226
226
  print("="*60)
227
227
 
228
- data_source = DataBentoDataPolarsLive(
228
+ data_source = DataBentoDataPolars(
229
229
  api_key=os.getenv('DATABENTO_API_KEY'),
230
230
  has_paid_subscription=True,
231
231
  enable_live_stream=True
@@ -324,14 +324,14 @@ def test_long_time_periods():
324
324
  def test_continuous_latency_monitoring():
325
325
  """Run continuous tests to verify consistent <1 minute lag"""
326
326
  from lumibot.entities import Asset
327
- from lumibot.data_sources.databento_data_polars_live import DataBentoDataPolarsLive
327
+ from lumibot.data_sources.databento_data_polars import DataBentoDataPolars
328
328
 
329
329
  print("\n" + "="*60)
330
330
  print("TEST 6: Continuous Latency Monitoring")
331
331
  print("="*60)
332
332
  print("Running 5 consecutive tests to verify consistent low latency...")
333
333
 
334
- data_source = DataBentoDataPolarsLive(
334
+ data_source = DataBentoDataPolars(
335
335
  api_key=os.getenv('DATABENTO_API_KEY'),
336
336
  has_paid_subscription=True,
337
337
  enable_live_stream=True
@@ -0,0 +1,38 @@
1
+ import datetime
2
+
3
+ import pytz
4
+
5
+ from lumibot.entities import Asset
6
+ from lumibot.tools import futures_roll
7
+
8
+ NY = pytz.timezone("America/New_York")
9
+
10
+
11
+ def _dt(year: int, month: int, day: int) -> datetime.datetime:
12
+ return NY.localize(datetime.datetime(year, month, day))
13
+
14
+
15
+ def test_equity_index_roll_eight_business_days_before_expiry():
16
+ asset = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
17
+
18
+ year, month = futures_roll.determine_contract_year_month(asset.symbol, _dt(2025, 9, 8))
19
+ assert (year, month) == (2025, 9)
20
+
21
+ year, month = futures_roll.determine_contract_year_month(asset.symbol, _dt(2025, 9, 10))
22
+ assert (year, month) == (2025, 12)
23
+
24
+
25
+ def test_fallback_mid_month_preserved_for_unknown_symbols():
26
+ asset = Asset("XYZ", asset_type=Asset.AssetType.CONT_FUTURE)
27
+
28
+ year, month = futures_roll.determine_contract_year_month(asset.symbol, _dt(2025, 3, 16))
29
+ assert (year, month) == (2025, 6)
30
+
31
+
32
+ def test_resolve_symbols_for_range_produces_sequential_contracts():
33
+ asset = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
34
+ start = _dt(2025, 8, 1)
35
+ end = _dt(2025, 12, 31)
36
+
37
+ symbols = futures_roll.resolve_symbols_for_range(asset, start, end, year_digits=1)
38
+ assert symbols == ["MESU5", "MESZ5", "MESH6"], symbols
@@ -3,10 +3,12 @@ from datetime import datetime as DateTime
3
3
  from unittest.mock import MagicMock
4
4
 
5
5
  import pandas as pd
6
+ import plotly.graph_objects as go
6
7
 
7
8
  from lumibot.backtesting import PandasDataBacktesting
8
9
  from lumibot.strategies.strategy import Strategy
9
10
  from lumibot.entities import Asset
11
+ from lumibot.tools.indicators import _build_trade_marker_tooltip, plot_returns
10
12
 
11
13
  from tests.fixtures import pandas_data_fixture
12
14
 
@@ -179,6 +181,105 @@ class TestIndicators:
179
181
  logger.info(f"Result: {result}")
180
182
  assert result is not None
181
183
 
184
+
185
+ def _make_trade_row_for_tooltip(status, trade_cost=pd.NA):
186
+ return pd.Series(
187
+ {
188
+ "status": status,
189
+ "filled_quantity": 10,
190
+ "price": 2.5,
191
+ "asset.multiplier": 100,
192
+ "trade_cost": trade_cost,
193
+ "symbol": "WDC",
194
+ "asset.asset_type": "option",
195
+ "asset.right": "CALL",
196
+ "asset.strike": 86,
197
+ "asset.expiration": "2025-09-19",
198
+ "type": "limit",
199
+ }
200
+ )
201
+
202
+
203
+ def test_cash_settled_tooltip_generated_without_trade_cost():
204
+ tooltip = _build_trade_marker_tooltip(_make_trade_row_for_tooltip("cash_settled", trade_cost=pd.NA))
205
+ assert tooltip is not None
206
+ assert "cash_settled" in tooltip
207
+
208
+
209
+ def test_non_terminal_status_filtered_out():
210
+ assert _build_trade_marker_tooltip(_make_trade_row_for_tooltip("new", trade_cost=pd.NA)) is None
211
+
212
+
213
+ def test_plot_returns_preserves_cash_settled_status(tmp_path, monkeypatch):
214
+ plot_path = tmp_path / "plot.html"
215
+
216
+ def _fake_write_html(self, file, auto_open=True, **kwargs):
217
+ # Prevent plotly from opening a browser during the test
218
+ return file
219
+
220
+ monkeypatch.setattr(go.Figure, "write_html", _fake_write_html, raising=False)
221
+
222
+ idx = pd.to_datetime(
223
+ ["2025-09-04 00:00:00-04:00", "2025-09-20 00:00:00-04:00"]
224
+ ).tz_convert("UTC")
225
+
226
+ strategy_df = pd.DataFrame(
227
+ {
228
+ "return": [0.0, 0.0],
229
+ "cash": [100000, 120000],
230
+ "positions": [
231
+ [{"asset": "WDC", "quantity": 25}],
232
+ [],
233
+ ],
234
+ },
235
+ index=idx,
236
+ )
237
+
238
+ benchmark_df = pd.DataFrame(
239
+ {
240
+ "return": [0.0, 0.0],
241
+ "open": [1.0, 1.0],
242
+ "high": [1.0, 1.0],
243
+ "low": [1.0, 1.0],
244
+ "close": [1.0, 1.0],
245
+ },
246
+ index=idx,
247
+ )
248
+
249
+ trades_df = pd.DataFrame(
250
+ [
251
+ {
252
+ "time": "2025-09-20 00:00:00-04:00",
253
+ "side": "sell",
254
+ "status": "cash_settled",
255
+ "filled_quantity": 25,
256
+ "symbol": "WDC",
257
+ "asset.asset_type": "option",
258
+ "asset.right": "CALL",
259
+ "asset.strike": 86,
260
+ "asset.expiration": "2025-09-19",
261
+ "price": 20.86,
262
+ "type": "cash_settled",
263
+ "asset.multiplier": 100,
264
+ "trade_cost": pd.NA,
265
+ }
266
+ ]
267
+ )
268
+
269
+ plot_returns(
270
+ strategy_df,
271
+ "Strategy",
272
+ benchmark_df,
273
+ "Benchmark",
274
+ plot_file_html=str(plot_path),
275
+ trades_df=trades_df,
276
+ show_plot=True,
277
+ initial_budget=1,
278
+ )
279
+
280
+ trades_csv = pd.read_csv(plot_path.with_suffix(".csv"))
281
+ assert "cash_settled" in trades_csv["status"].tolist()
282
+
182
283
  def test_named_lines(self, pandas_data_fixture):
183
284
  """Test the named lines"""
184
285
  strategy_name = "TestIndicatorStrategy"
@@ -16,11 +16,16 @@ STATUS: ✅ FIXED - ES futures now complete normally (1 restart vs infinite)
16
16
 
17
17
  import unittest
18
18
  from unittest.mock import patch
19
- from datetime import datetime
19
+ from datetime import datetime, timedelta
20
20
 
21
+ import pandas as pd
22
+
23
+ from lumibot.credentials import DATABENTO_CONFIG
21
24
  from lumibot.strategies import Strategy
22
- from lumibot.entities import Asset, TradingFee
23
- from lumibot.backtesting import DataBentoDataBacktesting
25
+ from lumibot.entities import Asset, TradingFee, Bars, Order
26
+ from lumibot.backtesting import BacktestingBroker, DataBentoDataBacktesting
27
+
28
+ DATABENTO_API_KEY = DATABENTO_CONFIG.get("API_KEY")
24
29
 
25
30
 
26
31
  class ESFuturesTestStrategy(Strategy):
@@ -39,10 +44,13 @@ class TestESFuturesHangBug(unittest.TestCase):
39
44
  """Test that ES futures strategies no longer hang/restart infinitely"""
40
45
 
41
46
  def setUp(self):
47
+ if not DATABENTO_API_KEY or DATABENTO_API_KEY == "<your key here>":
48
+ self.skipTest("DataBento API key required for DataBento backtesting tests")
42
49
  self.backtesting_params = {
43
50
  'datasource_class': DataBentoDataBacktesting,
44
51
  'backtesting_start': datetime(2025, 6, 5),
45
52
  'backtesting_end': datetime(2025, 6, 6),
53
+ 'api_key': DATABENTO_API_KEY,
46
54
  'show_plot': False,
47
55
  'show_tearsheet': False,
48
56
  'show_indicators': False,
@@ -299,3 +307,69 @@ class TestESFuturesHangBug(unittest.TestCase):
299
307
  if __name__ == '__main__':
300
308
  print("🧪 Testing ES Futures hang bug fix...")
301
309
  unittest.main(verbosity=2)
310
+
311
+
312
+ def test_broker_timeshift_guard():
313
+ captured = []
314
+
315
+ class StubDataSource:
316
+ SOURCE = "DATABENTO_POLARS"
317
+ IS_BACKTESTING_DATA_SOURCE = True
318
+
319
+ def __init__(self):
320
+ self._datetime = datetime(2025, 6, 5, 14, 30)
321
+
322
+ def get_historical_prices(self, asset, length, quote=None, timeshift=None, **kwargs):
323
+ captured.append(timeshift)
324
+ index = pd.DatetimeIndex([self._datetime - timedelta(minutes=1)])
325
+ frame = pd.DataFrame(
326
+ {
327
+ 'open': [4300.0],
328
+ 'high': [4301.0],
329
+ 'low': [4299.5],
330
+ 'close': [4300.5],
331
+ 'volume': [1500],
332
+ },
333
+ index=index,
334
+ )
335
+ target_asset = asset[0] if isinstance(asset, tuple) else asset
336
+ return Bars(frame, self.SOURCE, target_asset, raw=frame)
337
+
338
+ def get_datetime(self):
339
+ return self._datetime
340
+
341
+ broker = BacktestingBroker(data_source=StubDataSource())
342
+ broker._datetime = broker.data_source.get_datetime()
343
+
344
+ order = Order(
345
+ strategy="stub",
346
+ asset=Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE),
347
+ quantity=1,
348
+ side=Order.OrderSide.BUY,
349
+ )
350
+ order.order_type = Order.OrderType.MARKET
351
+ order.quote = Asset("USD", asset_type=Asset.AssetType.FOREX)
352
+ broker._new_orders.append(order)
353
+
354
+ class StubStrategy:
355
+ name = "stub"
356
+ buy_trading_fees = []
357
+ sell_trading_fees = []
358
+ timestep = 'minute'
359
+ bars_lookback = 1
360
+
361
+ def __init__(self, broker):
362
+ self.broker = broker
363
+ self.cash = 100000.0
364
+ self.quote_asset = Asset('USD', asset_type=Asset.AssetType.FOREX)
365
+
366
+ def log_message(self, *args, **kwargs):
367
+ return None
368
+
369
+ def _set_cash_position(self, value):
370
+ self.cash = value
371
+
372
+ broker.process_pending_orders(strategy=StubStrategy(broker))
373
+
374
+ assert captured, "BacktestingBroker did not request historical data"
375
+ assert captured[0] == timedelta(minutes=-2)
@@ -0,0 +1,67 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ import polars as pl
4
+ import pytest
5
+
6
+ from lumibot.tools.polars_utils import PolarsResampleError, resample_polars_ohlc
7
+
8
+
9
+ def _minute_frame(rows: int = 10) -> pl.DataFrame:
10
+ start = datetime(2024, 1, 1, 9, 30)
11
+ datetimes = [start + timedelta(minutes=i) for i in range(rows)]
12
+ return pl.DataFrame(
13
+ {
14
+ "datetime": datetimes,
15
+ "open": [100 + i for i in range(rows)],
16
+ "high": [101 + i for i in range(rows)],
17
+ "low": [99 + i for i in range(rows)],
18
+ "close": [100.5 + i for i in range(rows)],
19
+ "volume": [1_000 + 10 * i for i in range(rows)],
20
+ "signal": [i for i in range(rows)],
21
+ }
22
+ )
23
+
24
+
25
+ def test_resample_polars_minute_to_5min():
26
+ df = _minute_frame(10)
27
+ resampled = resample_polars_ohlc(df, multiplier=5, base_unit="minute", length=2)
28
+
29
+ assert resampled.height == 2
30
+ # First bucket should cover rows 0-4
31
+ first = resampled.row(0, named=True)
32
+ assert first["open"] == 100
33
+ assert first["high"] == 105
34
+ assert first["low"] == 99
35
+ assert first["close"] == pytest.approx(104.5)
36
+ assert first["volume"] == sum(1_000 + 10 * i for i in range(5))
37
+ # signal column keeps last observation in the bucket
38
+ assert first["signal"] == 4
39
+
40
+ # Tail limiting should keep last 2 buckets only
41
+ assert resampled.row(1, named=True)["signal"] == 9
42
+
43
+
44
+ def test_resample_polars_day_bucket():
45
+ start = datetime(2024, 1, 1, 0, 0)
46
+ datetimes = [start + timedelta(hours=i) for i in range(48)]
47
+ df = pl.DataFrame(
48
+ {
49
+ "datetime": datetimes,
50
+ "open": [10 + i for i in range(48)],
51
+ "high": [12 + i for i in range(48)],
52
+ "low": [8 + i for i in range(48)],
53
+ "close": [11 + i for i in range(48)],
54
+ "volume": [100 + i for i in range(48)],
55
+ }
56
+ )
57
+
58
+ resampled = resample_polars_ohlc(df, multiplier=1, base_unit="day", length=None)
59
+ assert resampled.height == 2
60
+ assert resampled["open"][0] == 10
61
+ assert resampled["close"][1] == 11 + 47
62
+
63
+
64
+ def test_resample_polars_invalid_unit():
65
+ df = _minute_frame(2)
66
+ with pytest.raises(PolarsResampleError):
67
+ resample_polars_ohlc(df, multiplier=1, base_unit="hour")
@@ -7,6 +7,7 @@ import pytest
7
7
  import pytz
8
8
 
9
9
  from lumibot.entities import Asset
10
+ from lumibot.backtesting import PolygonDataBacktesting
10
11
  from lumibot.entities.chains import normalize_option_chains
11
12
  from lumibot.tools import polygon_helper as ph
12
13
 
@@ -131,6 +132,8 @@ class TestPolygonHelpers:
131
132
  assert datetime.date(2023, 7, 10) in trading_dates
132
133
 
133
134
  def test_get_polygon_symbol(self, mocker):
135
+
136
+
134
137
  polygon_client = mocker.MagicMock()
135
138
 
136
139
  # ------- Unsupported Asset Type
@@ -568,3 +571,46 @@ class TestPolygonPriceData:
568
571
  # Should return identical data once normalized
569
572
  assert normalize_option_chains(result_second) == normalized_first
570
573
  assert mock_polyclient.list_options_contracts.call_count == 0
574
+
575
+ def test_polygon_no_future_bars_before_open(self, monkeypatch):
576
+ tz = pytz.timezone('America/New_York')
577
+ now = tz.localize(datetime.datetime(2023, 11, 1, 9, 30))
578
+ frame = pd.DataFrame(
579
+ {
580
+ 'open': [377.0, 378.5],
581
+ 'high': [377.5, 379.0],
582
+ 'low': [376.8, 378.2],
583
+ 'close': [377.2, 378.9],
584
+ 'volume': [10_000, 10_500],
585
+ },
586
+ index=pd.DatetimeIndex([
587
+ tz.localize(datetime.datetime(2023, 11, 1, 9, 29)),
588
+ tz.localize(datetime.datetime(2023, 11, 1, 9, 31)),
589
+ ]),
590
+ )
591
+
592
+ monkeypatch.setattr(
593
+ 'lumibot.backtesting.polygon_backtesting.polygon_helper.get_price_data_from_polygon',
594
+ lambda *args, **kwargs: frame,
595
+ )
596
+
597
+ data_source = PolygonDataBacktesting(
598
+ datetime_start=now - datetime.timedelta(days=1),
599
+ datetime_end=now + datetime.timedelta(days=1),
600
+ api_key='dummy',
601
+ )
602
+ data_source._datetime = now
603
+ asset = Asset('SPY')
604
+ quote = Asset('USD', 'forex')
605
+
606
+ bars = data_source.get_historical_prices(
607
+ asset,
608
+ length=1,
609
+ timestep='minute',
610
+ quote=quote,
611
+ timeshift=datetime.timedelta(minutes=-1),
612
+ )
613
+
614
+ assert bars.df.index[-1] <= now
615
+
616
+
@@ -0,0 +1,97 @@
1
+ """
2
+ ThetaData pandas compatibility tests.
3
+
4
+ Ensures the helper returns pandas DataFrames by default and raises a clear
5
+ error when callers request polars output (which is intentionally unsupported
6
+ in this branch).
7
+ """
8
+
9
+ from datetime import datetime, timezone
10
+ import pandas as pd
11
+ import pytest
12
+
13
+ from lumibot.entities import Asset
14
+ from lumibot.tools import thetadata_helper
15
+
16
+
17
+ def _mock_cache_frame(start: datetime, rows: int = 8) -> pd.DataFrame:
18
+ index = pd.date_range(start=start, periods=rows, freq="1min", tz="UTC")
19
+ df = pd.DataFrame(
20
+ {
21
+ "open": [200 + i for i in range(rows)],
22
+ "high": [200.5 + i for i in range(rows)],
23
+ "low": [199.5 + i for i in range(rows)],
24
+ "close": [200.25 + i for i in range(rows)],
25
+ "volume": [10_000 + 50 * i for i in range(rows)],
26
+ "missing": [False] * rows,
27
+ },
28
+ index=index,
29
+ )
30
+ return df
31
+
32
+
33
+ def test_get_price_data_returns_pandas_when_cache_hit(monkeypatch, tmp_path):
34
+ """Cache path with no missing intervals should return pandas DataFrame."""
35
+ cache_file = tmp_path / "spy.minute.ohlc.parquet"
36
+ cache_file.write_text("placeholder")
37
+
38
+ mock_df = _mock_cache_frame(datetime(2025, 1, 1, tzinfo=timezone.utc))
39
+
40
+ monkeypatch.setattr(
41
+ thetadata_helper,
42
+ "build_cache_filename",
43
+ lambda *args, **kwargs: cache_file,
44
+ )
45
+ monkeypatch.setattr(thetadata_helper, "load_cache", lambda _: mock_df)
46
+ monkeypatch.setattr(thetadata_helper, "get_missing_dates", lambda *args, **kwargs: [])
47
+ monkeypatch.setattr(thetadata_helper, "update_cache", lambda *args, **kwargs: None)
48
+
49
+ asset = Asset("SPY", asset_type=Asset.AssetType.STOCK)
50
+
51
+ result = thetadata_helper.get_price_data(
52
+ username="demo",
53
+ password="demo",
54
+ asset=asset,
55
+ start=datetime(2025, 1, 1, tzinfo=timezone.utc),
56
+ end=datetime(2025, 1, 2, tzinfo=timezone.utc),
57
+ timespan="minute",
58
+ datastyle="ohlc",
59
+ include_after_hours=True,
60
+ )
61
+
62
+ assert isinstance(result, pd.DataFrame)
63
+ assert not result.empty
64
+ assert "missing" not in result.columns
65
+
66
+
67
+ def test_get_price_data_polars_request_rejected(monkeypatch, tmp_path):
68
+ """Requesting return_polars=True should raise a clear ValueError."""
69
+ cache_file = tmp_path / "spy.minute.ohlc.parquet"
70
+ cache_file.write_text("placeholder")
71
+
72
+ mock_df = _mock_cache_frame(datetime(2025, 1, 1, tzinfo=timezone.utc))
73
+
74
+ monkeypatch.setattr(
75
+ thetadata_helper,
76
+ "build_cache_filename",
77
+ lambda *args, **kwargs: cache_file,
78
+ )
79
+ monkeypatch.setattr(thetadata_helper, "load_cache", lambda _: mock_df)
80
+ monkeypatch.setattr(thetadata_helper, "get_missing_dates", lambda *args, **kwargs: [])
81
+
82
+ asset = Asset("SPY", asset_type=Asset.AssetType.STOCK)
83
+
84
+ with pytest.raises(ValueError) as excinfo:
85
+ thetadata_helper.get_price_data(
86
+ username="demo",
87
+ password="demo",
88
+ asset=asset,
89
+ start=datetime(2025, 1, 1, tzinfo=timezone.utc),
90
+ end=datetime(2025, 1, 2, tzinfo=timezone.utc),
91
+ timespan="minute",
92
+ datastyle="ohlc",
93
+ include_after_hours=True,
94
+ return_polars=True,
95
+ )
96
+
97
+ assert "polars output" in str(excinfo.value).lower()