lumibot 4.1.2__py3-none-any.whl → 4.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lumibot might be problematic. Click here for more details.
- lumibot/backtesting/__init__.py +19 -5
- lumibot/backtesting/backtesting_broker.py +98 -18
- lumibot/backtesting/databento_backtesting.py +5 -686
- lumibot/backtesting/databento_backtesting_pandas.py +738 -0
- lumibot/backtesting/databento_backtesting_polars.py +860 -546
- lumibot/backtesting/fix_debug.py +37 -0
- lumibot/backtesting/thetadata_backtesting.py +9 -355
- lumibot/backtesting/thetadata_backtesting_pandas.py +1178 -0
- lumibot/brokers/alpaca.py +8 -1
- lumibot/brokers/schwab.py +12 -2
- lumibot/credentials.py +13 -0
- lumibot/data_sources/__init__.py +5 -8
- lumibot/data_sources/data_source.py +6 -2
- lumibot/data_sources/data_source_backtesting.py +30 -0
- lumibot/data_sources/databento_data.py +5 -390
- lumibot/data_sources/databento_data_pandas.py +440 -0
- lumibot/data_sources/databento_data_polars.py +15 -9
- lumibot/data_sources/pandas_data.py +30 -17
- lumibot/data_sources/polars_data.py +986 -0
- lumibot/data_sources/polars_mixin.py +472 -96
- lumibot/data_sources/polygon_data_polars.py +5 -0
- lumibot/data_sources/yahoo_data.py +9 -2
- lumibot/data_sources/yahoo_data_polars.py +5 -0
- lumibot/entities/__init__.py +15 -0
- lumibot/entities/asset.py +5 -28
- lumibot/entities/bars.py +89 -20
- lumibot/entities/data.py +29 -6
- lumibot/entities/data_polars.py +668 -0
- lumibot/entities/position.py +38 -4
- lumibot/strategies/_strategy.py +31 -9
- lumibot/strategies/strategy.py +61 -49
- lumibot/tools/backtest_cache.py +284 -0
- lumibot/tools/databento_helper.py +65 -42
- lumibot/tools/databento_helper_polars.py +748 -778
- lumibot/tools/futures_roll.py +251 -0
- lumibot/tools/indicators.py +135 -104
- lumibot/tools/polars_utils.py +142 -0
- lumibot/tools/thetadata_helper.py +1068 -134
- {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/METADATA +9 -1
- {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/RECORD +72 -148
- tests/backtest/test_databento.py +37 -6
- tests/backtest/test_databento_comprehensive_trading.py +70 -87
- tests/backtest/test_databento_parity.py +31 -7
- tests/backtest/test_debug_avg_fill_price.py +1 -1
- tests/backtest/test_example_strategies.py +11 -1
- tests/backtest/test_futures_edge_cases.py +96 -63
- tests/backtest/test_futures_single_trade.py +2 -2
- tests/backtest/test_futures_ultra_simple.py +2 -2
- tests/backtest/test_polars_lru_eviction.py +470 -0
- tests/backtest/test_yahoo.py +42 -0
- tests/test_asset.py +4 -4
- tests/test_backtest_cache_manager.py +149 -0
- tests/test_backtesting_data_source_env.py +50 -10
- tests/test_continuous_futures_resolution.py +60 -48
- tests/test_data_polars_parity.py +160 -0
- tests/test_databento_asset_validation.py +23 -5
- tests/test_databento_backtesting.py +1 -1
- tests/test_databento_backtesting_polars.py +312 -192
- tests/test_databento_data.py +220 -463
- tests/test_databento_helper.py +6 -1
- tests/test_databento_live.py +10 -10
- tests/test_futures_roll.py +38 -0
- tests/test_indicator_subplots.py +101 -0
- tests/test_market_infinite_loop_bug.py +77 -3
- tests/test_polars_resample.py +67 -0
- tests/test_polygon_helper.py +46 -0
- tests/test_thetadata_backwards_compat.py +97 -0
- tests/test_thetadata_helper.py +222 -23
- tests/test_thetadata_pandas_verification.py +186 -0
- lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/__pycache__/constants.cpython-312.pyc +0 -0
- lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
- lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
- lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
- lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
- lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
- lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
- lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
- lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
- lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
- lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
- lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
- {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/WHEEL +0 -0
- {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/licenses/LICENSE +0 -0
- {lumibot-4.1.2.dist-info → lumibot-4.2.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
19
|
+
try:
|
|
20
|
+
import databento as db
|
|
21
|
+
except ImportError: # pragma: no cover - optional dependency
|
|
22
|
+
db = None
|
|
20
23
|
|
|
21
|
-
from
|
|
22
|
-
from
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|