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.
- lumibot/backtesting/__init__.py +19 -5
- lumibot/backtesting/backtesting_broker.py +98 -18
- lumibot/backtesting/databento_backtesting.py +5 -686
- lumibot/backtesting/databento_backtesting_pandas.py +738 -0
- lumibot/backtesting/databento_backtesting_polars.py +860 -546
- lumibot/backtesting/fix_debug.py +37 -0
- lumibot/backtesting/thetadata_backtesting.py +9 -355
- lumibot/backtesting/thetadata_backtesting_pandas.py +1178 -0
- lumibot/brokers/alpaca.py +8 -1
- lumibot/brokers/schwab.py +12 -2
- lumibot/credentials.py +13 -0
- lumibot/data_sources/__init__.py +5 -8
- lumibot/data_sources/data_source.py +6 -2
- lumibot/data_sources/data_source_backtesting.py +30 -0
- lumibot/data_sources/databento_data.py +5 -390
- lumibot/data_sources/databento_data_pandas.py +440 -0
- lumibot/data_sources/databento_data_polars.py +15 -9
- lumibot/data_sources/pandas_data.py +30 -17
- lumibot/data_sources/polars_data.py +986 -0
- lumibot/data_sources/polars_mixin.py +472 -96
- lumibot/data_sources/polygon_data_polars.py +5 -0
- lumibot/data_sources/yahoo_data.py +9 -2
- lumibot/data_sources/yahoo_data_polars.py +5 -0
- lumibot/entities/__init__.py +15 -0
- lumibot/entities/asset.py +5 -28
- lumibot/entities/bars.py +89 -20
- lumibot/entities/data.py +29 -6
- lumibot/entities/data_polars.py +668 -0
- lumibot/entities/position.py +38 -4
- lumibot/strategies/_strategy.py +31 -9
- lumibot/strategies/strategy.py +61 -49
- lumibot/tools/backtest_cache.py +284 -0
- lumibot/tools/databento_helper.py +65 -42
- lumibot/tools/databento_helper_polars.py +748 -778
- lumibot/tools/futures_roll.py +251 -0
- lumibot/tools/indicators.py +135 -104
- lumibot/tools/polars_utils.py +142 -0
- lumibot/tools/thetadata_helper.py +1068 -134
- {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/METADATA +9 -1
- {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/RECORD +72 -148
- tests/backtest/test_databento.py +37 -6
- tests/backtest/test_databento_comprehensive_trading.py +70 -87
- tests/backtest/test_databento_parity.py +31 -7
- tests/backtest/test_debug_avg_fill_price.py +1 -1
- tests/backtest/test_example_strategies.py +11 -1
- tests/backtest/test_futures_edge_cases.py +96 -63
- tests/backtest/test_futures_single_trade.py +2 -2
- tests/backtest/test_futures_ultra_simple.py +2 -2
- tests/backtest/test_polars_lru_eviction.py +470 -0
- tests/backtest/test_yahoo.py +42 -0
- tests/test_asset.py +4 -4
- tests/test_backtest_cache_manager.py +149 -0
- tests/test_backtesting_data_source_env.py +50 -10
- tests/test_continuous_futures_resolution.py +60 -48
- tests/test_data_polars_parity.py +160 -0
- tests/test_databento_asset_validation.py +23 -5
- tests/test_databento_backtesting.py +1 -1
- tests/test_databento_backtesting_polars.py +312 -192
- tests/test_databento_data.py +220 -463
- tests/test_databento_helper.py +6 -1
- tests/test_databento_live.py +10 -10
- tests/test_futures_roll.py +38 -0
- tests/test_indicator_subplots.py +101 -0
- tests/test_market_infinite_loop_bug.py +77 -3
- tests/test_polars_resample.py +67 -0
- tests/test_polygon_helper.py +46 -0
- tests/test_thetadata_backwards_compat.py +97 -0
- tests/test_thetadata_helper.py +222 -23
- tests/test_thetadata_pandas_verification.py +186 -0
- lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/__pycache__/constants.cpython-312.pyc +0 -0
- lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
- lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
- lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
- lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
- {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/WHEEL +0 -0
- {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/licenses/LICENSE +0 -0
- {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("
|
|
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("
|
|
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
|
|
187
|
-
"""Test that
|
|
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
|
|
209
|
-
assert
|
|
210
|
-
|
|
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 (
|
|
244
|
-
assert any(
|
|
245
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
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
|
-
|
|
296
|
-
f"Date {test_date.strftime('%Y-%m-%d')} should resolve to {
|
|
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(
|
|
30
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
7
|
+
from lumibot.backtesting.databento_backtesting_pandas import DataBentoDataBacktestingPandas as DataBentoDataBacktesting
|
|
8
8
|
from lumibot.entities import Asset, Data
|
|
9
9
|
|
|
10
10
|
|