lumibot 4.0.22__py3-none-any.whl → 4.1.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/__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/__init__.py +6 -5
- 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/backtesting/backtesting_broker.py +209 -9
- lumibot/backtesting/databento_backtesting.py +141 -24
- lumibot/backtesting/thetadata_backtesting.py +63 -42
- 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/brokers/alpaca.py +11 -1
- lumibot/brokers/tradeovate.py +475 -0
- lumibot/components/grok_news_helper.py +284 -0
- lumibot/components/options_helper.py +90 -34
- lumibot/credentials.py +3 -0
- lumibot/data_sources/__init__.py +2 -1
- 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/data_sources/data_source_backtesting.py +3 -5
- lumibot/data_sources/databento_data.py +5 -5
- lumibot/data_sources/databento_data_polars_backtesting.py +636 -0
- lumibot/data_sources/databento_data_polars_live.py +793 -0
- lumibot/data_sources/pandas_data.py +6 -3
- lumibot/data_sources/polars_mixin.py +126 -21
- lumibot/data_sources/tradeovate_data.py +80 -0
- lumibot/data_sources/tradier_data.py +2 -1
- 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/entities/asset.py +8 -0
- lumibot/entities/order.py +1 -1
- lumibot/entities/quote.py +14 -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/strategies/_strategy.py +95 -27
- lumibot/strategies/strategy.py +5 -6
- lumibot/strategies/strategy_executor.py +2 -2
- 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/tools/databento_helper.py +384 -133
- lumibot/tools/databento_helper_polars.py +218 -156
- lumibot/tools/databento_roll.py +216 -0
- lumibot/tools/lumibot_logger.py +32 -17
- lumibot/tools/polygon_helper.py +65 -0
- lumibot/tools/thetadata_helper.py +588 -70
- lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
- lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
- lumibot/traders/trader.py +1 -1
- 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.0.22.dist-info → lumibot-4.1.0.dist-info}/METADATA +1 -2
- {lumibot-4.0.22.dist-info → lumibot-4.1.0.dist-info}/RECORD +164 -46
- tests/backtest/check_timing_offset.py +198 -0
- tests/backtest/check_volume_spike.py +112 -0
- tests/backtest/comprehensive_comparison.py +166 -0
- tests/backtest/debug_comparison.py +91 -0
- tests/backtest/diagnose_price_difference.py +97 -0
- tests/backtest/direct_api_comparison.py +203 -0
- tests/backtest/profile_thetadata_vs_polygon.py +255 -0
- tests/backtest/root_cause_analysis.py +109 -0
- tests/backtest/test_accuracy_verification.py +244 -0
- tests/backtest/test_daily_data_timestamp_comparison.py +801 -0
- tests/backtest/test_databento.py +57 -0
- tests/backtest/test_databento_comprehensive_trading.py +564 -0
- tests/backtest/test_debug_avg_fill_price.py +112 -0
- tests/backtest/test_dividends.py +8 -3
- tests/backtest/test_example_strategies.py +54 -47
- tests/backtest/test_futures_edge_cases.py +451 -0
- tests/backtest/test_futures_single_trade.py +270 -0
- tests/backtest/test_futures_ultra_simple.py +191 -0
- tests/backtest/test_index_data_verification.py +348 -0
- tests/backtest/test_polygon.py +45 -24
- tests/backtest/test_thetadata.py +246 -60
- tests/backtest/test_thetadata_comprehensive.py +729 -0
- tests/backtest/test_thetadata_vs_polygon.py +557 -0
- tests/backtest/test_yahoo.py +1 -2
- tests/conftest.py +20 -0
- tests/test_backtesting_data_source_env.py +249 -0
- tests/test_backtesting_quiet_logs_complete.py +10 -11
- tests/test_databento_helper.py +73 -86
- tests/test_databento_live.py +10 -10
- tests/test_databento_timezone_fixes.py +21 -4
- tests/test_get_historical_prices.py +6 -6
- tests/test_options_helper.py +162 -40
- tests/test_polygon_helper.py +21 -13
- tests/test_quiet_logs_requirements.py +5 -5
- tests/test_thetadata_helper.py +487 -171
- tests/test_yahoo_data.py +125 -0
- {lumibot-4.0.22.dist-info → lumibot-4.1.0.dist-info}/LICENSE +0 -0
- {lumibot-4.0.22.dist-info → lumibot-4.1.0.dist-info}/WHEEL +0 -0
- {lumibot-4.0.22.dist-info → lumibot-4.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test for BACKTESTING_DATA_SOURCE environment variable handling.
|
|
3
|
+
Ensures that datasource_class=None correctly auto-selects from the env var.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from unittest.mock import patch, MagicMock
|
|
8
|
+
import pytest
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
# Load environment variables from .env file
|
|
12
|
+
load_dotenv()
|
|
13
|
+
|
|
14
|
+
from lumibot.strategies import Strategy
|
|
15
|
+
from lumibot.backtesting import (
|
|
16
|
+
PolygonDataBacktesting,
|
|
17
|
+
ThetaDataBacktesting,
|
|
18
|
+
YahooDataBacktesting,
|
|
19
|
+
AlpacaBacktesting,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def restore_theta_credentials():
|
|
25
|
+
"""Save and restore ThetaData credentials file after test."""
|
|
26
|
+
creds_path = "/Users/robertgrzesik/ThetaData/ThetaTerminal/creds.txt"
|
|
27
|
+
original = None
|
|
28
|
+
|
|
29
|
+
# Save original credentials if file exists
|
|
30
|
+
if os.path.exists(creds_path):
|
|
31
|
+
with open(creds_path, 'r') as f:
|
|
32
|
+
original = f.read()
|
|
33
|
+
|
|
34
|
+
yield
|
|
35
|
+
|
|
36
|
+
# Restore original credentials
|
|
37
|
+
if original is not None:
|
|
38
|
+
with open(creds_path, 'w') as f:
|
|
39
|
+
f.write(original)
|
|
40
|
+
elif os.path.exists(creds_path):
|
|
41
|
+
# File didn't exist before, remove it
|
|
42
|
+
os.remove(creds_path)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def clean_environment():
|
|
47
|
+
"""Save and restore environment variables after test."""
|
|
48
|
+
original_env = os.environ.copy()
|
|
49
|
+
|
|
50
|
+
yield
|
|
51
|
+
|
|
52
|
+
# Restore original environment completely
|
|
53
|
+
os.environ.clear()
|
|
54
|
+
os.environ.update(original_env)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SimpleTestStrategy(Strategy):
|
|
58
|
+
"""Minimal strategy for testing datasource auto-selection."""
|
|
59
|
+
|
|
60
|
+
def initialize(self):
|
|
61
|
+
self.sleeptime = "1D"
|
|
62
|
+
|
|
63
|
+
def on_trading_iteration(self):
|
|
64
|
+
if self.first_iteration:
|
|
65
|
+
# Just buy one share to have some activity
|
|
66
|
+
order = self.create_order("SPY", quantity=1, side="buy")
|
|
67
|
+
self.submit_order(order)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestBacktestingDataSourceEnv:
|
|
71
|
+
"""Test BACKTESTING_DATA_SOURCE environment variable."""
|
|
72
|
+
|
|
73
|
+
def test_auto_select_polygon_case_insensitive(self, clean_environment, restore_theta_credentials, caplog):
|
|
74
|
+
"""Test that BACKTESTING_DATA_SOURCE=polygon (lowercase) selects PolygonDataBacktesting."""
|
|
75
|
+
# Configure caplog to capture INFO level logs from lumibot.strategies._strategy
|
|
76
|
+
import logging
|
|
77
|
+
caplog.set_level(logging.INFO, logger='lumibot.strategies._strategy')
|
|
78
|
+
|
|
79
|
+
with patch.dict(os.environ, {'BACKTESTING_DATA_SOURCE': 'polygon'}):
|
|
80
|
+
# Re-import credentials to pick up env change
|
|
81
|
+
from importlib import reload
|
|
82
|
+
import lumibot.credentials
|
|
83
|
+
reload(lumibot.credentials)
|
|
84
|
+
|
|
85
|
+
backtesting_start = datetime(2023, 1, 1)
|
|
86
|
+
backtesting_end = datetime(2023, 1, 10) # Shorter backtest for speed
|
|
87
|
+
|
|
88
|
+
# Run a short backtest to verify env var is read
|
|
89
|
+
SimpleTestStrategy.run_backtest(
|
|
90
|
+
None, # Auto-select from env var
|
|
91
|
+
backtesting_start=backtesting_start,
|
|
92
|
+
backtesting_end=backtesting_end,
|
|
93
|
+
polygon_api_key="test_key",
|
|
94
|
+
show_plot=False,
|
|
95
|
+
show_tearsheet=False,
|
|
96
|
+
show_indicators=False,
|
|
97
|
+
show_progress_bar=False,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Verify the log message shows polygon was selected
|
|
101
|
+
assert any("Auto-selected backtesting data source from BACKTESTING_DATA_SOURCE env var: polygon" in record.message
|
|
102
|
+
for record in caplog.records)
|
|
103
|
+
|
|
104
|
+
def test_auto_select_thetadata_case_insensitive(self, clean_environment, restore_theta_credentials, caplog):
|
|
105
|
+
"""Test that BACKTESTING_DATA_SOURCE=THETADATA (uppercase) selects ThetaDataBacktesting."""
|
|
106
|
+
with patch.dict(os.environ, {'BACKTESTING_DATA_SOURCE': 'THETADATA'}):
|
|
107
|
+
# Re-import credentials to pick up env change
|
|
108
|
+
from importlib import reload
|
|
109
|
+
import lumibot.credentials
|
|
110
|
+
reload(lumibot.credentials)
|
|
111
|
+
|
|
112
|
+
backtesting_start = datetime(2023, 1, 1)
|
|
113
|
+
backtesting_end = datetime(2023, 1, 10) # Shorter backtest for speed
|
|
114
|
+
|
|
115
|
+
# Try to run backtest - may fail due to test credentials, but that's okay
|
|
116
|
+
try:
|
|
117
|
+
SimpleTestStrategy.run_backtest(
|
|
118
|
+
None, # Auto-select from env var
|
|
119
|
+
backtesting_start=backtesting_start,
|
|
120
|
+
backtesting_end=backtesting_end,
|
|
121
|
+
thetadata_username="test_user",
|
|
122
|
+
thetadata_password="test_pass",
|
|
123
|
+
show_plot=False,
|
|
124
|
+
show_tearsheet=False,
|
|
125
|
+
show_indicators=False,
|
|
126
|
+
show_progress_bar=False,
|
|
127
|
+
)
|
|
128
|
+
except Exception:
|
|
129
|
+
# Expected to fail with test credentials - that's okay
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
# Verify the log message shows thetadata was selected OR check for ThetaData error
|
|
133
|
+
thetadata_selected = any("Auto-selected backtesting data source from BACKTESTING_DATA_SOURCE env var: THETADATA" in record.message
|
|
134
|
+
for record in caplog.records)
|
|
135
|
+
thetadata_attempted = any("Cannot connect to Theta Data" in record.message or "ThetaData" in record.message
|
|
136
|
+
for record in caplog.records)
|
|
137
|
+
assert thetadata_selected or thetadata_attempted, "ThetaData was not selected from env var"
|
|
138
|
+
|
|
139
|
+
def test_auto_select_yahoo(self, clean_environment, restore_theta_credentials, caplog):
|
|
140
|
+
"""Test that BACKTESTING_DATA_SOURCE=Yahoo selects YahooDataBacktesting."""
|
|
141
|
+
with patch.dict(os.environ, {'BACKTESTING_DATA_SOURCE': 'Yahoo'}):
|
|
142
|
+
# Re-import credentials to pick up env change
|
|
143
|
+
from importlib import reload
|
|
144
|
+
import lumibot.credentials
|
|
145
|
+
reload(lumibot.credentials)
|
|
146
|
+
|
|
147
|
+
backtesting_start = datetime(2023, 1, 1)
|
|
148
|
+
backtesting_end = datetime(2023, 1, 10) # Shorter backtest for speed
|
|
149
|
+
|
|
150
|
+
# Run a short backtest to verify env var is read
|
|
151
|
+
# If this completes without error, Yahoo was successfully selected
|
|
152
|
+
SimpleTestStrategy.run_backtest(
|
|
153
|
+
None, # Auto-select from env var
|
|
154
|
+
backtesting_start=backtesting_start,
|
|
155
|
+
backtesting_end=backtesting_end,
|
|
156
|
+
show_plot=False,
|
|
157
|
+
show_tearsheet=False,
|
|
158
|
+
show_indicators=False,
|
|
159
|
+
show_progress_bar=False,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# If we got here without exception, Yahoo was successfully used
|
|
163
|
+
# (No explicit verification needed - successful backtest is the proof)
|
|
164
|
+
|
|
165
|
+
def test_invalid_data_source_raises_error(self, clean_environment, restore_theta_credentials):
|
|
166
|
+
"""Test that invalid BACKTESTING_DATA_SOURCE raises ValueError."""
|
|
167
|
+
with patch.dict(os.environ, {'BACKTESTING_DATA_SOURCE': 'InvalidSource'}):
|
|
168
|
+
# Re-import credentials to pick up env change
|
|
169
|
+
from importlib import reload
|
|
170
|
+
import lumibot.credentials
|
|
171
|
+
reload(lumibot.credentials)
|
|
172
|
+
|
|
173
|
+
backtesting_start = datetime(2023, 1, 1)
|
|
174
|
+
backtesting_end = datetime(2023, 1, 31)
|
|
175
|
+
|
|
176
|
+
with pytest.raises(ValueError, match="Unknown BACKTESTING_DATA_SOURCE"):
|
|
177
|
+
SimpleTestStrategy.run_backtest(
|
|
178
|
+
None, # Auto-select from env var
|
|
179
|
+
backtesting_start=backtesting_start,
|
|
180
|
+
backtesting_end=backtesting_end,
|
|
181
|
+
show_plot=False,
|
|
182
|
+
show_tearsheet=False,
|
|
183
|
+
show_indicators=False,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def test_explicit_datasource_overrides_env(self, clean_environment, restore_theta_credentials, caplog):
|
|
187
|
+
"""Test that explicit datasource_class overrides BACKTESTING_DATA_SOURCE env var."""
|
|
188
|
+
with patch.dict(os.environ, {'BACKTESTING_DATA_SOURCE': 'polygon'}):
|
|
189
|
+
# Re-import credentials to pick up env change
|
|
190
|
+
from importlib import reload
|
|
191
|
+
import lumibot.credentials
|
|
192
|
+
reload(lumibot.credentials)
|
|
193
|
+
|
|
194
|
+
backtesting_start = datetime(2023, 1, 1)
|
|
195
|
+
backtesting_end = datetime(2023, 1, 10) # Shorter backtest for speed
|
|
196
|
+
|
|
197
|
+
# Run backtest with explicit Yahoo datasource despite env saying polygon
|
|
198
|
+
SimpleTestStrategy.run_backtest(
|
|
199
|
+
YahooDataBacktesting, # Explicit override
|
|
200
|
+
backtesting_start=backtesting_start,
|
|
201
|
+
backtesting_end=backtesting_end,
|
|
202
|
+
show_plot=False,
|
|
203
|
+
show_tearsheet=False,
|
|
204
|
+
show_indicators=False,
|
|
205
|
+
show_progress_bar=False,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Verify the auto-select message was NOT logged (explicit datasource was used)
|
|
209
|
+
assert not any("Auto-selected backtesting data source" in record.message
|
|
210
|
+
for record in caplog.records)
|
|
211
|
+
|
|
212
|
+
def test_default_thetadata_when_no_env_set(self, clean_environment, restore_theta_credentials, caplog):
|
|
213
|
+
"""Test that ThetaData is the default when BACKTESTING_DATA_SOURCE is not set."""
|
|
214
|
+
# Remove BACKTESTING_DATA_SOURCE from env
|
|
215
|
+
env_without_datasource = {k: v for k, v in os.environ.items() if k != 'BACKTESTING_DATA_SOURCE'}
|
|
216
|
+
|
|
217
|
+
with patch.dict(os.environ, env_without_datasource, clear=True):
|
|
218
|
+
# Re-import credentials to pick up env change
|
|
219
|
+
from importlib import reload
|
|
220
|
+
import lumibot.credentials
|
|
221
|
+
reload(lumibot.credentials)
|
|
222
|
+
|
|
223
|
+
backtesting_start = datetime(2023, 1, 1)
|
|
224
|
+
backtesting_end = datetime(2023, 1, 10) # Shorter backtest for speed
|
|
225
|
+
|
|
226
|
+
# Try to run backtest - may fail due to test credentials, but that's okay
|
|
227
|
+
try:
|
|
228
|
+
SimpleTestStrategy.run_backtest(
|
|
229
|
+
None, # Auto-select from env var (should default to ThetaData)
|
|
230
|
+
backtesting_start=backtesting_start,
|
|
231
|
+
backtesting_end=backtesting_end,
|
|
232
|
+
thetadata_username="test_user",
|
|
233
|
+
thetadata_password="test_pass",
|
|
234
|
+
show_plot=False,
|
|
235
|
+
show_tearsheet=False,
|
|
236
|
+
show_indicators=False,
|
|
237
|
+
show_progress_bar=False,
|
|
238
|
+
)
|
|
239
|
+
except Exception:
|
|
240
|
+
# Expected to fail with test credentials - that's okay
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
# Verify ThetaData was attempted (no auto-select message since it's the default)
|
|
244
|
+
assert any("Cannot connect to Theta Data" in record.message or "ThetaData" in record.message
|
|
245
|
+
for record in caplog.records), "ThetaData was not used as default"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
pytest.main([__file__, "-v"])
|
|
@@ -203,13 +203,12 @@ def test_quiet_logs_false_shows_all_info_plus(clean_environment, temp_logfile):
|
|
|
203
203
|
handler.flush()
|
|
204
204
|
|
|
205
205
|
console_output = captured_console.getvalue()
|
|
206
|
-
|
|
207
|
-
#
|
|
208
|
-
# BACKTESTING_QUIET_LOGS only controls file logging
|
|
206
|
+
|
|
207
|
+
# When BACKTESTING_QUIET_LOGS=false, console should show INFO+ during backtesting
|
|
209
208
|
assert "DEBUG should not appear" not in console_output
|
|
210
|
-
assert "INFO should appear"
|
|
211
|
-
assert "WARNING should appear"
|
|
212
|
-
assert "ERROR should appear" in console_output # Console SHOULD show ERROR
|
|
209
|
+
assert "INFO should appear" in console_output # Console SHOULD show INFO when quiet_logs=false
|
|
210
|
+
assert "WARNING should appear" in console_output # Console SHOULD show WARNING when quiet_logs=false
|
|
211
|
+
assert "ERROR should appear" in console_output # Console SHOULD show ERROR when quiet_logs=false
|
|
213
212
|
|
|
214
213
|
# Read file content
|
|
215
214
|
with open(temp_logfile, 'r') as f:
|
|
@@ -331,11 +330,11 @@ def test_bot_manager_compatibility(clean_environment, temp_logfile):
|
|
|
331
330
|
handler.flush()
|
|
332
331
|
|
|
333
332
|
console_output = captured_console.getvalue()
|
|
334
|
-
|
|
335
|
-
#
|
|
336
|
-
#
|
|
337
|
-
assert "Bot Manager should see this INFO"
|
|
338
|
-
assert "Bot Manager should see this WARNING"
|
|
333
|
+
|
|
334
|
+
# When BACKTESTING_QUIET_LOGS=False, console should show INFO+ during backtesting
|
|
335
|
+
# Bot Manager can read from either CloudWatch/file logs or console
|
|
336
|
+
assert "Bot Manager should see this INFO" in console_output
|
|
337
|
+
assert "Bot Manager should see this WARNING" in console_output
|
|
339
338
|
assert "Bot Manager should see this ERROR" in console_output
|
|
340
339
|
|
|
341
340
|
# Read file content
|
tests/test_databento_helper.py
CHANGED
|
@@ -41,10 +41,13 @@ class TestDataBentoHelper(unittest.TestCase):
|
|
|
41
41
|
result = databento_helper._format_futures_symbol_for_databento(mes_continuous, reference_date)
|
|
42
42
|
self.assertIn("MESH5", result)
|
|
43
43
|
|
|
44
|
-
# Test regular future (no expiration) - should
|
|
44
|
+
# Test regular future (no expiration) - should auto-resolve via idiot-proofing
|
|
45
|
+
# Idiot-proofing: futures without expiration are auto-treated as continuous and resolved
|
|
45
46
|
regular_future = Asset(symbol="ES", asset_type="future")
|
|
46
47
|
result = databento_helper._format_futures_symbol_for_databento(regular_future)
|
|
47
|
-
|
|
48
|
+
# Should resolve to a contract month (e.g., ESZ5 for Dec 2025)
|
|
49
|
+
self.assertIn("ES", result)
|
|
50
|
+
self.assertRegex(result, r"ES[FGHJKMNQUVXZ]\d", "Should auto-resolve to contract format like ESZ5")
|
|
48
51
|
|
|
49
52
|
# Test specific contract with expiration (March 2025 = H25)
|
|
50
53
|
result = databento_helper._format_futures_symbol_for_databento(self.test_asset_future)
|
|
@@ -129,45 +132,42 @@ class TestDataBentoHelper(unittest.TestCase):
|
|
|
129
132
|
self.assertEqual(client.timeout, 30)
|
|
130
133
|
self.assertEqual(client.max_retries, 3)
|
|
131
134
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
self.
|
|
168
|
-
|
|
169
|
-
# Verify cache was called
|
|
170
|
-
mock_save.assert_called_once()
|
|
135
|
+
def test_get_price_data_from_databento_success(self):
|
|
136
|
+
"""Test successful data retrieval using real DataBento API"""
|
|
137
|
+
import os
|
|
138
|
+
|
|
139
|
+
# Use real API key from environment
|
|
140
|
+
api_key = os.environ.get("DATABENTO_API_KEY")
|
|
141
|
+
if not api_key:
|
|
142
|
+
self.skipTest("DATABENTO_API_KEY not found in environment")
|
|
143
|
+
|
|
144
|
+
# Use Aug 2024 dates (past data that definitely exists)
|
|
145
|
+
start_date = datetime(2024, 8, 20)
|
|
146
|
+
end_date = datetime(2024, 8, 21)
|
|
147
|
+
|
|
148
|
+
# Test with ES continuous futures (will resolve to appropriate contract)
|
|
149
|
+
es_asset = Asset(
|
|
150
|
+
symbol="ES",
|
|
151
|
+
asset_type=Asset.AssetType.CONT_FUTURE
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
result = databento_helper.get_price_data_from_databento(
|
|
155
|
+
api_key=api_key,
|
|
156
|
+
asset=es_asset,
|
|
157
|
+
start=start_date,
|
|
158
|
+
end=end_date,
|
|
159
|
+
timestep="minute"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Verify result
|
|
163
|
+
self.assertIsNotNone(result, "Should return data from DataBento API")
|
|
164
|
+
self.assertIsInstance(result, pd.DataFrame)
|
|
165
|
+
self.assertGreater(len(result), 0, "Should have at least some data rows")
|
|
166
|
+
|
|
167
|
+
# Verify DataFrame has expected columns
|
|
168
|
+
expected_columns = ['open', 'high', 'low', 'close', 'volume']
|
|
169
|
+
for col in expected_columns:
|
|
170
|
+
self.assertIn(col, result.columns, f"DataFrame should have {col} column")
|
|
171
171
|
|
|
172
172
|
@patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', False)
|
|
173
173
|
def test_get_price_data_databento_unavailable(self):
|
|
@@ -221,51 +221,38 @@ class TestDataBentoHelper(unittest.TestCase):
|
|
|
221
221
|
expected_name = "ES_20250315_minute_20250101_20250131.parquet"
|
|
222
222
|
self.assertEqual(filename.name, expected_name)
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
#
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
# Verify result
|
|
259
|
-
self.assertIsNotNone(result)
|
|
260
|
-
|
|
261
|
-
# Verify that get_historical_data was called exactly once with correct parameters
|
|
262
|
-
mock_client_instance.get_historical_data.assert_called_once()
|
|
263
|
-
call_args = mock_client_instance.get_historical_data.call_args
|
|
264
|
-
|
|
265
|
-
# Check that it was called with the correct symbol (MES) and dataset (GLBX.MDP3)
|
|
266
|
-
self.assertEqual(call_args[1]['symbols'], 'MES')
|
|
267
|
-
self.assertEqual(call_args[1]['dataset'], 'GLBX.MDP3')
|
|
268
|
-
self.assertEqual(call_args[1]['schema'], 'ohlcv-1m')
|
|
224
|
+
def test_no_retry_logic_for_correct_symbol(self):
|
|
225
|
+
"""Test that the function uses correct symbol/dataset without retry logic - using real API"""
|
|
226
|
+
import os
|
|
227
|
+
|
|
228
|
+
# Use real API key from environment
|
|
229
|
+
api_key = os.environ.get("DATABENTO_API_KEY")
|
|
230
|
+
if not api_key:
|
|
231
|
+
self.skipTest("DATABENTO_API_KEY not found in environment")
|
|
232
|
+
|
|
233
|
+
# Use recent dates that should have data
|
|
234
|
+
start_date = datetime(2025, 1, 2)
|
|
235
|
+
end_date = datetime(2025, 1, 3)
|
|
236
|
+
|
|
237
|
+
# Test with MES continuous futures
|
|
238
|
+
mes_asset = Asset(symbol="MES", asset_type="future")
|
|
239
|
+
result = databento_helper.get_price_data_from_databento(
|
|
240
|
+
api_key=api_key,
|
|
241
|
+
asset=mes_asset,
|
|
242
|
+
start=start_date,
|
|
243
|
+
end=end_date,
|
|
244
|
+
timestep="minute"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Verify result
|
|
248
|
+
self.assertIsNotNone(result, "Should return data for MES futures")
|
|
249
|
+
self.assertIsInstance(result, pd.DataFrame)
|
|
250
|
+
self.assertGreater(len(result), 0, "Should have data rows for MES")
|
|
251
|
+
|
|
252
|
+
# Verify DataFrame structure
|
|
253
|
+
expected_columns = ['open', 'high', 'low', 'close', 'volume']
|
|
254
|
+
for col in expected_columns:
|
|
255
|
+
self.assertIn(col, result.columns, f"DataFrame should have {col} column")
|
|
269
256
|
|
|
270
257
|
def test_continuous_futures_integration_edge_cases(self):
|
|
271
258
|
"""Test edge cases for continuous futures integration with Asset class"""
|
tests/test_databento_live.py
CHANGED
|
@@ -27,13 +27,13 @@ load_dotenv()
|
|
|
27
27
|
def test_symbol_resolution():
|
|
28
28
|
"""Test that symbols are properly resolved to contract codes"""
|
|
29
29
|
from lumibot.entities import Asset
|
|
30
|
-
from lumibot.data_sources.
|
|
30
|
+
from lumibot.data_sources.databento_data_polars_live import DataBentoDataPolarsLive
|
|
31
31
|
|
|
32
32
|
print("\n" + "="*60)
|
|
33
33
|
print("TEST 1: Symbol Resolution")
|
|
34
34
|
print("="*60)
|
|
35
35
|
|
|
36
|
-
data_source =
|
|
36
|
+
data_source = DataBentoDataPolarsLive(
|
|
37
37
|
api_key=os.getenv('DATABENTO_API_KEY'),
|
|
38
38
|
has_paid_subscription=True,
|
|
39
39
|
enable_live_stream=False # Don't need streaming for this test
|
|
@@ -97,14 +97,14 @@ def test_live_api_connection():
|
|
|
97
97
|
def test_minute_bar_aggregation():
|
|
98
98
|
"""Test minute bar aggregation with <1 minute lag"""
|
|
99
99
|
from lumibot.entities import Asset
|
|
100
|
-
from lumibot.data_sources.
|
|
100
|
+
from lumibot.data_sources.databento_data_polars_live import DataBentoDataPolarsLive
|
|
101
101
|
|
|
102
102
|
print("\n" + "="*60)
|
|
103
103
|
print("TEST 3: Minute Bar Aggregation & Latency")
|
|
104
104
|
print("="*60)
|
|
105
105
|
|
|
106
106
|
# Initialize with Live API
|
|
107
|
-
data_source =
|
|
107
|
+
data_source = DataBentoDataPolarsLive(
|
|
108
108
|
api_key=os.getenv('DATABENTO_API_KEY'),
|
|
109
109
|
has_paid_subscription=True,
|
|
110
110
|
enable_live_stream=True
|
|
@@ -175,13 +175,13 @@ def test_minute_bar_aggregation():
|
|
|
175
175
|
)
|
|
176
176
|
def test_api_routing():
|
|
177
177
|
"""Test that correct API is used based on time range"""
|
|
178
|
-
from lumibot.data_sources.
|
|
178
|
+
from lumibot.data_sources.databento_data_polars_live import DataBentoDataPolarsLive
|
|
179
179
|
|
|
180
180
|
print("\n" + "="*60)
|
|
181
181
|
print("TEST 4: API Routing (Live vs Historical)")
|
|
182
182
|
print("="*60)
|
|
183
183
|
|
|
184
|
-
data_source =
|
|
184
|
+
data_source = DataBentoDataPolarsLive(
|
|
185
185
|
api_key=os.getenv('DATABENTO_API_KEY'),
|
|
186
186
|
has_paid_subscription=True,
|
|
187
187
|
enable_live_stream=True
|
|
@@ -219,13 +219,13 @@ def test_api_routing():
|
|
|
219
219
|
def test_long_time_periods():
|
|
220
220
|
"""Test different time periods including long periods (500+ bars)"""
|
|
221
221
|
from lumibot.entities import Asset
|
|
222
|
-
from lumibot.data_sources.
|
|
222
|
+
from lumibot.data_sources.databento_data_polars_live import DataBentoDataPolarsLive
|
|
223
223
|
|
|
224
224
|
print("\n" + "="*60)
|
|
225
225
|
print("TEST 5: Long Time Period Handling (500+ bars)")
|
|
226
226
|
print("="*60)
|
|
227
227
|
|
|
228
|
-
data_source =
|
|
228
|
+
data_source = DataBentoDataPolarsLive(
|
|
229
229
|
api_key=os.getenv('DATABENTO_API_KEY'),
|
|
230
230
|
has_paid_subscription=True,
|
|
231
231
|
enable_live_stream=True
|
|
@@ -324,14 +324,14 @@ def test_long_time_periods():
|
|
|
324
324
|
def test_continuous_latency_monitoring():
|
|
325
325
|
"""Run continuous tests to verify consistent <1 minute lag"""
|
|
326
326
|
from lumibot.entities import Asset
|
|
327
|
-
from lumibot.data_sources.
|
|
327
|
+
from lumibot.data_sources.databento_data_polars_live import DataBentoDataPolarsLive
|
|
328
328
|
|
|
329
329
|
print("\n" + "="*60)
|
|
330
330
|
print("TEST 6: Continuous Latency Monitoring")
|
|
331
331
|
print("="*60)
|
|
332
332
|
print("Running 5 consecutive tests to verify consistent low latency...")
|
|
333
333
|
|
|
334
|
-
data_source =
|
|
334
|
+
data_source = DataBentoDataPolarsLive(
|
|
335
335
|
api_key=os.getenv('DATABENTO_API_KEY'),
|
|
336
336
|
has_paid_subscription=True,
|
|
337
337
|
enable_live_stream=True
|
|
@@ -273,10 +273,27 @@ def test_databento_api_integration(api_key_available):
|
|
|
273
273
|
"""Test DataBento API integration when API key is available"""
|
|
274
274
|
if not api_key_available:
|
|
275
275
|
pytest.skip("DataBento API key not available")
|
|
276
|
-
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
|
|
277
|
+
# Test actual DataBento API integration with real credentials
|
|
278
|
+
from lumibot.credentials import DATABENTO_CONFIG
|
|
279
|
+
|
|
280
|
+
# Verify credentials are available
|
|
281
|
+
api_key = DATABENTO_CONFIG.get('API_KEY')
|
|
282
|
+
if not api_key or api_key == '<your key here>':
|
|
283
|
+
pytest.skip("Valid DataBento API key not configured")
|
|
284
|
+
|
|
285
|
+
# Initialize DataBento data source with real API key
|
|
286
|
+
data_source = DataBentoData(api_key=api_key)
|
|
287
|
+
|
|
288
|
+
# Verify initialization succeeded
|
|
289
|
+
assert data_source is not None
|
|
290
|
+
assert data_source.SOURCE == "DATABENTO"
|
|
291
|
+
assert data_source._api_key == api_key # Note: private attribute
|
|
292
|
+
|
|
293
|
+
# Verify the data source is a live data source (not backtesting)
|
|
294
|
+
assert hasattr(data_source, 'get_last_price') or hasattr(data_source, 'get_quote')
|
|
295
|
+
|
|
296
|
+
print(f"✓ DataBento API integration successful with key: {api_key[:10]}...")
|
|
280
297
|
|
|
281
298
|
|
|
282
299
|
if __name__ == "__main__":
|
|
@@ -128,10 +128,10 @@ class TestBacktestingDataSources:
|
|
|
128
128
|
timestep = "day"
|
|
129
129
|
tzinfo = pytz.timezone('America/New_York')
|
|
130
130
|
|
|
131
|
-
datetime_start = tzinfo.localize(datetime(
|
|
132
|
-
datetime_end = tzinfo.localize(datetime(
|
|
131
|
+
datetime_start = tzinfo.localize(datetime(2025, 1, 2))
|
|
132
|
+
datetime_end = tzinfo.localize(datetime(2025, 12, 31))
|
|
133
133
|
# First trading day after MLK day
|
|
134
|
-
now = tzinfo.localize(datetime(
|
|
134
|
+
now = tzinfo.localize(datetime(2025, 1, 21)).replace(hour=9, minute=30)
|
|
135
135
|
data_source = PolygonDataBacktesting(
|
|
136
136
|
datetime_start,
|
|
137
137
|
datetime_end,
|
|
@@ -162,10 +162,10 @@ class TestBacktestingDataSources:
|
|
|
162
162
|
timestep = "day"
|
|
163
163
|
tzinfo = pytz.timezone('America/New_York')
|
|
164
164
|
|
|
165
|
-
datetime_start = tzinfo.localize(datetime(
|
|
166
|
-
datetime_end = tzinfo.localize(datetime(
|
|
165
|
+
datetime_start = tzinfo.localize(datetime(2025, 1, 2))
|
|
166
|
+
datetime_end = tzinfo.localize(datetime(2025, 12, 31))
|
|
167
167
|
# First trading day after MLK day
|
|
168
|
-
now = tzinfo.localize(datetime(
|
|
168
|
+
now = tzinfo.localize(datetime(2025, 1, 21)).replace(hour=9, minute=30)
|
|
169
169
|
|
|
170
170
|
length = 10
|
|
171
171
|
data_source = PolygonDataBacktesting(
|