lumibot 4.1.3__py3-none-any.whl → 4.2.1__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (163) 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 +1167 -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 +2 -1
  31. lumibot/strategies/strategy.py +61 -49
  32. lumibot/tools/backtest_cache.py +284 -0
  33. lumibot/tools/databento_helper.py +35 -35
  34. lumibot/tools/databento_helper_polars.py +738 -775
  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.3.dist-info → lumibot-4.2.1.dist-info}/METADATA +9 -1
  40. {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/RECORD +71 -147
  41. tests/backtest/test_databento.py +37 -6
  42. tests/backtest/test_databento_comprehensive_trading.py +8 -4
  43. tests/backtest/test_databento_parity.py +4 -2
  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 +3 -3
  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 +6 -0
  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_live.py +10 -10
  61. tests/test_futures_roll.py +38 -0
  62. tests/test_indicator_subplots.py +101 -0
  63. tests/test_market_infinite_loop_bug.py +77 -3
  64. tests/test_polars_resample.py +67 -0
  65. tests/test_polygon_helper.py +46 -0
  66. tests/test_thetadata_backwards_compat.py +97 -0
  67. tests/test_thetadata_helper.py +222 -23
  68. tests/test_thetadata_pandas_verification.py +186 -0
  69. lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
  70. lumibot/__pycache__/constants.cpython-312.pyc +0 -0
  71. lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
  72. lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
  73. lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
  74. lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
  75. lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
  76. lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
  77. lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
  78. lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
  79. lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
  80. lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
  81. lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
  82. lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
  83. lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
  84. lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
  85. lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
  86. lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
  87. lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
  88. lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
  89. lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
  90. lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
  91. lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
  92. lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
  93. lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
  94. lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
  95. lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
  96. lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
  97. lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
  98. lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
  99. lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
  100. lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
  101. lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
  102. lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
  103. lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
  104. lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
  105. lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
  106. lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
  107. lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
  108. lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
  109. lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
  110. lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
  111. lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
  112. lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
  113. lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
  114. lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
  115. lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
  116. lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
  117. lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
  118. lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
  119. lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
  120. lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
  121. lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
  122. lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
  123. lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
  124. lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
  125. lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
  126. lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
  127. lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
  128. lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  129. lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
  130. lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  131. lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
  132. lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
  133. lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
  134. lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  135. lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
  136. lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
  137. lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
  138. lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
  139. lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
  140. lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
  141. lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
  142. lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
  143. lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
  144. lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
  145. lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
  146. lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
  147. lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
  148. lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
  149. lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
  150. lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
  151. lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
  152. lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
  153. lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
  154. lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
  155. lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
  156. lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
  157. lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
  158. lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
  159. lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
  160. lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
  161. {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/WHEEL +0 -0
  162. {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/licenses/LICENSE +0 -0
  163. {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,440 @@
1
+ from datetime import datetime, timedelta
2
+ from decimal import Decimal
3
+ from typing import Union, Optional
4
+
5
+ import pandas as pd
6
+ import polars as pl
7
+
8
+ from .data_source import DataSource
9
+ from lumibot.entities import Asset, Bars, Quote
10
+ from lumibot.tools import databento_helper, databento_helper_polars
11
+ from lumibot.tools.lumibot_logger import get_logger
12
+
13
+ try:
14
+ from .databento_data_polars import DataBentoDataPolars
15
+ except Exception: # pragma: no cover - optional dependency path
16
+ DataBentoDataPolars = None
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class DataBentoDataPandas(DataSource):
22
+ """
23
+ DataBento data source for historical market data
24
+
25
+ This data source provides access to DataBento's institutional-grade market data,
26
+ with a focus on futures data and support for multiple asset types.
27
+ """
28
+
29
+ SOURCE = "DATABENTO"
30
+ MIN_TIMESTEP = "minute"
31
+ TIMESTEP_MAPPING = [
32
+ {"timestep": "minute", "representations": ["1m", "minute", "1 minute"]},
33
+ {"timestep": "hour", "representations": ["1h", "hour", "1 hour"]},
34
+ {"timestep": "day", "representations": ["1d", "day", "1 day"]},
35
+ ]
36
+
37
+ def __init__(
38
+ self,
39
+ api_key: str,
40
+ timeout: int = 30,
41
+ max_retries: int = 3,
42
+ **kwargs
43
+ ):
44
+ """
45
+ Initialize DataBento data source
46
+
47
+ Parameters
48
+ ----------
49
+ api_key : str
50
+ DataBento API key
51
+ timeout : int, optional
52
+ API request timeout in seconds, default 30
53
+ max_retries : int, optional
54
+ Maximum number of API retry attempts, default 3
55
+ **kwargs
56
+ Additional parameters passed to parent class
57
+ """
58
+ enable_live_stream = kwargs.pop("enable_live_stream", False)
59
+
60
+ # Initialize parent class
61
+ super().__init__(api_key=api_key, **kwargs)
62
+
63
+ self._api_key = api_key
64
+ self._timeout = timeout
65
+ self._max_retries = max_retries
66
+ self._data_store = {}
67
+ self._live_delegate = None
68
+ self._default_quote_asset = Asset("USD", "forex")
69
+
70
+ # For live trading, this is a live data source
71
+ self.is_backtesting_mode = False
72
+ self.enable_live_stream = enable_live_stream
73
+
74
+ # Verify DataBento availability
75
+ if not databento_helper.DATABENTO_AVAILABLE:
76
+ logger.error("DataBento package not available. Please install with: pip install databento")
77
+ raise ImportError("DataBento package not available")
78
+
79
+ def get_historical_prices(
80
+ self,
81
+ asset: Asset,
82
+ length: int,
83
+ timestep: str = "minute",
84
+ timeshift: timedelta = None,
85
+ quote: Asset = None,
86
+ exchange: str = None,
87
+ include_after_hours: bool = True,
88
+ return_polars: bool = False
89
+ ) -> Bars:
90
+ """
91
+ Get historical price data for an asset
92
+
93
+ Parameters
94
+ ----------
95
+ asset : Asset
96
+ The asset to get historical prices for
97
+ length : int
98
+ Number of bars to retrieve
99
+ timestep : str, optional
100
+ Timestep for the data ('minute', 'hour', 'day'), default 'minute'
101
+ timeshift : timedelta, optional
102
+ Time shift to apply to the data retrieval
103
+ quote : Asset, optional
104
+ Quote asset (not used for DataBento)
105
+ exchange : str, optional
106
+ Exchange/venue filter
107
+ include_after_hours : bool, optional
108
+ Whether to include after-hours data, default True
109
+
110
+ Returns
111
+ -------
112
+ Bars
113
+ Historical price data as Bars object
114
+ """
115
+ logger.debug(f"Getting historical prices for {asset.symbol}, length={length}, timestep={timestep}")
116
+
117
+ # Validate asset type - DataBento primarily supports futures
118
+ supported_asset_types = [Asset.AssetType.FUTURE, Asset.AssetType.CONT_FUTURE]
119
+ if asset.asset_type not in supported_asset_types:
120
+ error_msg = (
121
+ "DataBento data source only supports futures assets. "
122
+ f"Received asset type '{asset.asset_type}' for symbol '{asset.symbol}'. "
123
+ f"Supported types: {[t.value for t in supported_asset_types]}"
124
+ )
125
+ logger.error(error_msg)
126
+ return None
127
+
128
+ # Additional logging for debugging
129
+ logger.debug(f"DataBento request - Asset: {asset.symbol}, Type: {asset.asset_type}, Length: {length}, Timestep: {timestep}")
130
+ logger.debug(f"DataBento live trading mode: Requesting data for futures asset {asset.symbol}")
131
+
132
+ # Calculate the date range for data retrieval
133
+ # Use timezone-naive datetime for consistency
134
+ current_dt = datetime.now()
135
+ if current_dt.tzinfo is not None:
136
+ current_dt = current_dt.replace(tzinfo=None)
137
+
138
+ logger.debug(f"Using current datetime for live trading: {current_dt}")
139
+
140
+ # Apply timeshift if specified
141
+ if timeshift:
142
+ current_dt = current_dt - timeshift
143
+
144
+ # Calculate start date based on length and timestep
145
+ if timestep == "day":
146
+ buffer_days = max(10, length // 2) # Buffer for live trading
147
+ start_dt = current_dt - timedelta(days=length + buffer_days)
148
+ # For live trading, end should be current time (no future data available)
149
+ end_dt = current_dt
150
+ elif timestep == "hour":
151
+ buffer_hours = max(24, length // 2) # Buffer for live trading
152
+ start_dt = current_dt - timedelta(hours=length + buffer_hours)
153
+ # For live trading, end should be current time (no future data available)
154
+ end_dt = current_dt
155
+ else: # minute or other
156
+ buffer_minutes = max(1440, length) # Buffer for live trading
157
+ start_dt = current_dt - timedelta(minutes=length + buffer_minutes)
158
+ # For live trading, end should be current time (no future data available)
159
+ end_dt = current_dt
160
+
161
+ # Ensure both dates are timezone-naive for consistency
162
+ if start_dt.tzinfo is not None:
163
+ start_dt = start_dt.replace(tzinfo=None)
164
+ if end_dt.tzinfo is not None:
165
+ end_dt = end_dt.replace(tzinfo=None)
166
+
167
+ # Ensure we always have a valid date range (start < end)
168
+ if start_dt >= end_dt:
169
+ # If dates are equal or start is after end, adjust end date
170
+ if timestep == "day":
171
+ end_dt = start_dt + timedelta(days=max(1, length))
172
+ elif timestep == "hour":
173
+ end_dt = start_dt + timedelta(hours=max(1, length))
174
+ else: # minute or other
175
+ end_dt = start_dt + timedelta(minutes=max(1, length))
176
+
177
+ # Final safety check: ensure end is always after start
178
+ if start_dt >= end_dt:
179
+ logger.error(f"Invalid date range after adjustment: start={start_dt}, end={end_dt}")
180
+ if timestep == "day":
181
+ end_dt = start_dt + timedelta(days=1)
182
+ elif timestep == "hour":
183
+ end_dt = start_dt + timedelta(hours=1)
184
+ else:
185
+ end_dt = start_dt + timedelta(minutes=1)
186
+
187
+ # Get data from DataBento
188
+ logger.debug(f"Requesting DataBento data for asset: {asset} (type: {asset.asset_type})")
189
+ logger.debug(f"Date range: {start_dt} to {end_dt}")
190
+
191
+ try:
192
+ df = databento_helper_polars.get_price_data_from_databento_polars(
193
+ api_key=self._api_key,
194
+ asset=asset,
195
+ start=start_dt,
196
+ end=end_dt,
197
+ timestep=timestep,
198
+ venue=exchange
199
+ )
200
+ except Exception as e:
201
+ logger.error(f"Error getting data from DataBento for {asset.symbol}: {e}")
202
+ raise
203
+
204
+ if df is None:
205
+ logger.error(f"No data returned from DataBento for {asset.symbol}. This could be due to:")
206
+ logger.error("1. Incorrect symbol format")
207
+ logger.error("2. Wrong dataset selection")
208
+ logger.error("3. Data not available for the requested time range")
209
+ logger.error("4. API authentication issues")
210
+ return None
211
+
212
+ if not isinstance(df, pd.DataFrame):
213
+ df = df.to_pandas()
214
+ if "datetime" in df.columns:
215
+ df = df.set_index(pd.to_datetime(df["datetime"], utc=True))
216
+ df.index.name = "datetime"
217
+
218
+ if df.empty:
219
+ logger.error(f"No data returned from DataBento for {asset.symbol}. This could be due to:")
220
+ logger.error("1. Incorrect symbol format")
221
+ logger.error("2. Wrong dataset selection")
222
+ logger.error("3. Data not available for the requested time range")
223
+ logger.error("4. API authentication issues")
224
+ return None
225
+
226
+ # Filter data to the current time (for live trading)
227
+ # Handle timezone consistency for comparison
228
+ if hasattr(df.index, 'tz') and df.index.tz is not None:
229
+ # DataFrame has timezone-aware index, convert current_dt to match
230
+ if current_dt.tzinfo is None:
231
+ import pytz
232
+ current_dt = current_dt.replace(tzinfo=pytz.UTC)
233
+ else:
234
+ # DataFrame has timezone-naive index, ensure current_dt is also naive
235
+ if current_dt.tzinfo is not None:
236
+ current_dt = current_dt.replace(tzinfo=None)
237
+
238
+ df_filtered = df[df.index <= current_dt]
239
+ if df_filtered.empty:
240
+ df_filtered = df
241
+
242
+ # Take the last 'length' bars
243
+ df_result = df_filtered.tail(length)
244
+
245
+ if df_result.empty:
246
+ logger.warning(f"No data available for {asset.symbol} up to {current_dt}")
247
+ return None
248
+
249
+ # Create and return Bars object
250
+ bars = Bars(
251
+ df=df_result,
252
+ source=self.SOURCE,
253
+ asset=asset,
254
+ quote=quote,
255
+ return_polars=return_polars
256
+ )
257
+ quote_asset = quote if quote is not None else self._default_quote_asset
258
+ self._data_store[(asset, quote_asset)] = bars
259
+
260
+ logger.debug(f"Retrieved {len(df_result)} bars for {asset.symbol}")
261
+ return bars
262
+
263
+ def get_last_price(
264
+ self,
265
+ asset: Asset,
266
+ quote: Asset = None,
267
+ exchange: str = None
268
+ ) -> Union[float, Decimal, None]:
269
+ """
270
+ Get the last known price for an asset
271
+
272
+ Parameters
273
+ ----------
274
+ asset : Asset
275
+ The asset to get the last price for
276
+ quote : Asset, optional
277
+ Quote asset (not used for DataBento)
278
+ exchange : str, optional
279
+ Exchange/venue filter
280
+
281
+ Returns
282
+ -------
283
+ float, Decimal, or None
284
+ Last known price of the asset
285
+ """
286
+ logger.debug(f"Getting last price for {asset.symbol}")
287
+
288
+ # Prefer live delegate when available
289
+ delegate = self._ensure_live_delegate()
290
+ if delegate:
291
+ price = delegate.get_last_price(asset, quote=quote, exchange=exchange)
292
+ if price is not None:
293
+ return price
294
+
295
+ quote_asset = quote if quote is not None else self._default_quote_asset
296
+ cached_bars = self._data_store.get((asset, quote_asset))
297
+ if cached_bars is None:
298
+ try:
299
+ self.get_historical_prices(asset, length=1, timestep=self.MIN_TIMESTEP, quote=quote, return_polars=False)
300
+ except Exception:
301
+ pass
302
+ cached_bars = self._data_store.get((asset, quote_asset))
303
+ if cached_bars is not None:
304
+ df = cached_bars.df if hasattr(cached_bars, "df") else None
305
+ if df is not None and not df.empty and "close" in df.columns:
306
+ return float(df["close"].iloc[-1])
307
+
308
+ try:
309
+ last_price = databento_helper.get_last_price_from_databento(
310
+ api_key=self._api_key,
311
+ asset=asset,
312
+ venue=exchange
313
+ )
314
+
315
+ if last_price is not None:
316
+ logger.debug(f"Last price for {asset.symbol}: {last_price}")
317
+ return last_price
318
+ else:
319
+ logger.warning(f"No last price available for {asset.symbol}")
320
+ return None
321
+
322
+ except Exception as e:
323
+ logger.error(f"Error getting last price for {asset.symbol}: {e}")
324
+ return None
325
+
326
+ def get_chains(self, asset: Asset, quote: Asset = None) -> dict:
327
+ """
328
+ Get option chains for an asset
329
+
330
+ Note: DataBento primarily focuses on market data rather than options chains.
331
+ This method returns an empty dict as DataBento doesn't provide options chain data.
332
+
333
+ Parameters
334
+ ----------
335
+ asset : Asset
336
+ The asset to get option chains for
337
+ quote : Asset, optional
338
+ Quote asset
339
+
340
+ Returns
341
+ -------
342
+ dict
343
+ Empty dictionary as DataBento doesn't provide options chains
344
+ """
345
+ logger.warning("DataBento does not provide options chain data")
346
+ return {}
347
+
348
+ def get_quote(self, asset: Asset, quote: Asset = None) -> Union[float, Decimal, None]:
349
+ """
350
+ Get current quote for an asset
351
+
352
+ For DataBento, this returns the last known price since real-time quotes
353
+ may not be available for all assets.
354
+
355
+ Parameters
356
+ ----------
357
+ asset : Asset
358
+ The asset to get the quote for
359
+ quote : Asset, optional
360
+ Quote asset (not used for DataBento)
361
+
362
+ Returns
363
+ -------
364
+ float, Decimal, or None
365
+ Current quote/last price of the asset
366
+ """
367
+ delegate = self._ensure_live_delegate()
368
+ if delegate:
369
+ quote_obj = delegate.get_quote(asset, quote=quote, exchange=None)
370
+ if quote_obj:
371
+ return quote_obj
372
+
373
+ price = self.get_last_price(asset, quote=quote)
374
+ return Quote(asset=asset, price=price, bid=price, ask=price)
375
+
376
+ def _ensure_live_delegate(self) -> Optional['DataBentoDataPolars']:
377
+ if not self.enable_live_stream:
378
+ return None
379
+ if DataBentoDataPolars is None or self.is_backtesting_mode:
380
+ return None
381
+
382
+ if self._live_delegate is None:
383
+ try:
384
+ self._live_delegate = DataBentoDataPolars(
385
+ api_key=self._api_key,
386
+ has_paid_subscription=True,
387
+ enable_cache=False,
388
+ cache_duration_minutes=0,
389
+ enable_live_stream=True,
390
+ )
391
+ except Exception as e:
392
+ logger.error(f"Failed to initialize live DataBento delegate: {e}")
393
+ self._live_delegate = None
394
+
395
+ return self._live_delegate
396
+
397
+ def _parse_source_symbol_bars(self, response, asset, quote=None, return_polars: bool = False):
398
+ """
399
+ Parse source data for a single asset into Bars format
400
+
401
+ Parameters
402
+ ----------
403
+ response : pd.DataFrame
404
+ Raw data from DataBento API
405
+ asset : Asset
406
+ The asset the data is for
407
+ quote : Asset, optional
408
+ Quote asset (not used for DataBento)
409
+ return_polars : bool, optional
410
+ Whether to return a Polars DataFrame instead of pandas, default False
411
+
412
+ Returns
413
+ -------
414
+ Bars or None
415
+ Parsed bars data or None if parsing fails
416
+ """
417
+ try:
418
+ if response is None or response.empty:
419
+ return None
420
+
421
+ # Check if required columns exist
422
+ required_columns = ['open', 'high', 'low', 'close', 'volume']
423
+ if not all(col in response.columns for col in required_columns):
424
+ logger.warning(f"Missing required columns in DataBento data for {asset.symbol}")
425
+ return None
426
+
427
+ # Create Bars object
428
+ bars = Bars(
429
+ df=response,
430
+ source=self.SOURCE,
431
+ asset=asset,
432
+ quote=quote,
433
+ return_polars=return_polars
434
+ )
435
+
436
+ return bars
437
+
438
+ except Exception as e:
439
+ logger.error(f"Error parsing DataBento data for {asset.symbol}: {e}")
440
+ return None
@@ -16,10 +16,13 @@ import queue
16
16
  from collections import defaultdict
17
17
 
18
18
  import polars as pl
19
- import databento as db
19
+ try:
20
+ import databento as db
21
+ except ImportError: # pragma: no cover - optional dependency
22
+ db = None
20
23
 
21
- from lumibot.data_sources import DataSource
22
- from lumibot.data_sources.polars_mixin import PolarsMixin
24
+ from .data_source import DataSource
25
+ from .polars_mixin import PolarsMixin
23
26
  from lumibot.entities import Asset, Bars, Quote
24
27
  from lumibot.tools import databento_helper_polars
25
28
  from lumibot.tools.databento_helper_polars import _ensure_polars_datetime_timezone as _ensure_polars_tz
@@ -31,7 +34,7 @@ logger = get_logger(__name__)
31
34
  class DataBentoDataPolars(PolarsMixin, DataSource):
32
35
  """
33
36
  DataBento data source optimized with Polars and proper Live API usage.
34
-
37
+
35
38
  Uses Live API for real-time trade streaming to achieve <1 minute lag.
36
39
  Falls back to Historical API for older data.
37
40
  """
@@ -57,6 +60,9 @@ class DataBentoDataPolars(PolarsMixin, DataSource):
57
60
  """Initialize DataBento data source with Live API support"""
58
61
  super().__init__(api_key=api_key, has_paid_subscription=has_paid_subscription)
59
62
 
63
+ if db is None:
64
+ raise ImportError("DataBento package not available. Please install with: pip install databento")
65
+
60
66
  # Core configuration
61
67
  self._api_key = api_key
62
68
  self.has_paid_subscription = has_paid_subscription
@@ -212,7 +218,7 @@ class DataBentoDataPolars(PolarsMixin, DataSource):
212
218
  logger.warning(f"[DATABENTO][PRODUCER] Queue full, dropping record")
213
219
 
214
220
  # Clean exit
215
- logger.info(f"[DATABENTO][PRODUCER] {symbol} stopped after {record_count} records")
221
+ logger.debug(f"[DATABENTO][PRODUCER] {symbol} stopped after {record_count} records")
216
222
  break # Successful completion
217
223
 
218
224
  except Exception as e:
@@ -221,7 +227,7 @@ class DataBentoDataPolars(PolarsMixin, DataSource):
221
227
 
222
228
  if reconnect_attempts < max_reconnect_attempts:
223
229
  sleep_time = backoff_seconds * (2 ** reconnect_attempts)
224
- logger.info(f"[DATABENTO][PRODUCER] Reconnecting {symbol} in {sleep_time}s (attempt {reconnect_attempts})")
230
+ logger.debug(f"[DATABENTO][PRODUCER] Reconnecting {symbol} in {sleep_time}s (attempt {reconnect_attempts})")
225
231
  time.sleep(sleep_time)
226
232
 
227
233
  # Update start time for reconnection to avoid duplicate data
@@ -230,7 +236,7 @@ class DataBentoDataPolars(PolarsMixin, DataSource):
230
236
  ts_ns = self._last_ts_event[symbol]
231
237
  if ts_ns > 0:
232
238
  start_time = datetime.fromtimestamp(ts_ns / 1e9, tz=timezone.utc)
233
- logger.info(f"[DATABENTO][PRODUCER] Resuming from last event: {start_time.isoformat()}")
239
+ logger.debug(f"[DATABENTO][PRODUCER] Resuming from last event: {start_time.isoformat()}")
234
240
  else:
235
241
  logger.error(f"[DATABENTO][PRODUCER] {symbol} max reconnection attempts reached")
236
242
 
@@ -342,7 +348,7 @@ class DataBentoDataPolars(PolarsMixin, DataSource):
342
348
  except Exception as e:
343
349
  logger.error(f"[DATABENTO][CONSUMER] Error processing record: {e}")
344
350
 
345
- logger.info(f"[DATABENTO][CONSUMER] Stopped after {trade_count} trades")
351
+ logger.debug(f"[DATABENTO][CONSUMER] Stopped after {trade_count} trades")
346
352
 
347
353
  def _finalizer_loop(self):
348
354
  """Finalizer thread that marks old bars as complete"""
@@ -378,7 +384,7 @@ class DataBentoDataPolars(PolarsMixin, DataSource):
378
384
  except Exception as e:
379
385
  logger.error(f"[DATABENTO][FINALIZER] Error: {e}")
380
386
 
381
- logger.info("[DATABENTO][FINALIZER] Stopped")
387
+ logger.debug("[DATABENTO][FINALIZER] Stopped")
382
388
 
383
389
  def _aggregate_trade(self, symbol: str, price: float, size: float, trade_time: datetime):
384
390
  """Aggregate a trade into minute bars"""
@@ -304,17 +304,26 @@ class PandasData(DataSourceBacktesting):
304
304
  result[asset] = self.get_last_price(asset, quote=quote, exchange=exchange)
305
305
  return result
306
306
 
307
- def find_asset_in_data_store(self, asset, quote=None):
308
- if asset in self._data_store:
309
- return asset
310
- elif quote is not None:
311
- asset = (asset, quote)
312
- if asset in self._data_store:
313
- return asset
314
- elif isinstance(asset, Asset) and asset.asset_type in ["option", "future", "stock", "index"]:
315
- asset = (asset, Asset("USD", "forex"))
316
- if asset in self._data_store:
317
- return asset
307
+ def find_asset_in_data_store(self, asset, quote=None, timestep=None):
308
+ candidates = []
309
+
310
+ if timestep is not None:
311
+ base_quote = quote if quote is not None else Asset("USD", "forex")
312
+ candidates.append((asset, base_quote, timestep))
313
+ if quote is not None:
314
+ candidates.append((asset, Asset("USD", "forex"), timestep))
315
+
316
+ if quote is not None:
317
+ candidates.append((asset, quote))
318
+
319
+ if isinstance(asset, Asset) and asset.asset_type in ["option", "future", "stock", "index"]:
320
+ candidates.append((asset, Asset("USD", "forex")))
321
+
322
+ candidates.append(asset)
323
+
324
+ for key in candidates:
325
+ if key in self._data_store:
326
+ return key
318
327
  return None
319
328
 
320
329
  def _pull_source_symbol_bars(
@@ -336,7 +345,7 @@ class PandasData(DataSourceBacktesting):
336
345
  if not timeshift:
337
346
  timeshift = 0
338
347
 
339
- asset_to_find = self.find_asset_in_data_store(asset, quote)
348
+ asset_to_find = self.find_asset_in_data_store(asset, quote, timestep)
340
349
 
341
350
  if asset_to_find in self._data_store:
342
351
  data = self._data_store[asset_to_find]
@@ -369,7 +378,7 @@ class PandasData(DataSourceBacktesting):
369
378
  ):
370
379
  """Pull all bars for an asset"""
371
380
  timestep = timestep if timestep else self.MIN_TIMESTEP
372
- asset_to_find = self.find_asset_in_data_store(asset, quote)
381
+ asset_to_find = self.find_asset_in_data_store(asset, quote, timestep)
373
382
 
374
383
  if asset_to_find in self._data_store:
375
384
  data = self._data_store[asset_to_find]
@@ -412,13 +421,17 @@ class PandasData(DataSourceBacktesting):
412
421
 
413
422
  return result
414
423
 
415
- def _parse_source_symbol_bars(self, response, asset, quote=None, length=None):
416
- """parse broker response for a single asset"""
424
+ def _parse_source_symbol_bars(self, response, asset, quote=None, length=None, return_polars: bool = False):
425
+ """parse broker response for a single asset
426
+
427
+ CRITICAL: return_polars defaults to False for backwards compatibility.
428
+ PandasData always returns pandas-backed Bars for consistency.
429
+ """
417
430
  asset1 = asset
418
431
  asset2 = quote
419
432
  if isinstance(asset, tuple):
420
433
  asset1, asset2 = asset
421
- bars = Bars(response, self.SOURCE, asset1, quote=asset2, raw=response)
434
+ bars = Bars(response, self.SOURCE, asset1, quote=asset2, raw=response, return_polars=return_polars)
422
435
  return bars
423
436
 
424
437
  def get_yesterday_dividend(self, asset, quote=None):
@@ -541,5 +554,5 @@ class PandasData(DataSourceBacktesting):
541
554
  elif response is None:
542
555
  return None
543
556
 
544
- bars = self._parse_source_symbol_bars(response, asset, quote=quote, length=length)
557
+ bars = self._parse_source_symbol_bars(response, asset, quote=quote, length=length, return_polars=return_polars)
545
558
  return bars