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

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

Potentially problematic release.


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

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 +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 +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.0.dist-info}/METADATA +9 -1
  40. {lumibot-4.1.3.dist-info → lumibot-4.2.0.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.0.dist-info}/WHEEL +0 -0
  162. {lumibot-4.1.3.dist-info → lumibot-4.2.0.dist-info}/licenses/LICENSE +0 -0
  163. {lumibot-4.1.3.dist-info → lumibot-4.2.0.dist-info}/top_level.txt +0 -0
lumibot/brokers/alpaca.py CHANGED
@@ -8,7 +8,7 @@ from decimal import Decimal
8
8
 
9
9
  import pandas_market_calendars as mcal
10
10
  from alpaca.trading.client import TradingClient
11
- from alpaca.trading.enums import QueryOrderStatus
11
+ from alpaca.trading.enums import QueryOrderStatus, PositionSide
12
12
  from alpaca.trading.requests import GetOrdersRequest, ReplaceOrderRequest
13
13
  from alpaca.trading.stream import TradingStream
14
14
  from dateutil import tz
@@ -404,6 +404,13 @@ class Alpaca(Broker):
404
404
  avg_fill_price = None
405
405
 
406
406
  position = Position(strategy, asset, quantity, orders=orders, avg_fill_price=avg_fill_price)
407
+
408
+ position.pnl = float(broker_position.unrealized_pl) if broker_position.unrealized_pl else None
409
+ position.current_price = float(broker_position.current_price) if broker_position.current_price else None
410
+ position.side = Position.PositionSide.LONG if broker_position.side == PositionSide.LONG else Position.PositionSide.SHORT
411
+ position.market_value = float(broker_position.market_value) if broker_position.market_value else None
412
+
413
+
407
414
  return position
408
415
 
409
416
  def _pull_broker_position(self, asset):
lumibot/brokers/schwab.py CHANGED
@@ -87,7 +87,7 @@ class Schwab(Broker):
87
87
  # Initialize Schwab specific attributes
88
88
  self._subscribers = []
89
89
  # Use standard logging module's logger
90
- self.logger = get_logger(__name__)
90
+ # self.logger = get_logger(__name__)
91
91
  self.extended_trading_minutes = 0
92
92
  # self.schwab_authorization_error = False # Moved earlier
93
93
  self.client = None
@@ -526,6 +526,8 @@ class Schwab(Broker):
526
526
 
527
527
  # Extract position-specific details
528
528
  average_price = schwab_position.get('averagePrice', 0.0)
529
+ pnl = schwab_position.get('longOpenProfitLoss') or schwab_position.get('shortOpenProfitLoss') or None
530
+ market_value = schwab_position.get('marketValue', None)
529
531
 
530
532
  # Only create position object if we have a valid asset
531
533
  if asset is not None:
@@ -537,7 +539,12 @@ class Schwab(Broker):
537
539
 
538
540
  # If we already have this asset in our dict, update the quantity
539
541
  if key in pos_dict:
540
- pos_dict[key].quantity += net_quantity
542
+ existing_position = pos_dict[key]
543
+ existing_position.quantity += net_quantity
544
+ if pnl is not None:
545
+ existing_position.pnl += pnl
546
+ if market_value is not None:
547
+ existing_position.market_value += market_value
541
548
  else:
542
549
  # Create a new Position object
543
550
  pos_dict[key] = Position(
@@ -546,6 +553,9 @@ class Schwab(Broker):
546
553
  quantity=net_quantity,
547
554
  avg_fill_price=average_price,
548
555
  )
556
+
557
+ pos_dict[key].pnl = pnl
558
+ pos_dict[key].market_value = market_value
549
559
 
550
560
  # Log the number of positions found
551
561
  logger.debug(f"Pulled {len(pos_dict)} unique positions from Schwab")
lumibot/credentials.py CHANGED
@@ -182,6 +182,19 @@ DATABENTO_CONFIG = {
182
182
  "MAX_RETRIES": int(os.environ.get("DATABENTO_MAX_RETRIES", "3")),
183
183
  }
