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.
- 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 +1167 -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 +2 -1
- lumibot/strategies/strategy.py +61 -49
- lumibot/tools/backtest_cache.py +284 -0
- lumibot/tools/databento_helper.py +35 -35
- lumibot/tools/databento_helper_polars.py +738 -775
- 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.3.dist-info → lumibot-4.2.1.dist-info}/METADATA +9 -1
- {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/RECORD +71 -147
- tests/backtest/test_databento.py +37 -6
- tests/backtest/test_databento_comprehensive_trading.py +8 -4
- tests/backtest/test_databento_parity.py +4 -2
- 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 +3 -3
- 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 +6 -0
- 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_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.3.dist-info → lumibot-4.2.1.dist-info}/WHEEL +0 -0
- {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
|