lumibot 4.1.3__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 +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.0.dist-info}/METADATA +9 -1
- {lumibot-4.1.3.dist-info → lumibot-4.2.0.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.0.dist-info}/WHEEL +0 -0
- {lumibot-4.1.3.dist-info → lumibot-4.2.0.dist-info}/licenses/LICENSE +0 -0
- {lumibot-4.1.3.dist-info → lumibot-4.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,201 +1,321 @@
|
|
|
1
|
-
import
|
|
2
|
-
from datetime import datetime, timedelta
|
|
3
|
-
from unittest.mock import patch
|
|
4
|
-
|
|
1
|
+
import pandas as pd
|
|
5
2
|
import polars as pl
|
|
6
|
-
import
|
|
3
|
+
import pytest
|
|
4
|
+
from datetime import datetime, timezone, timedelta
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
7
6
|
|
|
7
|
+
from lumibot.tools.databento_helper_polars import DataBentoAuthenticationError
|
|
8
8
|
from lumibot.backtesting.databento_backtesting_polars import DataBentoDataBacktestingPolars
|
|
9
9
|
from lumibot.entities import Asset
|
|
10
|
-
from lumibot.tools.databento_helper_polars import get_price_data_from_databento_polars
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class TestDataBentoDataBacktestingPolars(unittest.TestCase):
|
|
14
|
-
"""Regression tests for the polars DataBento backtesting implementation."""
|
|
15
|
-
|
|
16
|
-
def setUp(self):
|
|
17
|
-
self.api_key = "test_key"
|
|
18
|
-
self.start_date = datetime(2022, 1, 1)
|
|
19
|
-
self.end_date = datetime(2022, 12, 31)
|
|
20
|
-
self.asset = Asset("MNQ", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
21
|
-
self.utc = pytz.UTC
|
|
22
|
-
|
|
23
|
-
@patch("lumibot.tools.databento_helper_polars.get_price_data_from_databento_polars")
|
|
24
|
-
def test_daily_history_is_fetched_once_for_full_range(self, mock_get_data):
|
|
25
|
-
"""Daily data should be cached so repeated requests avoid redundant API calls."""
|
|
26
|
-
date_range = pl.datetime_range(
|
|
27
|
-
start=datetime(2021, 12, 1, tzinfo=self.utc),
|
|
28
|
-
end=datetime(2022, 12, 30, tzinfo=self.utc),
|
|
29
|
-
interval="1d",
|
|
30
|
-
eager=True,
|
|
31
|
-
)
|
|
32
|
-
num_rows = len(date_range)
|
|
33
|
-
base_values = [float(i) for i in range(num_rows)]
|
|
34
|
-
mock_get_data.return_value = pl.DataFrame(
|
|
35
|
-
{
|
|
36
|
-
"datetime": date_range,
|
|
37
|
-
"open": [v + 1.0 for v in base_values],
|
|
38
|
-
"high": [v + 2.0 for v in base_values],
|
|
39
|
-
"low": base_values,
|
|
40
|
-
"close": [v + 1.5 for v in base_values],
|
|
41
|
-
"volume": [1000.0] * num_rows,
|
|
42
|
-
}
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
datasource = DataBentoDataBacktestingPolars(
|
|
46
|
-
datetime_start=self.start_date,
|
|
47
|
-
datetime_end=self.end_date,
|
|
48
|
-
api_key=self.api_key,
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
# First request should trigger a fetch
|
|
52
|
-
datasource._datetime = self.start_date + timedelta(days=40)
|
|
53
|
-
first_bars = datasource.get_historical_prices(
|
|
54
|
-
self.asset,
|
|
55
|
-
length=20,
|
|
56
|
-
timestep="day",
|
|
57
|
-
return_polars=True,
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
self.assertIsNotNone(first_bars)
|
|
61
|
-
self.assertGreaterEqual(first_bars.df.height, 20)
|
|
62
|
-
self.assertEqual(mock_get_data.call_count, 1)
|
|
63
|
-
|
|
64
|
-
metadata = datasource._cache_metadata.get((self.asset, "day"))
|
|
65
|
-
self.assertIsNotNone(metadata)
|
|
66
|
-
|
|
67
|
-
# Subsequent request later in the backtest should use cached data only
|
|
68
|
-
datasource._datetime = self.end_date - timedelta(days=5)
|
|
69
|
-
second_bars = datasource.get_historical_prices(
|
|
70
|
-
self.asset,
|
|
71
|
-
length=20,
|
|
72
|
-
timestep="day",
|
|
73
|
-
return_polars=True,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
self.assertIsNotNone(second_bars)
|
|
77
|
-
self.assertGreaterEqual(second_bars.df.height, 20)
|
|
78
|
-
self.assertEqual(mock_get_data.call_count, 1, "Expected cached data to satisfy the second call")
|
|
79
|
-
|
|
80
|
-
metadata = datasource._cache_metadata.get((self.asset, "day"))
|
|
81
|
-
self.assertIsNotNone(metadata)
|
|
82
|
-
max_dt = datasource._to_naive_datetime(metadata.get("max_dt"))
|
|
83
|
-
expected_end = datasource._to_naive_datetime(datasource.datetime_end)
|
|
84
|
-
# Allow a small tolerance because fetched data is midnight whereas the backtest end is end-of-day
|
|
85
|
-
self.assertGreaterEqual(max_dt, expected_end - timedelta(days=2))
|
|
86
|
-
|
|
87
|
-
@patch("lumibot.tools.databento_helper_polars.get_price_data_from_databento_polars")
|
|
88
|
-
def test_minute_history_request_has_valid_range(self, mock_get_data):
|
|
89
|
-
"""Minute requests should never invert the start/end timestamps handed to DataBento."""
|
|
90
|
-
|
|
91
|
-
captured = {}
|
|
92
|
-
|
|
93
|
-
def fake_databento_fetch(api_key, asset, start, end, timestep, venue=None, force_cache_update=False, reference_date=None, **kwargs):
|
|
94
|
-
captured["start"] = start
|
|
95
|
-
captured["end"] = end
|
|
96
|
-
date_range = pl.datetime_range(
|
|
97
|
-
start=datetime(2022, 1, 31, 22, 0, tzinfo=self.utc),
|
|
98
|
-
end=datetime(2022, 2, 1, 0, 0, tzinfo=self.utc),
|
|
99
|
-
interval="1m",
|
|
100
|
-
eager=True,
|
|
101
|
-
)
|
|
102
|
-
base_values = [float(i) for i in range(len(date_range))]
|
|
103
|
-
return pl.DataFrame(
|
|
104
|
-
{
|
|
105
|
-
"datetime": date_range,
|
|
106
|
-
"open": base_values,
|
|
107
|
-
"high": [v + 1.0 for v in base_values],
|
|
108
|
-
"low": [v - 1.0 for v in base_values],
|
|
109
|
-
"close": base_values,
|
|
110
|
-
"volume": [10.0] * len(base_values),
|
|
111
|
-
"symbol": ["MNQH2"] * len(base_values),
|
|
112
|
-
}
|
|
113
|
-
)
|
|
114
10
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
11
|
+
API_KEY = "test_key"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def mocked_polars_helper(monkeypatch):
|
|
16
|
+
monkeypatch.setattr(
|
|
17
|
+
"lumibot.tools.databento_helper_polars.DataBentoClient",
|
|
18
|
+
MagicMock(),
|
|
19
|
+
)
|
|
20
|
+
monkeypatch.setattr(
|
|
21
|
+
"lumibot.tools.databento_helper_polars.DATABENTO_AVAILABLE",
|
|
22
|
+
True,
|
|
23
|
+
)
|
|
24
|
+
monkeypatch.setattr(
|
|
25
|
+
"lumibot.tools.databento_helper_polars._fetch_and_update_futures_multiplier",
|
|
26
|
+
lambda *args, **kwargs: None,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _polars_frame(start_minute: int, rows: int = 5) -> pl.DataFrame:
|
|
31
|
+
base = datetime(2025, 1, 6, 14, 0, tzinfo=timezone.utc)
|
|
32
|
+
datetimes = [base + timedelta(minutes=start_minute + i) for i in range(rows)]
|
|
33
|
+
return pl.DataFrame(
|
|
34
|
+
{
|
|
35
|
+
"datetime": datetimes,
|
|
36
|
+
"open": [100.0 + i * 0.1 for i in range(rows)],
|
|
37
|
+
"high": [100.2 + i * 0.1 for i in range(rows)],
|
|
38
|
+
"low": [99.8 + i * 0.1 for i in range(rows)],
|
|
39
|
+
"close": [100.1 + i * 0.1 for i in range(rows)],
|
|
40
|
+
"volume": [1200 + i * 5 for i in range(rows)],
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.usefixtures("mocked_polars_helper")
|
|
46
|
+
def test_initialization_sets_properties():
|
|
47
|
+
start = datetime(2025, 1, 1, tzinfo=timezone.utc)
|
|
48
|
+
end = datetime(2025, 1, 10, tzinfo=timezone.utc)
|
|
49
|
+
backtester = DataBentoDataBacktestingPolars(
|
|
50
|
+
datetime_start=start,
|
|
51
|
+
datetime_end=end,
|
|
52
|
+
api_key=API_KEY,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert backtester._api_key == API_KEY
|
|
56
|
+
assert backtester.datetime_start == start
|
|
57
|
+
# Implementation subtracts one minute from the end boundary to keep the last
|
|
58
|
+
# candle fully formed.
|
|
59
|
+
assert backtester.datetime_end == end - timedelta(minutes=1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.usefixtures("mocked_polars_helper")
|
|
63
|
+
@patch(
|
|
64
|
+
"lumibot.backtesting.databento_backtesting_polars.databento_helper.get_price_data_from_databento"
|
|
65
|
+
)
|
|
66
|
+
def test_prefetch_data_populates_cache(mock_get_data):
|
|
67
|
+
mock_get_data.return_value = _polars_frame(0, rows=8)
|
|
68
|
+
start = datetime(2025, 2, 3, tzinfo=timezone.utc)
|
|
69
|
+
end = datetime(2025, 2, 5, tzinfo=timezone.utc)
|
|
70
|
+
asset = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
71
|
+
|
|
72
|
+
backtester = DataBentoDataBacktestingPolars(
|
|
73
|
+
datetime_start=start,
|
|
74
|
+
datetime_end=end,
|
|
75
|
+
api_key=API_KEY,
|
|
76
|
+
show_progress_bar=False,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if not hasattr(backtester, "prefetch_data"):
|
|
80
|
+
pytest.skip("prefetch_data not implemented for polars backtesting backend")
|
|
81
|
+
|
|
82
|
+
backtester.prefetch_data([asset], timestep="minute")
|
|
83
|
+
assert (asset, Asset("USD", "forex")) in backtester._prefetched_assets
|
|
84
|
+
mock_get_data.assert_called_once()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.mark.usefixtures("mocked_polars_helper")
|
|
88
|
+
@patch(
|
|
89
|
+
"lumibot.backtesting.databento_backtesting_polars.databento_helper.get_price_data_from_databento"
|
|
90
|
+
)
|
|
91
|
+
def test_get_historical_prices_returns_bars(mock_get_data):
|
|
92
|
+
mock_get_data.return_value = _polars_frame(0, rows=20)
|
|
93
|
+
start = datetime(2025, 3, 3, tzinfo=timezone.utc)
|
|
94
|
+
end = datetime(2025, 3, 4, tzinfo=timezone.utc)
|
|
95
|
+
asset = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
96
|
+
|
|
97
|
+
backtester = DataBentoDataBacktestingPolars(
|
|
98
|
+
datetime_start=start,
|
|
99
|
+
datetime_end=end,
|
|
100
|
+
api_key=API_KEY,
|
|
101
|
+
show_progress_bar=False,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
bars = backtester.get_historical_prices(asset, length=10, timestep="minute")
|
|
105
|
+
assert bars is not None
|
|
106
|
+
assert bars.polars_df.height == 10
|
|
107
|
+
mock_get_data.assert_called()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.mark.usefixtures("mocked_polars_helper")
|
|
111
|
+
@patch(
|
|
112
|
+
"lumibot.backtesting.databento_backtesting_polars.databento_helper.get_price_data_from_databento"
|
|
113
|
+
)
|
|
114
|
+
def test_get_last_price_returns_close(mock_get_data):
|
|
115
|
+
mock_get_data.return_value = _polars_frame(0, rows=5)
|
|
116
|
+
start = datetime(2025, 4, 1, tzinfo=timezone.utc)
|
|
117
|
+
end = datetime(2025, 4, 2, tzinfo=timezone.utc)
|
|
118
|
+
asset = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
119
|
+
|
|
120
|
+
backtester = DataBentoDataBacktestingPolars(
|
|
121
|
+
datetime_start=start,
|
|
122
|
+
datetime_end=end,
|
|
123
|
+
api_key=API_KEY,
|
|
124
|
+
show_progress_bar=False,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
price = backtester.get_last_price(asset)
|
|
128
|
+
expected = mock_get_data.return_value.tail(1)["close"][0]
|
|
129
|
+
assert price == pytest.approx(float(expected))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.mark.usefixtures("mocked_polars_helper")
|
|
133
|
+
def test_get_historical_prices_non_future_returns_none():
|
|
134
|
+
start = datetime(2025, 5, 1, tzinfo=timezone.utc)
|
|
135
|
+
end = datetime(2025, 5, 2, tzinfo=timezone.utc)
|
|
136
|
+
asset = Asset("AAPL", asset_type=Asset.AssetType.STOCK)
|
|
137
|
+
|
|
138
|
+
backtester = DataBentoDataBacktestingPolars(
|
|
139
|
+
datetime_start=start,
|
|
140
|
+
datetime_end=end,
|
|
141
|
+
api_key=API_KEY,
|
|
142
|
+
show_progress_bar=False,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
bars = backtester.get_historical_prices(asset, length=5)
|
|
146
|
+
assert bars is None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@pytest.mark.usefixtures("mocked_polars_helper")
|
|
150
|
+
def test_databento_polars_quote_midpoint(monkeypatch):
|
|
151
|
+
start = datetime(2025, 6, 1, tzinfo=timezone.utc)
|
|
152
|
+
end = datetime(2025, 6, 2, tzinfo=timezone.utc)
|
|
153
|
+
asset = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
154
|
+
|
|
155
|
+
backtester = DataBentoDataBacktestingPolars(
|
|
156
|
+
datetime_start=start,
|
|
157
|
+
datetime_end=end,
|
|
158
|
+
api_key=API_KEY,
|
|
159
|
+
show_progress_bar=False,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
current_dt = datetime(2025, 6, 1, 15, 30, tzinfo=timezone.utc)
|
|
163
|
+
monkeypatch.setattr(backtester, "get_datetime", lambda: current_dt)
|
|
164
|
+
|
|
165
|
+
sample_df = pl.DataFrame(
|
|
166
|
+
{
|
|
167
|
+
"datetime": [current_dt],
|
|
168
|
+
"open": [4300.0],
|
|
169
|
+
"high": [4301.0],
|
|
170
|
+
"low": [4299.5],
|
|
171
|
+
"close": [4300.5],
|
|
172
|
+
"volume": [1500],
|
|
173
|
+
"bid": [4299.75],
|
|
174
|
+
"ask": [4301.25],
|
|
175
|
+
"bid_size": [5],
|
|
176
|
+
"ask_size": [6],
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def fake_pull(self, *_args, **_kwargs):
|
|
181
|
+
return sample_df
|
|
182
|
+
|
|
183
|
+
monkeypatch.setattr(backtester, "_pull_source_symbol_bars", fake_pull.__get__(backtester, type(backtester)))
|
|
184
|
+
|
|
185
|
+
quote = backtester.get_quote(asset)
|
|
186
|
+
expected_mid = (sample_df["bid"][0] + sample_df["ask"][0]) / 2.0
|
|
187
|
+
|
|
188
|
+
assert quote.mid_price == pytest.approx(expected_mid)
|
|
189
|
+
assert quote.price == pytest.approx(expected_mid)
|
|
190
|
+
assert getattr(quote, "source", None) == "polars"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@pytest.mark.usefixtures("mocked_polars_helper")
|
|
194
|
+
def test_get_historical_prices_reuses_cache(monkeypatch, tmp_path):
|
|
195
|
+
"""Second identical request should hit disk cache instead of refetching."""
|
|
196
|
+
|
|
197
|
+
cache_dir = tmp_path / "databento_cache"
|
|
198
|
+
cache_dir.mkdir()
|
|
199
|
+
|
|
200
|
+
monkeypatch.setattr(
|
|
201
|
+
"lumibot.tools.databento_helper_polars.LUMIBOT_DATABENTO_CACHE_FOLDER",
|
|
202
|
+
str(cache_dir),
|
|
203
|
+
raising=False,
|
|
204
|
+
)
|
|
205
|
+
monkeypatch.setattr(
|
|
206
|
+
"lumibot.backtesting.databento_backtesting_polars.databento_helper.LUMIBOT_DATABENTO_CACHE_FOLDER",
|
|
207
|
+
str(cache_dir),
|
|
208
|
+
raising=False,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
fetch_calls = 0
|
|
212
|
+
|
|
213
|
+
class FakeClient:
|
|
214
|
+
def __init__(self, *args, **kwargs):
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
def get_historical_data(self, dataset, symbols, schema, start, end, **kwargs):
|
|
218
|
+
nonlocal fetch_calls
|
|
219
|
+
fetch_calls += 1
|
|
220
|
+
index = pd.date_range(start=start, periods=5, freq="1min", tz="UTC")
|
|
221
|
+
return pd.DataFrame(
|
|
152
222
|
{
|
|
153
|
-
"
|
|
154
|
-
"open":
|
|
155
|
-
"high": [
|
|
156
|
-
"low": [
|
|
157
|
-
"close":
|
|
158
|
-
"volume": [1_000
|
|
159
|
-
"symbol": [symbol_code] * len(rng),
|
|
223
|
+
"ts_event": index,
|
|
224
|
+
"open": [100.0 + i for i in range(5)],
|
|
225
|
+
"high": [100.5 + i for i in range(5)],
|
|
226
|
+
"low": [99.5 + i for i in range(5)],
|
|
227
|
+
"close": [100.2 + i for i in range(5)],
|
|
228
|
+
"volume": [1_000 + 10 * i for i in range(5)],
|
|
160
229
|
}
|
|
161
230
|
)
|
|
162
231
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
232
|
+
monkeypatch.setattr(
|
|
233
|
+
"lumibot.tools.databento_helper_polars.DataBentoClient",
|
|
234
|
+
FakeClient,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
start = datetime(2025, 7, 1, tzinfo=timezone.utc)
|
|
238
|
+
end = datetime(2025, 7, 2, tzinfo=timezone.utc)
|
|
239
|
+
asset = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
240
|
+
|
|
241
|
+
backtester = DataBentoDataBacktestingPolars(
|
|
242
|
+
datetime_start=start,
|
|
243
|
+
datetime_end=end,
|
|
244
|
+
api_key=API_KEY,
|
|
245
|
+
show_progress_bar=False,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
first_bars = backtester.get_historical_prices(asset, length=5, timestep="minute", return_polars=True)
|
|
249
|
+
second_bars = backtester.get_historical_prices(asset, length=5, timestep="minute", return_polars=True)
|
|
250
|
+
|
|
251
|
+
assert first_bars is not None and second_bars is not None
|
|
252
|
+
pd.testing.assert_frame_equal(second_bars.pandas_df, first_bars.pandas_df)
|
|
253
|
+
assert fetch_calls == 1, "Expected cached response on second call"
|
|
254
|
+
assert list(cache_dir.glob("*.parquet")), "Cache directory should contain parquet artifacts"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@pytest.mark.usefixtures("mocked_polars_helper")
|
|
258
|
+
def test_auth_failure_propagates(monkeypatch):
|
|
259
|
+
start = datetime(2025, 1, 6, tzinfo=timezone.utc)
|
|
260
|
+
end = datetime(2025, 1, 7, tzinfo=timezone.utc)
|
|
261
|
+
asset = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
262
|
+
|
|
263
|
+
def boom(*args, **kwargs):
|
|
264
|
+
raise DataBentoAuthenticationError("401 auth_authentication_failed")
|
|
265
|
+
|
|
266
|
+
monkeypatch.setattr(
|
|
267
|
+
"lumibot.backtesting.databento_backtesting_polars.databento_helper.get_price_data_from_databento",
|
|
268
|
+
boom,
|
|
269
|
+
)
|
|
270
|
+
monkeypatch.setattr(
|
|
271
|
+
"lumibot.tools.databento_helper_polars.get_price_data_from_databento",
|
|
272
|
+
boom,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
backtester = DataBentoDataBacktestingPolars(
|
|
276
|
+
datetime_start=start,
|
|
277
|
+
datetime_end=end,
|
|
278
|
+
api_key=API_KEY,
|
|
279
|
+
show_progress_bar=False,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
with pytest.raises(DataBentoAuthenticationError):
|
|
283
|
+
backtester.get_historical_prices(asset, length=1, timestep="minute", return_polars=True)
|
|
284
|
+
|
|
285
|
+
@patch(
|
|
286
|
+
"lumibot.backtesting.databento_backtesting_polars.databento_helper.get_price_data_from_databento"
|
|
287
|
+
)
|
|
288
|
+
@pytest.mark.usefixtures("mocked_polars_helper")
|
|
289
|
+
def test_polars_no_future_minutes(mock_get_data, mocked_polars_helper):
|
|
290
|
+
base = datetime(2025, 1, 6, 14, 30, tzinfo=timezone.utc)
|
|
291
|
+
frame = pl.DataFrame(
|
|
292
|
+
{
|
|
293
|
+
"datetime": [base - timedelta(minutes=1), base + timedelta(minutes=1)],
|
|
294
|
+
"open": [4300.0, 4302.0],
|
|
295
|
+
"high": [4300.5, 4302.5],
|
|
296
|
+
"low": [4299.5, 4301.5],
|
|
297
|
+
"close": [4300.2, 4302.2],
|
|
298
|
+
"volume": [1500, 1510],
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
mock_get_data.return_value = frame
|
|
302
|
+
|
|
303
|
+
asset = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
304
|
+
backtester = DataBentoDataBacktestingPolars(
|
|
305
|
+
datetime_start=base - timedelta(days=1),
|
|
306
|
+
datetime_end=base + timedelta(days=1),
|
|
307
|
+
api_key=API_KEY,
|
|
308
|
+
show_progress_bar=False,
|
|
309
|
+
)
|
|
310
|
+
backtester._datetime = base
|
|
311
|
+
|
|
312
|
+
bars = backtester.get_historical_prices(
|
|
313
|
+
asset,
|
|
314
|
+
length=1,
|
|
315
|
+
timestep="minute",
|
|
316
|
+
return_polars=True,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
assert bars is not None
|
|
320
|
+
# Ensure we never look past the current iteration timestamp.
|
|
321
|
+
assert bars.polars_df["datetime"][-1] <= base
|