lumibot 4.1.2__py3-none-any.whl → 4.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of lumibot might be problematic. Click here for more details.

Files changed (164) hide show
  1. lumibot/backtesting/__init__.py +19 -5
  2. lumibot/backtesting/backtesting_broker.py +98 -18
  3. lumibot/backtesting/databento_backtesting.py +5 -686
  4. lumibot/backtesting/databento_backtesting_pandas.py +738 -0
  5. lumibot/backtesting/databento_backtesting_polars.py +860 -546
  6. lumibot/backtesting/fix_debug.py +37 -0
  7. lumibot/backtesting/thetadata_backtesting.py +9 -355
  8. lumibot/backtesting/thetadata_backtesting_pandas.py +1178 -0
  9. lumibot/brokers/alpaca.py +8 -1
  10. lumibot/brokers/schwab.py +12 -2
  11. lumibot/credentials.py +13 -0
  12. lumibot/data_sources/__init__.py +5 -8
  13. lumibot/data_sources/data_source.py +6 -2
  14. lumibot/data_sources/data_source_backtesting.py +30 -0
  15. lumibot/data_sources/databento_data.py +5 -390
  16. lumibot/data_sources/databento_data_pandas.py +440 -0
  17. lumibot/data_sources/databento_data_polars.py +15 -9
  18. lumibot/data_sources/pandas_data.py +30 -17
  19. lumibot/data_sources/polars_data.py +986 -0
  20. lumibot/data_sources/polars_mixin.py +472 -96
  21. lumibot/data_sources/polygon_data_polars.py +5 -0
  22. lumibot/data_sources/yahoo_data.py +9 -2
  23. lumibot/data_sources/yahoo_data_polars.py +5 -0
  24. lumibot/entities/__init__.py +15 -0
  25. lumibot/entities/asset.py +5 -28
  26. lumibot/entities/bars.py +89 -20
  27. lumibot/entities/data.py +29 -6
  28. lumibot/entities/data_polars.py +668 -0
  29. lumibot/entities/position.py +38 -4
  30. lumibot/strategies/_strategy.py +31 -9
  31. lumibot/strategies/strategy.py +61 -49
  32. lumibot/tools/backtest_cache.py +284 -0
  33. lumibot/tools/databento_helper.py +65 -42
  34. lumibot/tools/databento_helper_polars.py +748 -778
  35. lumibot/tools/futures_roll.py +251 -0
  36. lumibot/tools/indicators.py +135 -104
  37. lumibot/tools/polars_utils.py +142 -0
  38. lumibot/tools/thetadata_helper.py +1068 -134
  39. {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/METADATA +9 -1
  40. {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/RECORD +72 -148
  41. tests/backtest/test_databento.py +37 -6
  42. tests/backtest/test_databento_comprehensive_trading.py +70 -87
  43. tests/backtest/test_databento_parity.py +31 -7
  44. tests/backtest/test_debug_avg_fill_price.py +1 -1
  45. tests/backtest/test_example_strategies.py +11 -1
  46. tests/backtest/test_futures_edge_cases.py +96 -63
  47. tests/backtest/test_futures_single_trade.py +2 -2
  48. tests/backtest/test_futures_ultra_simple.py +2 -2
  49. tests/backtest/test_polars_lru_eviction.py +470 -0
  50. tests/backtest/test_yahoo.py +42 -0
  51. tests/test_asset.py +4 -4
  52. tests/test_backtest_cache_manager.py +149 -0
  53. tests/test_backtesting_data_source_env.py +50 -10
  54. tests/test_continuous_futures_resolution.py +60 -48
  55. tests/test_data_polars_parity.py +160 -0
  56. tests/test_databento_asset_validation.py +23 -5
  57. tests/test_databento_backtesting.py +1 -1
  58. tests/test_databento_backtesting_polars.py +312 -192
  59. tests/test_databento_data.py +220 -463
  60. tests/test_databento_helper.py +6 -1
  61. tests/test_databento_live.py +10 -10
  62. tests/test_futures_roll.py +38 -0
  63. tests/test_indicator_subplots.py +101 -0
  64. tests/test_market_infinite_loop_bug.py +77 -3
  65. tests/test_polars_resample.py +67 -0
  66. tests/test_polygon_helper.py +46 -0
  67. tests/test_thetadata_backwards_compat.py +97 -0
  68. tests/test_thetadata_helper.py +222 -23
  69. tests/test_thetadata_pandas_verification.py +186 -0
  70. lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
  71. lumibot/__pycache__/constants.cpython-312.pyc +0 -0
  72. lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
  73. lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
  74. lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
  75. lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
  76. lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
  77. lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
  78. lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
  79. lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
  80. lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
  81. lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
  82. lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
  83. lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
  84. lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
  85. lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
  86. lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
  87. lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
  88. lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
  89. lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
  90. lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
  91. lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
  92. lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
  93. lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
  94. lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
  95. lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
  96. lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
  97. lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
  98. lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
  99. lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
  100. lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
  101. lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
  102. lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
  103. lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
  104. lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
  105. lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
  106. lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
  107. lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
  108. lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
  109. lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
  110. lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
  111. lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
  112. lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
  113. lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
  114. lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
  115. lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
  116. lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
  117. lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
  118. lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
  119. lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
  120. lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
  121. lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
  122. lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
  123. lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
  124. lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
  125. lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
  126. lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
  127. lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
  128. lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
  129. lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  130. lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
  131. lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  132. lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
  133. lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
  134. lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
  135. lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  136. lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
  137. lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
  138. lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
  139. lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
  140. lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
  141. lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
  142. lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
  143. lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
  144. lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
  145. lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
  146. lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
  147. lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
  148. lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
  149. lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
  150. lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
  151. lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
  152. lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
  153. lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
  154. lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
  155. lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
  156. lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
  157. lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
  158. lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
  159. lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
  160. lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
  161. lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
  162. {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/WHEEL +0 -0
  163. {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/licenses/LICENSE +0 -0
  164. {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/top_level.txt +0 -0
@@ -1,493 +1,250 @@
1
+ import os
1
2
  import unittest
2
- from unittest.mock import Mock, patch, MagicMock
3
- from datetime import datetime, timedelta
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.databento_data import DataBentoData
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
- """Test cases for DataBentoData data source"""
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
- self.start_date = datetime(2025, 1, 1)
17
- self.end_date = datetime(2025, 1, 31)
18
-
19
- self.test_asset = Asset(
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="future",
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
- @patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', False)
39
- def test_initialization_databento_unavailable(self):
40
- """Test initialization when DataBento is unavailable"""
41
- with self.assertRaises(ImportError):
42
- DataBentoData(
43
- api_key=self.api_key,
44
- datetime_start=self.start_date,
45
- datetime_end=self.end_date
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
- def test_initialization_default_dates(self):
49
- """Test initialization with default dates"""
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
- @patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
105
- @patch('lumibot.tools.databento_helper.get_price_data_from_databento')
106
- def test_get_historical_prices_no_data(self, mock_get_data):
107
- """Test historical price retrieval with no data"""
108
- mock_get_data.return_value = None
109
-
110
- data_source = DataBentoData(
111
- api_key=self.api_key,
112
- datetime_start=self.start_date,
113
- datetime_end=self.end_date
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
- @patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
162
- def test_get_chains(self):
163
- """Test options chains retrieval (should return empty dict)"""
164
- data_source = DataBentoData(
165
- api_key=self.api_key,
166
- datetime_start=self.start_date,
167
- datetime_end=self.end_date
168
- )
169
-
170
- result = data_source.get_chains(asset=self.test_asset)
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
- @patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
225
- def test_parse_source_bars_valid_data(self):
226
- """Test parsing of valid source data using inherited method"""
227
- # Create test DataFrame
228
- test_df = pd.DataFrame({
229
- 'open': [100.0, 101.0],
230
- 'high': [102.0, 103.0],
231
- 'low': [99.0, 100.0],
232
- 'close': [101.0, 102.0],
233
- 'volume': [1000, 1100]
234
- })
235
- test_df.index = pd.to_datetime([
236
- '2025-01-01 09:30:00',
237
- '2025-01-01 09:31:00'
238
- ])
239
-
240
- data_source = DataBentoData(api_key=self.api_key)
241
-
242
- # Test that _parse_source_bars is available from parent class
243
- result = data_source._parse_source_bars({self.test_asset: test_df})
244
-
245
- self.assertIsInstance(result, dict)
246
- self.assertIn(self.test_asset, result)
247
- self.assertIsInstance(result[self.test_asset], Bars)
248
- self.assertEqual(len(result[self.test_asset].df), 2)
249
-
250
- @patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
251
- def test_parse_source_bars_missing_columns(self):
252
- """Test parsing of data with missing columns using inherited method"""
253
- # Create test DataFrame missing required columns
254
- test_df = pd.DataFrame({
255
- 'open': [100.0, 101.0],
256
- 'high': [102.0, 103.0],
257
- # Missing 'low', 'close', 'volume'
258
- })
259
- test_df.index = pd.to_datetime([
260
- '2025-01-01 09:30:00',
261
- '2025-01-01 09:31:00'
262
- ])
263
-
264
- data_source = DataBentoData(api_key=self.api_key)
265
-
266
- # Test that _parse_source_bars handles missing columns
267
- result = data_source._parse_source_bars({self.test_asset: test_df})
268
-
269
- self.assertIsInstance(result, dict)
270
- self.assertIn(self.test_asset, result)
271
-
272
- @patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
273
- def test_parse_source_bars_empty_data(self):
274
- """Test parsing of empty data using inherited method"""
275
- test_df = pd.DataFrame()
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
- @patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True)
319
- @patch('lumibot.tools.databento_helper.get_price_data_from_databento')
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
- import os
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
- # Get dates from environment variables
418
- start_str = os.getenv("BACKTESTING_START", "2024-01-01")
419
- end_str = os.getenv("BACKTESTING_END", "2024-12-31")
420
-
421
- start_date = datetime.strptime(start_str, "%Y-%m-%d")
422
- end_date = datetime.strptime(end_str, "%Y-%m-%d")
423
-
424
- # Use a recent subset for testing (last 3 months)
425
- test_end_date = datetime(2024, 12, 31)
426
- test_start_date = datetime(2024, 10, 1)
427
-
428
- with patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True):
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
- """Test MES strategy logic with simulated data"""
457
- with patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', True):
458
- data_source = DataBentoData(api_key=self.api_key)
459
-
460
- mes_asset = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
461
-
462
- # Create mock data that simulates 60 minutes of MES futures
463
- mock_bars = Mock()
464
- mock_bars.df = pd.DataFrame({
465
- 'open': [4500 + i for i in range(60)],
466
- 'high': [4510 + i for i in range(60)],
467
- 'low': [4490 + i for i in range(60)],
468
- 'close': [4505 + i for i in range(60)],
469
- 'volume': [1000 + i*10 for i in range(60)]
470
- }, index=pd.date_range(start=datetime(2024, 6, 10, 8, 0), periods=60, freq='min'))
471
-
472
- with patch.object(data_source, 'get_historical_prices', return_value=mock_bars):
473
- bars = data_source.get_historical_prices(
474
- asset=mes_asset,
475
- length=60,
476
- timestep="minute"
477
- )
478
-
479
- self.assertIsNotNone(bars)
480
- self.assertIsNotNone(bars.df)
481
- self.assertEqual(len(bars.df), 60)
482
-
483
- # Test strategy logic
484
- df = bars.df
485
- current_price = df["close"].iloc[-1]
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()