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.
- lumibot/backtesting/__init__.py +19 -5
- lumibot/backtesting/backtesting_broker.py +98 -18
- lumibot/backtesting/databento_backtesting.py +5 -686
- lumibot/backtesting/databento_backtesting_pandas.py +738 -0
- lumibot/backtesting/databento_backtesting_polars.py +860 -546
- lumibot/backtesting/fix_debug.py +37 -0
- lumibot/backtesting/thetadata_backtesting.py +9 -355
- lumibot/backtesting/thetadata_backtesting_pandas.py +1167 -0
- lumibot/brokers/alpaca.py +8 -1
- lumibot/brokers/schwab.py +12 -2
- lumibot/credentials.py +13 -0
- lumibot/data_sources/__init__.py +5 -8
- lumibot/data_sources/data_source.py +6 -2
- lumibot/data_sources/data_source_backtesting.py +30 -0
- lumibot/data_sources/databento_data.py +5 -390
- lumibot/data_sources/databento_data_pandas.py +440 -0
- lumibot/data_sources/databento_data_polars.py +15 -9
- lumibot/data_sources/pandas_data.py +30 -17
- lumibot/data_sources/polars_data.py +986 -0
- lumibot/data_sources/polars_mixin.py +472 -96
- lumibot/data_sources/polygon_data_polars.py +5 -0
- lumibot/data_sources/yahoo_data.py +9 -2
- lumibot/data_sources/yahoo_data_polars.py +5 -0
- lumibot/entities/__init__.py +15 -0
- lumibot/entities/asset.py +5 -28
- lumibot/entities/bars.py +89 -20
- lumibot/entities/data.py +29 -6
- lumibot/entities/data_polars.py +668 -0
- lumibot/entities/position.py +38 -4
- lumibot/strategies/_strategy.py +2 -1
- lumibot/strategies/strategy.py +61 -49
- lumibot/tools/backtest_cache.py +284 -0
- lumibot/tools/databento_helper.py +35 -35
- lumibot/tools/databento_helper_polars.py +738 -775
- lumibot/tools/futures_roll.py +251 -0
- lumibot/tools/indicators.py +135 -104
- lumibot/tools/polars_utils.py +142 -0
- lumibot/tools/thetadata_helper.py +1068 -134
- {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/METADATA +9 -1
- {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/RECORD +71 -147
- tests/backtest/test_databento.py +37 -6
- tests/backtest/test_databento_comprehensive_trading.py +8 -4
- tests/backtest/test_databento_parity.py +4 -2
- tests/backtest/test_debug_avg_fill_price.py +1 -1
- tests/backtest/test_example_strategies.py +11 -1
- tests/backtest/test_futures_edge_cases.py +3 -3
- tests/backtest/test_futures_single_trade.py +2 -2
- tests/backtest/test_futures_ultra_simple.py +2 -2
- tests/backtest/test_polars_lru_eviction.py +470 -0
- tests/backtest/test_yahoo.py +42 -0
- tests/test_asset.py +4 -4
- tests/test_backtest_cache_manager.py +149 -0
- tests/test_backtesting_data_source_env.py +6 -0
- tests/test_continuous_futures_resolution.py +60 -48
- tests/test_data_polars_parity.py +160 -0
- tests/test_databento_asset_validation.py +23 -5
- tests/test_databento_backtesting.py +1 -1
- tests/test_databento_backtesting_polars.py +312 -192
- tests/test_databento_data.py +220 -463
- tests/test_databento_live.py +10 -10
- tests/test_futures_roll.py +38 -0
- tests/test_indicator_subplots.py +101 -0
- tests/test_market_infinite_loop_bug.py +77 -3
- tests/test_polars_resample.py +67 -0
- tests/test_polygon_helper.py +46 -0
- tests/test_thetadata_backwards_compat.py +97 -0
- tests/test_thetadata_helper.py +222 -23
- tests/test_thetadata_pandas_verification.py +186 -0
- lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/__pycache__/constants.cpython-312.pyc +0 -0
- lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
- lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
- lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
- lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
- {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/WHEEL +0 -0
- {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/licenses/LICENSE +0 -0
- {lumibot-4.1.3.dist-info → lumibot-4.2.1.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]
|
|
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
|
lumibot/data_sources/__init__.py
CHANGED
|
@@ -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 .
|
|
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,
|
|
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
|
-
|
|
2
|
-
from decimal import Decimal
|
|
3
|
-
from typing import Union, Optional
|
|
1
|
+
"""Canonical DataBento data source aliasing the Polars implementation."""
|
|
4
2
|
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from
|
|
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
|
-
|
|
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"]
|