184
184
 
185
+ # Remote cache configuration (disabled by default)
186
+ CACHE_REMOTE_CONFIG = {
187
+ "backend": os.environ.get("LUMIBOT_CACHE_BACKEND", "local"),
188
+ "mode": os.environ.get("LUMIBOT_CACHE_MODE", "disabled"),
189
+ "s3_bucket": os.environ.get("LUMIBOT_CACHE_S3_BUCKET"),
190
+ "s3_prefix": os.environ.get("LUMIBOT_CACHE_S3_PREFIX", ""),
191
+ "s3_region": os.environ.get("LUMIBOT_CACHE_S3_REGION"),
192
+ "s3_access_key_id": os.environ.get("LUMIBOT_CACHE_S3_ACCESS_KEY_ID"),
193
+ "s3_secret_access_key": os.environ.get("LUMIBOT_CACHE_S3_SECRET_ACCESS_KEY"),
194
+ "s3_session_token": os.environ.get("LUMIBOT_CACHE_S3_SESSION_TOKEN"),
195
+ "s3_version": os.environ.get("LUMIBOT_CACHE_S3_VERSION", "v1"),
196
+ }
197
+
185
198
  # Alpaca Configuration
186
199
  ALPACA_CONFIG = {
187
200
  # Add ALPACA_API_KEY, ALPACA_API_SECRET, ALPACA_OAUTH_TOKEN, and ALPACA_IS_PAPER to your .env file or set them as secrets
@@ -6,20 +6,17 @@ from .data_source_backtesting import DataSourceBacktesting
6
6
  from .exceptions import NoDataFound, UnavailabeTimestep
7
7
  from .interactive_brokers_data import InteractiveBrokersData
8
8
  from .pandas_data import PandasData
9
+ from .polars_data import PolarsData
9
10
  from .tradier_data import TradierData
10
-
11
- from .yahoo_data_polars import YahooDataPolars as YahooData
12
-
13
-
14
- from .polygon_data_polars import PolygonDataPolars as PolygonDataBacktesting
15
-
16
11
  from .bitunix_data import BitunixData
17
12
  from .ccxt_backtesting_data import CcxtBacktestingData
18
13
  from .example_broker_data import ExampleBrokerData
19
14
  from .interactive_brokers_rest_data import InteractiveBrokersRESTData
20
15
  from .schwab_data import SchwabData
21
16
  from .tradovate_data import TradovateData
17
+ from .yahoo_data import YahooData
22
18
 
23
- from .databento_data_polars_live import DataBentoDataPolarsLive as DataBentoData
24
- from .databento_data_polars_backtesting import DataBentoDataPolarsBacktesting as DataBentoDataBacktesting
19
+ from .databento_data import DataBentoData, DataBentoDataPandas, DataBentoDataPolars
25
20
  from .projectx_data import ProjectXData
21
+
22
+ from ..backtesting.polygon_backtesting import PolygonDataBacktesting
@@ -154,12 +154,16 @@ class DataSource(ABC):
154
154
  include_after_hours : bool
155
155
  Whether to include after hours data.
156
156
  return_polars : bool
157
- If True, return Bars with Polars DataFrame for better performance. Default is False (returns pandas).
157
+ If True, returns Polars DataFrame via bars.df (2-3x faster for indicator calculations).
158
+ All data sources support this parameter. The Bars class automatically converts
159
+ pandas→polars when needed. Default is False for backward compatibility (returns pandas).
158
160
 
159
161
  Returns
160
162
  -------
161
163
  Bars
162
- The bars for the asset.
164
+ The bars for the asset. Access via bars.df which returns:
165
+ - Polars DataFrame if return_polars=True (recommended for performance)
166
+ - Pandas DataFrame if return_polars=False (default, backward compatible)
163
167
  """
164
168
  pass
165
169
 
@@ -77,6 +77,36 @@ class DataSourceBacktesting(DataSource, ABC):
77
77
  self._last_logging_time = None
78
78
  self._portfolio_value = None
79
79
 
80
+ @staticmethod
81
+ def estimate_requested_length(length=None, start_date=None, end_date=None, timestep="minute"):
82
+ """
83
+ Infer the number of rows required to satisfy a backtest data request.
84
+ """
85
+ if length is not None:
86
+ try:
87
+ return max(int(length), 1)
88
+ except (TypeError, ValueError):
89
+ pass
90
+
91
+ if start_date is None or end_date is None:
92
+ return 1
93
+
94
+ try:
95
+ td, unit = DataSource.convert_timestep_str_to_timedelta(timestep)
96
+ except Exception:
97
+ return 1
98
+
99
+ if end_date < start_date:
100
+ start_date, end_date = end_date, start_date
101
+
102
+ if unit == "day":
103
+ delta_days = (end_date.date() - start_date.date()).days
104
+ return max(delta_days + 1, 1)
105
+
106
+ interval_seconds = max(td.total_seconds(), 1)
107
+ total_seconds = max((end_date - start_date).total_seconds(), 0)
108
+ return max(int(total_seconds // interval_seconds) + 1, 1)
109
+
80
110
  def get_datetime(self, adjust_for_delay=False):
81
111
  """
82
112
  Get the current datetime of the backtest.
@@ -1,392 +1,7 @@
1
- from datetime import datetime, timedelta
2
- from decimal import Decimal
3
- from typing import Union, Optional
1
+ """Canonical DataBento data source aliasing the Polars implementation."""
4
2
 
5
- from lumibot.data_sources import DataSource
6
- from lumibot.entities import Asset, Bars, Quote
7
- from lumibot.tools import databento_helper
8
- from lumibot.tools.lumibot_logger import get_logger
3
+ from .databento_data_polars import DataBentoDataPolars as DataBentoData
4
+ from .databento_data_pandas import DataBentoDataPandas
5
+ from .databento_data_polars import DataBentoDataPolars
9
6
 
10
- try:
11
- from .databento_data_polars_live import DataBentoDataPolarsLive
12
- except Exception: # pragma: no cover - optional dependency path
13
- DataBentoDataPolarsLive = None
14
-
15
- logger = get_logger(__name__)
16
-
17
-
18
- class DataBentoData(DataSource):
19
- """
20
- DataBento data source for historical market data
21
-
22
- This data source provides access to DataBento's institutional-grade market data,
23
- with a focus on futures data and support for multiple asset types.
24
- """
25
-
26
- SOURCE = "DATABENTO"
27
- MIN_TIMESTEP = "minute"
28
- TIMESTEP_MAPPING = [
29
- {"timestep": "minute", "representations": ["1m", "minute", "1 minute"]},
30
- {"timestep": "hour", "representations": ["1h", "hour", "1 hour"]},
31
- {"timestep": "day", "representations": ["1d", "day", "1 day"]},
32
- ]
33
-
34
- def __init__(
35
- self,
36
- api_key: str,
37
- timeout: int = 30,
38
- max_retries: int = 3,
39
- **kwargs
40
- ):
41
- """
42
- Initialize DataBento data source
43
-
44
- Parameters
45
- ----------
46
- api_key : str
47
- DataBento API key
48
- timeout : int, optional
49
- API request timeout in seconds, default 30
50
- max_retries : int, optional
51
- Maximum number of API retry attempts, default 3
52
- **kwargs
53
- Additional parameters passed to parent class
54
- """
55
- # Initialize parent class
56
- super().__init__(api_key=api_key, **kwargs)
57
-
58
- self.name = "databento"
59
- self._api_key = api_key
60
- self._timeout = timeout
61
- self._max_retries = max_retries
62
- self._data_store = {}
63
- self._live_delegate = None
64
-
65
- # For live trading, this is a live data source
66
- self.is_backtesting_mode = False
67
-
68
- # Verify DataBento availability
69
- if not databento_helper.DATABENTO_AVAILABLE:
70
- logger.error("DataBento package not available. Please install with: pip install databento")
71
- raise ImportError("DataBento package not available")
72
-
73
- def get_historical_prices(
74
- self,
75
- asset: Asset,
76
- length: int,
77
- timestep: str = "minute",
78
- timeshift: timedelta = None,
79
- quote: Asset = None,
80
- exchange: str = None,
81
- include_after_hours: bool = True
82
- ) -> Bars:
83
- """
84
- Get historical price data for an asset
85
-
86
- Parameters
87
- ----------
88
- asset : Asset
89
- The asset to get historical prices for
90
- length : int
91
- Number of bars to retrieve
92
- timestep : str, optional
93
- Timestep for the data ('minute', 'hour', 'day'), default 'minute'
94
- timeshift : timedelta, optional
95
- Time shift to apply to the data retrieval
96
- quote : Asset, optional
97
- Quote asset (not used for DataBento)
98
- exchange : str, optional
99
- Exchange/venue filter
100
- include_after_hours : bool, optional
101
- Whether to include after-hours data, default True
102
-
103
- Returns
104
- -------
105
- Bars
106
- Historical price data as Bars object
107
- """
108
- logger.info(f"Getting historical prices for {asset.symbol}, length={length}, timestep={timestep}")
109
-
110
- # Validate asset type - DataBento primarily supports futures
111
- supported_asset_types = [Asset.AssetType.FUTURE, Asset.AssetType.CONT_FUTURE]
112
- if asset.asset_type not in supported_asset_types:
113
- error_msg = f"DataBento data source only supports futures assets. Received asset type '{asset.asset_type}' for symbol '{asset.symbol}'. Supported types: {[t.value for t in supported_asset_types]}"
114
- logger.error(error_msg)
115
- raise ValueError(error_msg)
116
-
117
- # Additional logging for debugging
118
- logger.info(f"DataBento request - Asset: {asset.symbol}, Type: {asset.asset_type}, Length: {length}, Timestep: {timestep}")
119
- logger.info(f"DataBento live trading mode: Requesting data for futures asset {asset.symbol}")
120
-
121
- # Calculate the date range for data retrieval
122
- # Use timezone-naive datetime for consistency
123
- current_dt = datetime.now()
124
- if current_dt.tzinfo is not None:
125
- current_dt = current_dt.replace(tzinfo=None)
126
-
127
- logger.info(f"Using current datetime for live trading: {current_dt}")
128
-
129
- # Apply timeshift if specified
130
- if timeshift:
131
- current_dt = current_dt - timeshift
132
-
133
- # Calculate start date based on length and timestep
134
- if timestep == "day":
135
- buffer_days = max(10, length // 2) # Buffer for live trading
136
- start_dt = current_dt - timedelta(days=length + buffer_days)
137
- # For live trading, end should be current time (no future data available)
138
- end_dt = current_dt
139
- elif timestep == "hour":
140
- buffer_hours = max(24, length // 2) # Buffer for live trading
141
- start_dt = current_dt - timedelta(hours=length + buffer_hours)
142
- # For live trading, end should be current time (no future data available)
143
- end_dt = current_dt
144
- else: # minute or other
145
- buffer_minutes = max(1440, length) # Buffer for live trading
146
- start_dt = current_dt - timedelta(minutes=length + buffer_minutes)
147
- # For live trading, end should be current time (no future data available)
148
- end_dt = current_dt
149
-
150
- # Ensure both dates are timezone-naive for consistency
151
- if start_dt.tzinfo is not None:
152
- start_dt = start_dt.replace(tzinfo=None)
153
- if end_dt.tzinfo is not None:
154
- end_dt = end_dt.replace(tzinfo=None)
155
-
156
- # Ensure we always have a valid date range (start < end)
157
- if start_dt >= end_dt:
158
- # If dates are equal or start is after end, adjust end date
159
- if timestep == "day":
160
- end_dt = start_dt + timedelta(days=max(1, length))
161
- elif timestep == "hour":
162
- end_dt = start_dt + timedelta(hours=max(1, length))
163
- else: # minute or other
164
- end_dt = start_dt + timedelta(minutes=max(1, length))
165
-
166
- # Final safety check: ensure end is always after start
167
- if start_dt >= end_dt:
168
- logger.error(f"Invalid date range after adjustment: start={start_dt}, end={end_dt}")
169
- if timestep == "day":
170
- end_dt = start_dt + timedelta(days=1)
171
- elif timestep == "hour":
172
- end_dt = start_dt + timedelta(hours=1)
173
- else:
174
- end_dt = start_dt + timedelta(minutes=1)
175
-
176
- # Get data from DataBento
177
- logger.info(f"Requesting DataBento data for asset: {asset} (type: {asset.asset_type})")
178
- logger.info(f"Date range: {start_dt} to {end_dt}")
179
-
180
- try:
181
- df = databento_helper.get_price_data_from_databento(
182
- api_key=self._api_key,
183
- asset=asset,
184
- start=start_dt,
185
- end=end_dt,
186
- timestep=timestep,
187
- venue=exchange
188
- )
189
- except Exception as e:
190
- logger.error(f"Error getting data from DataBento for {asset.symbol}: {e}")
191
- return None
192
-
193
- if df is None or df.empty:
194
- logger.error(f"No data returned from DataBento for {asset.symbol}. This could be due to:")
195
- logger.error("1. Incorrect symbol format")
196
- logger.error("2. Wrong dataset selection")
197
- logger.error("3. Data not available for the requested time range")
198
- logger.error("4. API authentication issues")
199
- return None
200
-
201
- # Filter data to the current time (for live trading)
202
- # Handle timezone consistency for comparison
203
- if hasattr(df.index, 'tz') and df.index.tz is not None:
204
- # DataFrame has timezone-aware index, convert current_dt to match
205
- if current_dt.tzinfo is None:
206
- import pytz
207
- current_dt = current_dt.replace(tzinfo=pytz.UTC)
208
- else:
209
- # DataFrame has timezone-naive index, ensure current_dt is also naive
210
- if current_dt.tzinfo is not None:
211
- current_dt = current_dt.replace(tzinfo=None)
212
-
213
- df_filtered = df[df.index <= current_dt]
214
-
215
- # Take the last 'length' bars
216
- df_result = df_filtered.tail(length)
217
-
218
- if df_result.empty:
219
- logger.warning(f"No data available for {asset.symbol} up to {current_dt}")
220
- return None
221
-
222
- # Create and return Bars object
223
- bars = Bars(
224
- df=df_result,
225
- source=self.SOURCE,
226
- asset=asset,
227
- quote=quote
228
- )
229
-
230
- logger.info(f"Retrieved {len(df_result)} bars for {asset.symbol}")
231
- return bars
232
-
233
- def get_last_price(
234
- self,
235
- asset: Asset,
236
- quote: Asset = None,
237
- exchange: str = None
238
- ) -> Union[float, Decimal, None]:
239
- """
240
- Get the last known price for an asset
241
-
242
- Parameters
243
- ----------
244
- asset : Asset
245
- The asset to get the last price for
246
- quote : Asset, optional
247
- Quote asset (not used for DataBento)
248
- exchange : str, optional
249
- Exchange/venue filter
250
-
251
- Returns
252
- -------
253
- float, Decimal, or None
254
- Last known price of the asset
255
- """
256
- logger.info(f"Getting last price for {asset.symbol}")
257
-
258
- # Prefer live delegate when available
259
- delegate = self._ensure_live_delegate()
260
- if delegate:
261
- price = delegate.get_last_price(asset, quote=quote, exchange=exchange)
262
- if price is not None:
263
- return price
264
-
265
- try:
266
- last_price = databento_helper.get_last_price_from_databento(
267
- api_key=self._api_key,
268
- asset=asset,
269
- venue=exchange
270
- )
271
-
272
- if last_price is not None:
273
- logger.info(f"Last price for {asset.symbol}: {last_price}")
274
- return last_price
275
- else:
276
- logger.warning(f"No last price available for {asset.symbol}")
277
- return None
278
-
279
- except Exception as e:
280
- logger.error(f"Error getting last price for {asset.symbol}: {e}")
281
- return None
282
-
283
- def get_chains(self, asset: Asset, quote: Asset = None) -> dict:
284
- """
285
- Get option chains for an asset
286
-
287
- Note: DataBento primarily focuses on market data rather than options chains.
288
- This method returns an empty dict as DataBento doesn't provide options chain data.
289
-
290
- Parameters
291
- ----------
292
- asset : Asset
293
- The asset to get option chains for
294
- quote : Asset, optional
295
- Quote asset
296
-
297
- Returns
298
- -------
299
- dict
300
- Empty dictionary as DataBento doesn't provide options chains
301
- """
302
- logger.warning("DataBento does not provide options chain data")
303
- return {}
304
-
305
- def get_quote(self, asset: Asset, quote: Asset = None) -> Union[float, Decimal, None]:
306
- """
307
- Get current quote for an asset
308
-
309
- For DataBento, this returns the last known price since real-time quotes
310
- may not be available for all assets.
311
-
312
- Parameters
313
- ----------
314
- asset : Asset
315
- The asset to get the quote for
316
- quote : Asset, optional
317
- Quote asset (not used for DataBento)
318
-
319
- Returns
320
- -------
321
- float, Decimal, or None
322
- Current quote/last price of the asset
323
- """
324
- delegate = self._ensure_live_delegate()
325
- if delegate:
326
- quote_obj = delegate.get_quote(asset, quote=quote, exchange=None)
327
- if quote_obj:
328
- return quote_obj
329
-
330
- price = self.get_last_price(asset, quote=quote)
331
- return Quote(asset=asset, price=price)
332
-
333
- def _ensure_live_delegate(self) -> Optional['DataBentoDataPolarsLive']:
334
- if DataBentoDataPolarsLive is None or self.is_backtesting_mode:
335
- return None
336
-
337
- if self._live_delegate is None:
338
- try:
339
- self._live_delegate = DataBentoDataPolarsLive(
340
- api_key=self._api_key,
341
- has_paid_subscription=True,
342
- enable_cache=False,
343
- cache_duration_minutes=0,
344
- enable_live_stream=True,
345
- )
346
- except Exception as e:
347
- logger.error(f"Failed to initialize live DataBento delegate: {e}")
348
- self._live_delegate = None
349
-
350
- return self._live_delegate
351
-
352
- def _parse_source_symbol_bars(self, response, asset, quote=None):
353
- """
354
- Parse source data for a single asset into Bars format
355
-
356
- Parameters
357
- ----------
358
- response : pd.DataFrame
359
- Raw data from DataBento API
360
- asset : Asset
361
- The asset the data is for
362
- quote : Asset, optional
363
- Quote asset (not used for DataBento)
364
-
365
- Returns
366
- -------
367
- Bars or None
368
- Parsed bars data or None if parsing fails
369
- """
370
- try:
371
- if response is None or response.empty:
372
- return None
373
-
374
- # Check if required columns exist
375
- required_columns = ['open', 'high', 'low', 'close', 'volume']
376
- if not all(col in response.columns for col in required_columns):
377
- logger.warning(f"Missing required columns in DataBento data for {asset.symbol}")
378
- return None
379
-
380
- # Create Bars object
381
- bars = Bars(
382
- df=response,
383
- source=self.SOURCE,
384
- asset=asset,
385
- quote=quote
386
- )
387
-
388
- return bars
389
-
390
- except Exception as e:
391
- logger.error(f"Error parsing DataBento data for {asset.symbol}: {e}")
392
- return None
7
+ __all__ = ["DataBentoData", "DataBentoDataPandas", "DataBentoDataPolars"]