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
tests/test_databento_data.py
CHANGED
|
@@ -1,493 +1,250 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import unittest
|
|
2
|
-
from
|
|
3
|
-
from
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
4
6
|
import pandas as pd
|
|
7
|
+
import polars as pl
|
|
5
8
|
|
|
6
|
-
from lumibot.data_sources
|
|
9
|
+
from lumibot.data_sources import DataBentoData
|
|
7
10
|
from lumibot.entities import Asset, Bars
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
class TestDataBentoData(unittest.TestCase):
|
|
11
|
-
"""
|
|
14
|
+
"""Unit tests for the canonical DataBento data source (Polars-backed)."""
|
|
12
15
|
|
|
13
16
|
def setUp(self):
|
|
14
|
-
"""Set up test fixtures"""
|
|
15
17
|
self.api_key = "test_api_key"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
patchers = [
|
|
19
|
+
patch("lumibot.tools.databento_helper.DATABENTO_AVAILABLE", True),
|
|
20
|
+
patch("lumibot.tools.databento_helper_polars.DATABENTO_AVAILABLE", True),
|
|
21
|
+
patch("lumibot.tools.databento_helper_polars.DataBentoClient", MagicMock()),
|
|
22
|
+
patch("lumibot.tools.databento_helper_polars._fetch_and_update_futures_multiplier", lambda *args, **kwargs: None),
|
|
23
|
+
]
|
|
24
|
+
for patcher in patchers:
|
|
25
|
+
patched = patcher.start()
|
|
26
|
+
self.addCleanup(patcher.stop)
|
|
27
|
+
|
|
28
|
+
import importlib
|
|
29
|
+
|
|
30
|
+
polars_module = importlib.import_module("lumibot.data_sources.databento_data_polars")
|
|
31
|
+
patcher_db = patch.object(polars_module, "db", MagicMock())
|
|
32
|
+
patcher_db.start()
|
|
33
|
+
self.addCleanup(patcher_db.stop)
|
|
34
|
+
|
|
35
|
+
self.future_asset = Asset(
|
|
20
36
|
symbol="ES",
|
|
21
|
-
asset_type=
|
|
22
|
-
expiration=datetime(2025, 3, 15).date()
|
|
23
|
-
)
|
|
37
|
+
asset_type=Asset.AssetType.FUTURE,
|
|
38
|
+
expiration=datetime(2025, 3, 15).date(),
|
|
39
|
+
)
|
|
40
|
+
self.cont_future_asset = Asset(
|
|
41
|
+
symbol="MES",
|
|
42
|
+
asset_type=Asset.AssetType.CONT_FUTURE,
|
|
43
|
+
)
|
|
44
|
+
self.equity_asset = Asset("AAPL", asset_type=Asset.AssetType.STOCK)
|
|
45
|
+
# Disable live streaming threads for unit-speed tests
|
|
46
|
+
self.datasource_kwargs = {"api_key": self.api_key, "enable_live_stream": False}
|
|
47
|
+
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
# Helpers
|
|
50
|
+
# ------------------------------------------------------------------
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _polars_ohlcv(rows: int = 3) -> pl.DataFrame:
|
|
53
|
+
base_time = datetime(2025, 1, 1, 9, 30, tzinfo=timezone.utc)
|
|
54
|
+
minutes = [base_time + timedelta(minutes=i) for i in range(rows)]
|
|
55
|
+
return pl.DataFrame(
|
|
56
|
+
{
|
|
57
|
+
"datetime": minutes,
|
|
58
|
+
"open": [100.0 + i for i in range(rows)],
|
|
59
|
+
"high": [101.0 + i for i in range(rows)],
|
|
60
|
+
"low": [99.0 + i for i in range(rows)],
|
|
61
|
+
"close": [100.5 + i for i in range(rows)],
|
|
62
|
+
"volume": [1_000 + 10 * i for i in range(rows)],
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _bars(self, rows: int = 2) -> Bars:
|
|
67
|
+
df = self._polars_ohlcv(rows)
|
|
68
|
+
return Bars(df=df, source="DATABENTO", asset=self.future_asset)
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
# Initialization
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
def test_initialization_sets_core_attributes(self):
|
|
74
|
+
data_source = DataBentoData(**self.datasource_kwargs)
|
|
24
75
|
|
|
25
|
-
@patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
|
|
26
|
-
def test_initialization_success(self):
|
|
27
|
-
"""Test successful initialization"""
|
|
28
|
-
data_source = DataBentoData(
|
|
29
|
-
api_key=self.api_key,
|
|
30
|
-
datetime_start=self.start_date,
|
|
31
|
-
datetime_end=self.end_date
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
self.assertEqual(data_source.name, "databento")
|
|
35
|
-
self.assertEqual(data_source.SOURCE, "DATABENTO")
|
|
36
76
|
self.assertEqual(data_source._api_key, self.api_key)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
77
|
+
self.assertEqual(data_source.SOURCE, "DATABENTO")
|
|
78
|
+
# Live streaming disabled for tests should be reflected on the instance
|
|
79
|
+
self.assertFalse(data_source.enable_live_stream)
|
|
80
|
+
# Name comes from DataSource base class
|
|
81
|
+
self.assertEqual(data_source.name, "data_source")
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# Historical data
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
def test_get_historical_prices_returns_bars(self):
|
|
87
|
+
with patch(
|
|
88
|
+
"lumibot.data_sources.databento_data_polars.databento_helper_polars.get_price_data_from_databento_polars",
|
|
89
|
+
return_value=self._polars_ohlcv(3),
|
|
90
|
+
) as mock_get_data:
|
|
91
|
+
data_source = DataBentoData(**self.datasource_kwargs)
|
|
92
|
+
bars = data_source.get_historical_prices(
|
|
93
|
+
asset=self.future_asset,
|
|
94
|
+
length=3,
|
|
95
|
+
timestep="minute",
|
|
46
96
|
)
|
|
47
97
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
with patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True):
|
|
51
|
-
data_source = DataBentoData(api_key=self.api_key)
|
|
52
|
-
|
|
53
|
-
# Should have set API key and other attributes
|
|
54
|
-
self.assertIsNotNone(data_source._api_key)
|
|
55
|
-
self.assertEqual(data_source._api_key, self.api_key)
|
|
56
|
-
self.assertIsNotNone(data_source.name)
|
|
57
|
-
self.assertEqual(data_source.name, "databento")
|
|
58
|
-
self.assertFalse(data_source.is_backtesting_mode) # Default is False for live trading
|
|
59
|
-
|
|
60
|
-
@patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
|
|
61
|
-
@patch('lumibot.tools.databento_helper.get_price_data_from_databento')
|
|
62
|
-
def test_get_historical_prices_success(self, mock_get_data):
|
|
63
|
-
"""Test successful historical price retrieval"""
|
|
64
|
-
# Create test data
|
|
65
|
-
test_df = pd.DataFrame({
|
|
66
|
-
'open': [100.0, 101.0, 102.0],
|
|
67
|
-
'high': [102.0, 103.0, 104.0],
|
|
68
|
-
'low': [99.0, 100.0, 101.0],
|
|
69
|
-
'close': [101.0, 102.0, 103.0],
|
|
70
|
-
'volume': [1000, 1100, 1200]
|
|
71
|
-
})
|
|
72
|
-
test_df.index = pd.to_datetime([
|
|
73
|
-
'2025-01-01 09:30:00',
|
|
74
|
-
'2025-01-01 09:31:00',
|
|
75
|
-
'2025-01-01 09:32:00'
|
|
76
|
-
])
|
|
77
|
-
|
|
78
|
-
mock_get_data.return_value = test_df
|
|
79
|
-
|
|
80
|
-
# Initialize data source
|
|
81
|
-
data_source = DataBentoData(
|
|
82
|
-
api_key=self.api_key,
|
|
83
|
-
datetime_start=self.start_date,
|
|
84
|
-
datetime_end=self.end_date
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
# Set current datetime for backtesting
|
|
88
|
-
data_source._datetime = datetime(2025, 1, 1, 10, 0, 0)
|
|
89
|
-
|
|
90
|
-
# Get historical prices
|
|
91
|
-
result = data_source.get_historical_prices(
|
|
92
|
-
asset=self.test_asset,
|
|
93
|
-
length=3,
|
|
94
|
-
timestep="minute"
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
# Verify result
|
|
98
|
-
self.assertIsInstance(result, Bars)
|
|
99
|
-
self.assertEqual(len(result.df), 3)
|
|
100
|
-
|
|
101
|
-
# Verify mock was called with correct parameters
|
|
98
|
+
self.assertIsInstance(bars, Bars)
|
|
99
|
+
self.assertEqual(len(bars.df), 3)
|
|
102
100
|
mock_get_data.assert_called_once()
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
result = data_source.get_historical_prices(
|
|
117
|
-
asset=self.test_asset,
|
|
118
|
-
length=10,
|
|
119
|
-
timestep="minute"
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
self.assertIsNone(result)
|
|
123
|
-
|
|
124
|
-
@patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
|
|
125
|
-
@patch('lumibot.tools.databento_helper.get_last_price_from_databento')
|
|
126
|
-
def test_get_last_price_success(self, mock_get_last_price):
|
|
127
|
-
"""Test successful last price retrieval"""
|
|
128
|
-
mock_get_last_price.return_value = 4250.75
|
|
129
|
-
|
|
130
|
-
data_source = DataBentoData(
|
|
131
|
-
api_key=self.api_key,
|
|
132
|
-
datetime_start=self.start_date,
|
|
133
|
-
datetime_end=self.end_date
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
result = data_source.get_last_price(asset=self.test_asset)
|
|
137
|
-
|
|
138
|
-
self.assertEqual(result, 4250.75)
|
|
139
|
-
mock_get_last_price.assert_called_once_with(
|
|
140
|
-
api_key=self.api_key,
|
|
141
|
-
asset=self.test_asset,
|
|
142
|
-
venue=None
|
|
143
|
-
)
|
|
102
|
+
def test_get_historical_prices_returns_none_for_non_futures(self):
|
|
103
|
+
with patch(
|
|
104
|
+
"lumibot.data_sources.databento_data_polars.databento_helper_polars.get_price_data_from_databento_polars"
|
|
105
|
+
) as mock_get_data:
|
|
106
|
+
data_source = DataBentoData(**self.datasource_kwargs)
|
|
107
|
+
result = data_source.get_historical_prices(
|
|
108
|
+
asset=self.equity_asset,
|
|
109
|
+
length=5,
|
|
110
|
+
timestep="minute",
|
|
111
|
+
)
|
|
144
112
|
|
|
145
|
-
@patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
|
|
146
|
-
@patch('lumibot.tools.databento_helper.get_last_price_from_databento')
|
|
147
|
-
def test_get_last_price_no_data(self, mock_get_last_price):
|
|
148
|
-
"""Test last price retrieval with no data"""
|
|
149
|
-
mock_get_last_price.return_value = None
|
|
150
|
-
|
|
151
|
-
data_source = DataBentoData(
|
|
152
|
-
api_key=self.api_key,
|
|
153
|
-
datetime_start=self.start_date,
|
|
154
|
-
datetime_end=self.end_date
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
result = data_source.get_last_price(asset=self.test_asset)
|
|
158
|
-
|
|
159
113
|
self.assertIsNone(result)
|
|
114
|
+
mock_get_data.assert_not_called()
|
|
115
|
+
|
|
116
|
+
def test_get_historical_prices_handles_exceptions(self):
|
|
117
|
+
data_source = DataBentoData(**self.datasource_kwargs)
|
|
118
|
+
with patch(
|
|
119
|
+
"lumibot.data_sources.databento_data_polars.databento_helper_polars.get_price_data_from_databento_polars",
|
|
120
|
+
side_effect=RuntimeError("boom"),
|
|
121
|
+
):
|
|
122
|
+
with self.assertRaises(RuntimeError):
|
|
123
|
+
data_source.get_historical_prices(
|
|
124
|
+
asset=self.future_asset,
|
|
125
|
+
length=2,
|
|
126
|
+
timestep="minute",
|
|
127
|
+
)
|
|
160
128
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
self.assertEqual(result, {})
|
|
173
|
-
|
|
174
|
-
@patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
|
|
175
|
-
@patch('lumibot.tools.databento_helper.get_price_data_from_databento')
|
|
176
|
-
def test_get_historical_prices_single_asset(self, mock_get_data):
|
|
177
|
-
"""Test historical price retrieval for a single asset"""
|
|
178
|
-
# Create test data
|
|
179
|
-
test_df = pd.DataFrame({
|
|
180
|
-
'open': [100.0, 101.0],
|
|
181
|
-
'high': [102.0, 103.0],
|
|
182
|
-
'low': [99.0, 100.0],
|
|
183
|
-
'close': [101.0, 102.0],
|
|
184
|
-
'volume': [1000, 1100]
|
|
185
|
-
})
|
|
186
|
-
test_df.index = pd.to_datetime([
|
|
187
|
-
'2025-01-01 09:30:00',
|
|
188
|
-
'2025-01-01 09:31:00'
|
|
189
|
-
])
|
|
190
|
-
|
|
191
|
-
mock_get_data.return_value = test_df
|
|
192
|
-
|
|
193
|
-
data_source = DataBentoData(api_key=self.api_key)
|
|
194
|
-
|
|
195
|
-
result = data_source.get_historical_prices(
|
|
196
|
-
asset=self.test_asset,
|
|
197
|
-
length=2,
|
|
198
|
-
timestep="minute"
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
self.assertIsNotNone(result)
|
|
202
|
-
self.assertEqual(len(result.df), 2)
|
|
203
|
-
self.assertEqual(result.asset, self.test_asset)
|
|
204
|
-
self.assertEqual(result.source, "DATABENTO")
|
|
205
|
-
|
|
206
|
-
@patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
|
|
207
|
-
@patch('lumibot.tools.databento_helper.get_price_data_from_databento')
|
|
208
|
-
def test_get_historical_prices_error_handling(self, mock_get_data):
|
|
209
|
-
"""Test error handling in get_historical_prices"""
|
|
210
|
-
# Setup: Mock to raise exception
|
|
211
|
-
mock_get_data.side_effect = Exception("Test error")
|
|
212
|
-
|
|
213
|
-
data_source = DataBentoData(api_key=self.api_key)
|
|
214
|
-
|
|
215
|
-
# This should not raise an exception but return None
|
|
216
|
-
result = data_source.get_historical_prices(
|
|
217
|
-
asset=self.test_asset,
|
|
218
|
-
length=2,
|
|
219
|
-
timestep="minute"
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
self.assertIsNone(result)
|
|
129
|
+
def test_get_historical_prices_trims_to_requested_length(self):
|
|
130
|
+
with patch(
|
|
131
|
+
"lumibot.data_sources.databento_data_polars.databento_helper_polars.get_price_data_from_databento_polars",
|
|
132
|
+
return_value=self._polars_ohlcv(10),
|
|
133
|
+
):
|
|
134
|
+
data_source = DataBentoData(**self.datasource_kwargs)
|
|
135
|
+
bars = data_source.get_historical_prices(
|
|
136
|
+
asset=self.future_asset,
|
|
137
|
+
length=4,
|
|
138
|
+
timestep="minute",
|
|
139
|
+
)
|
|
223
140
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
data_source = DataBentoData(api_key=self.api_key)
|
|
278
|
-
|
|
279
|
-
# Test that _parse_source_bars handles empty data
|
|
280
|
-
result = data_source._parse_source_bars({self.test_asset: test_df})
|
|
281
|
-
|
|
282
|
-
self.assertIsInstance(result, dict)
|
|
283
|
-
self.assertIn(self.test_asset, result)
|
|
284
|
-
|
|
285
|
-
@patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
|
|
286
|
-
def test_timestep_mapping(self):
|
|
287
|
-
"""Test timestep mapping functionality"""
|
|
288
|
-
data_source = DataBentoData(api_key=self.api_key)
|
|
289
|
-
|
|
290
|
-
# Test valid timestep mappings
|
|
291
|
-
test_cases = [
|
|
292
|
-
("minute", "minute"),
|
|
293
|
-
("1m", "minute"),
|
|
294
|
-
("hour", "hour"),
|
|
295
|
-
("1h", "hour"),
|
|
296
|
-
("day", "day"),
|
|
297
|
-
("1d", "day"),
|
|
298
|
-
]
|
|
299
|
-
|
|
300
|
-
for input_timestep, expected in test_cases:
|
|
301
|
-
with self.subTest(timestep=input_timestep):
|
|
302
|
-
result = data_source._parse_source_timestep(input_timestep)
|
|
303
|
-
self.assertEqual(result, expected)
|
|
304
|
-
|
|
305
|
-
@patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
|
|
306
|
-
def test_backtesting_mode_detection(self):
|
|
307
|
-
"""Test that backtesting mode is properly detected"""
|
|
308
|
-
# Test with default initialization (live trading mode)
|
|
309
|
-
data_source = DataBentoData(api_key=self.api_key)
|
|
310
|
-
|
|
311
|
-
# DataBento is currently configured for live trading by default
|
|
312
|
-
self.assertFalse(data_source.is_backtesting_mode)
|
|
313
|
-
|
|
314
|
-
# Test that name and source are set correctly
|
|
315
|
-
self.assertEqual(data_source.name, "databento")
|
|
316
|
-
self.assertEqual(data_source.SOURCE, "DATABENTO")
|
|
141
|
+
self.assertEqual(len(bars.df), 4)
|
|
142
|
+
self.assertTrue((bars.df.index[-1] > bars.df.index[0]))
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
# Last price & quotes
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
def test_get_last_price_uses_historical_fallback(self):
|
|
148
|
+
frame = self._polars_ohlcv(2)
|
|
149
|
+
last_close = float(frame.select("close").to_series().tail(1)[0])
|
|
150
|
+
|
|
151
|
+
with patch(
|
|
152
|
+
"lumibot.data_sources.databento_data_polars.databento_helper_polars.get_price_data_from_databento_polars",
|
|
153
|
+
return_value=frame,
|
|
154
|
+
):
|
|
155
|
+
data_source = DataBentoData(**self.datasource_kwargs)
|
|
156
|
+
price = data_source.get_last_price(asset=self.future_asset)
|
|
157
|
+
|
|
158
|
+
self.assertEqual(price, last_close)
|
|
159
|
+
|
|
160
|
+
def test_get_last_price_returns_none_when_no_data(self):
|
|
161
|
+
with patch(
|
|
162
|
+
"lumibot.data_sources.databento_data_polars.databento_helper_polars.get_price_data_from_databento_polars",
|
|
163
|
+
return_value=None,
|
|
164
|
+
):
|
|
165
|
+
data_source = DataBentoData(**self.datasource_kwargs)
|
|
166
|
+
price = data_source.get_last_price(asset=self.future_asset)
|
|
167
|
+
|
|
168
|
+
self.assertIsNone(price)
|
|
169
|
+
|
|
170
|
+
def test_get_quote_falls_back_to_last_price(self):
|
|
171
|
+
with patch.object(DataBentoData, "get_last_price", return_value=123.45):
|
|
172
|
+
data_source = DataBentoData(**self.datasource_kwargs)
|
|
173
|
+
quote = data_source.get_quote(asset=self.future_asset)
|
|
174
|
+
|
|
175
|
+
self.assertEqual(quote.asset, self.future_asset)
|
|
176
|
+
self.assertEqual(quote.price, 123.45)
|
|
177
|
+
self.assertGreaterEqual(quote.ask, quote.bid)
|
|
178
|
+
|
|
179
|
+
# ------------------------------------------------------------------
|
|
180
|
+
# Continuous futures resolution
|
|
181
|
+
# ------------------------------------------------------------------
|
|
182
|
+
def test_continuous_future_resolves_symbol(self):
|
|
183
|
+
with patch(
|
|
184
|
+
"lumibot.data_sources.databento_data_polars.databento_helper_polars.get_price_data_from_databento_polars",
|
|
185
|
+
return_value=self._polars_ohlcv(2),
|
|
186
|
+
):
|
|
187
|
+
data_source = DataBentoData(**self.datasource_kwargs)
|
|
188
|
+
bars = data_source.get_historical_prices(
|
|
189
|
+
asset=self.cont_future_asset,
|
|
190
|
+
length=2,
|
|
191
|
+
timestep="minute",
|
|
192
|
+
)
|
|
317
193
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
def test_get_historical_prices_backtesting_path(self, mock_get_data):
|
|
321
|
-
"""Test that get_historical_prices uses backtesting path when in backtesting mode"""
|
|
322
|
-
# Create test data
|
|
323
|
-
test_df = pd.DataFrame({
|
|
324
|
-
'open': [100.0, 101.0, 102.0],
|
|
325
|
-
'high': [102.0, 103.0, 104.0],
|
|
326
|
-
'low': [99.0, 100.0, 101.0],
|
|
327
|
-
'close': [101.0, 102.0, 103.0],
|
|
328
|
-
'volume': [1000, 1100, 1200]
|
|
329
|
-
})
|
|
330
|
-
test_df.index = pd.to_datetime([
|
|
331
|
-
'2025-01-01 09:30:00',
|
|
332
|
-
'2025-01-01 09:31:00',
|
|
333
|
-
'2025-01-01 09:32:00'
|
|
334
|
-
])
|
|
335
|
-
|
|
336
|
-
mock_get_data.return_value = test_df
|
|
337
|
-
|
|
338
|
-
data_source = DataBentoData(
|
|
339
|
-
api_key=self.api_key,
|
|
340
|
-
datetime_start=self.start_date,
|
|
341
|
-
datetime_end=self.end_date
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
# Test that the method works correctly
|
|
345
|
-
result = data_source.get_historical_prices(
|
|
346
|
-
asset=self.test_asset,
|
|
347
|
-
length=3,
|
|
348
|
-
timestep="minute"
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
# Verify that the data was retrieved
|
|
352
|
-
self.assertIsNotNone(result)
|
|
353
|
-
self.assertIsInstance(result, Bars)
|
|
354
|
-
self.assertEqual(len(result.df), 3)
|
|
355
|
-
|
|
356
|
-
@patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
|
|
357
|
-
@patch('lumibot.tools.databento_helper.get_price_data_from_databento')
|
|
358
|
-
def test_timezone_handling(self, mock_get_data):
|
|
359
|
-
"""Test timezone handling in get_historical_prices"""
|
|
360
|
-
# Create test data
|
|
361
|
-
test_df = pd.DataFrame({
|
|
362
|
-
'open': [100.0, 101.0],
|
|
363
|
-
'high': [102.0, 103.0],
|
|
364
|
-
'low': [99.0, 100.0],
|
|
365
|
-
'close': [101.0, 102.0],
|
|
366
|
-
'volume': [1000, 1100]
|
|
367
|
-
})
|
|
368
|
-
test_df.index = pd.to_datetime([
|
|
369
|
-
'2025-01-01 09:30:00',
|
|
370
|
-
'2025-01-01 09:31:00'
|
|
371
|
-
])
|
|
372
|
-
|
|
373
|
-
mock_get_data.return_value = test_df
|
|
374
|
-
|
|
375
|
-
data_source = DataBentoData(api_key=self.api_key)
|
|
376
|
-
|
|
377
|
-
# This should not raise an exception
|
|
378
|
-
result = data_source.get_historical_prices(
|
|
379
|
-
asset=self.test_asset,
|
|
380
|
-
length=2,
|
|
381
|
-
timestep="minute"
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
self.assertIsNotNone(result)
|
|
385
|
-
self.assertIsInstance(result, Bars)
|
|
386
|
-
self.assertEqual(len(result.df), 2)
|
|
387
|
-
|
|
388
|
-
@patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
|
|
389
|
-
@patch('lumibot.tools.databento_helper.get_price_data_from_databento')
|
|
390
|
-
def test_error_handling(self, mock_get_data):
|
|
391
|
-
"""Test error handling in get_historical_prices"""
|
|
392
|
-
# Setup: Mock to raise exception
|
|
393
|
-
mock_get_data.side_effect = Exception("Test error")
|
|
394
|
-
|
|
395
|
-
data_source = DataBentoData(api_key=self.api_key)
|
|
396
|
-
|
|
397
|
-
# This should not raise an exception but return None
|
|
398
|
-
result = data_source.get_historical_prices(
|
|
399
|
-
asset=self.test_asset,
|
|
400
|
-
length=1,
|
|
401
|
-
timestep="minute"
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
# Should return None on error
|
|
405
|
-
self.assertIsNone(result)
|
|
194
|
+
self.assertIsNotNone(bars)
|
|
195
|
+
self.assertEqual(bars.asset, self.cont_future_asset)
|
|
406
196
|
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
# Integration-style helpers (mocked)
|
|
199
|
+
# ------------------------------------------------------------------
|
|
407
200
|
def test_environment_dates_integration(self):
|
|
408
|
-
"""Test DataBento with environment file dates"""
|
|
409
201
|
from dotenv import load_dotenv
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
# Load environment variables from the strategy .env file
|
|
202
|
+
|
|
413
203
|
env_path = "/Users/robertgrzesik/Documents/Development/Strategy Library/Alligator Futures Bot Strategy/src/.env"
|
|
414
204
|
if os.path.exists(env_path):
|
|
415
205
|
load_dotenv(env_path)
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
data_source = DataBentoData(api_key=self.api_key)
|
|
430
|
-
|
|
431
|
-
# Test with MES continuous futures
|
|
432
|
-
mes_asset = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
433
|
-
|
|
434
|
-
# Mock the get_historical_prices method
|
|
435
|
-
mock_bars = Mock()
|
|
436
|
-
mock_bars.df = pd.DataFrame({
|
|
437
|
-
'open': [4500, 4505, 4510],
|
|
438
|
-
'high': [4510, 4515, 4520],
|
|
439
|
-
'low': [4495, 4500, 4505],
|
|
440
|
-
'close': [4505, 4510, 4515],
|
|
441
|
-
'volume': [1000, 1100, 1200]
|
|
442
|
-
}, index=pd.date_range(start=test_start_date, periods=3, freq='h'))
|
|
443
|
-
|
|
444
|
-
with patch.object(data_source, 'get_historical_prices', return_value=mock_bars):
|
|
445
|
-
bars = data_source.get_historical_prices(
|
|
446
|
-
asset=mes_asset,
|
|
447
|
-
length=60,
|
|
448
|
-
timestep="minute"
|
|
449
|
-
)
|
|
450
|
-
|
|
451
|
-
self.assertIsNotNone(bars)
|
|
452
|
-
self.assertIsNotNone(bars.df)
|
|
453
|
-
self.assertEqual(len(bars.df), 3)
|
|
454
|
-
|
|
206
|
+
|
|
207
|
+
with patch.object(DataBentoData, "get_historical_prices", return_value=self._bars(3)) as mock_get_hist:
|
|
208
|
+
data_source = DataBentoData(**self.datasource_kwargs)
|
|
209
|
+
bars = data_source.get_historical_prices(
|
|
210
|
+
asset=self.cont_future_asset,
|
|
211
|
+
length=60,
|
|
212
|
+
timestep="minute",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
mock_get_hist.assert_called_once()
|
|
216
|
+
self.assertIsNotNone(bars)
|
|
217
|
+
self.assertEqual(len(bars.df), 3)
|
|
218
|
+
|
|
455
219
|
def test_mes_strategy_logic_simulation(self):
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
sma_60 = df["close"].mean()
|
|
487
|
-
|
|
488
|
-
# Should have a clear trend in our test data
|
|
489
|
-
self.assertGreater(current_price, sma_60)
|
|
490
|
-
self.assertGreater(current_price, 4500) # Should be trending up
|
|
491
|
-
|
|
492
|
-
if __name__ == '__main__':
|
|
220
|
+
data_source = DataBentoData(**self.datasource_kwargs)
|
|
221
|
+
mock_bars = MagicMock()
|
|
222
|
+
mock_df = pd.DataFrame(
|
|
223
|
+
{
|
|
224
|
+
"open": [4500 + i for i in range(60)],
|
|
225
|
+
"high": [4510 + i for i in range(60)],
|
|
226
|
+
"low": [4490 + i for i in range(60)],
|
|
227
|
+
"close": [4505 + i for i in range(60)],
|
|
228
|
+
"volume": [1_000 + i * 10 for i in range(60)],
|
|
229
|
+
},
|
|
230
|
+
index=pd.date_range(start=datetime(2024, 6, 10, 8, 0), periods=60, freq="min"),
|
|
231
|
+
)
|
|
232
|
+
mock_bars.df = mock_df
|
|
233
|
+
|
|
234
|
+
with patch.object(data_source, "get_historical_prices", return_value=mock_bars):
|
|
235
|
+
bars = data_source.get_historical_prices(
|
|
236
|
+
asset=self.cont_future_asset,
|
|
237
|
+
length=60,
|
|
238
|
+
timestep="minute",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
self.assertEqual(len(bars.df), 60)
|
|
242
|
+
current_price = bars.df["close"].iloc[-1]
|
|
243
|
+
sma_60 = bars.df["close"].mean()
|
|
244
|
+
|
|
245
|
+
self.assertGreater(current_price, sma_60)
|
|
246
|
+
self.assertGreater(current_price, 4500)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
493
250
|
unittest.main()
|