lumibot 4.0.23__py3-none-any.whl → 4.1.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/__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 +145 -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/__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_polars_backtesting.py +194 -48
- 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.1.1.data/data/ThetaTerminal.jar +0 -0
- {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/METADATA +1 -2
- {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/RECORD +161 -44
- 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 +4 -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 +76 -90
- 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.23.dist-info → lumibot-4.1.1.dist-info}/LICENSE +0 -0
- {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/WHEEL +0 -0
- {lumibot-4.0.23.dist-info → lumibot-4.1.1.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
|
@@ -16,8 +16,7 @@ class TestDataBentoHelper(unittest.TestCase):
|
|
|
16
16
|
self.api_key = "test_api_key"
|
|
17
17
|
self.test_asset_future = Asset(
|
|
18
18
|
symbol="ES",
|
|
19
|
-
asset_type="
|
|
20
|
-
expiration=datetime(2025, 3, 15).date()
|
|
19
|
+
asset_type="CONT_FUTURE"
|
|
21
20
|
)
|
|
22
21
|
self.test_asset_stock = Asset(
|
|
23
22
|
symbol="AAPL",
|
|
@@ -41,10 +40,13 @@ class TestDataBentoHelper(unittest.TestCase):
|
|
|
41
40
|
result = databento_helper._format_futures_symbol_for_databento(mes_continuous, reference_date)
|
|
42
41
|
self.assertIn("MESH5", result)
|
|
43
42
|
|
|
44
|
-
# Test regular future (no expiration) - should
|
|
43
|
+
# Test regular future (no expiration) - should auto-resolve via idiot-proofing
|
|
44
|
+
# Idiot-proofing: futures without expiration are auto-treated as continuous and resolved
|
|
45
45
|
regular_future = Asset(symbol="ES", asset_type="future")
|
|
46
46
|
result = databento_helper._format_futures_symbol_for_databento(regular_future)
|
|
47
|
-
|
|
47
|
+
# Should resolve to a contract month (e.g., ESZ5 for Dec 2025)
|
|
48
|
+
self.assertIn("ES", result)
|
|
49
|
+
self.assertRegex(result, r"ES[FGHJKMNQUVXZ]\d", "Should auto-resolve to contract format like ESZ5")
|
|
48
50
|
|
|
49
51
|
# Test specific contract with expiration (March 2025 = H25)
|
|
50
52
|
result = databento_helper._format_futures_symbol_for_databento(self.test_asset_future)
|
|
@@ -129,45 +131,42 @@ class TestDataBentoHelper(unittest.TestCase):
|
|
|
129
131
|
self.assertEqual(client.timeout, 30)
|
|
130
132
|
self.assertEqual(client.max_retries, 3)
|
|
131
133
|
|
|
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()
|
|
134
|
+
def test_get_price_data_from_databento_success(self):
|
|
135
|
+
"""Test successful data retrieval using real DataBento API"""
|
|
136
|
+
import os
|
|
137
|
+
|
|
138
|
+
# Use real API key from environment
|
|
139
|
+
api_key = os.environ.get("DATABENTO_API_KEY")
|
|
140
|
+
if not api_key:
|
|
141
|
+
self.skipTest("DATABENTO_API_KEY not found in environment")
|
|
142
|
+
|
|
143
|
+
# Use Aug 2024 dates (past data that definitely exists)
|
|
144
|
+
start_date = datetime(2024, 8, 20)
|
|
145
|
+
end_date = datetime(2024, 8, 21)
|
|
146
|
+
|
|
147
|
+
# Test with ES continuous futures (will resolve to appropriate contract)
|
|
148
|
+
es_asset = Asset(
|
|
149
|
+
symbol="ES",
|
|
150
|
+
asset_type=Asset.AssetType.CONT_FUTURE
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
result = databento_helper.get_price_data_from_databento(
|
|
154
|
+
api_key=api_key,
|
|
155
|
+
asset=es_asset,
|
|
156
|
+
start=start_date,
|
|
157
|
+
end=end_date,
|
|
158
|
+
timestep="minute"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Verify result
|
|
162
|
+
self.assertIsNotNone(result, "Should return data from DataBento API")
|
|
163
|
+
self.assertIsInstance(result, pd.DataFrame)
|
|
164
|
+
self.assertGreater(len(result), 0, "Should have at least some data rows")
|
|
165
|
+
|
|
166
|
+
# Verify DataFrame has expected columns
|
|
167
|
+
expected_columns = ['open', 'high', 'low', 'close', 'volume']
|
|
168
|
+
for col in expected_columns:
|
|
169
|
+
self.assertIn(col, result.columns, f"DataFrame should have {col} column")
|
|
171
170
|
|
|
172
171
|
@patch('lumibot.tools.databento_helper.DATABENTO_AVAILABLE', False)
|
|
173
172
|
def test_get_price_data_databento_unavailable(self):
|
|
@@ -217,55 +216,42 @@ class TestDataBentoHelper(unittest.TestCase):
|
|
|
217
216
|
self.end_date,
|
|
218
217
|
"minute"
|
|
219
218
|
)
|
|
220
|
-
|
|
221
|
-
expected_name = "
|
|
219
|
+
|
|
220
|
+
expected_name = "ES_minute_202501010000_202501310000.parquet"
|
|
222
221
|
self.assertEqual(filename.name, expected_name)
|
|
223
222
|
|
|
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')
|
|
223
|
+
def test_no_retry_logic_for_correct_symbol(self):
|
|
224
|
+
"""Test that the function uses correct symbol/dataset without retry logic - using real API"""
|
|
225
|
+
import os
|
|
226
|
+
|
|
227
|
+
# Use real API key from environment
|
|
228
|
+
api_key = os.environ.get("DATABENTO_API_KEY")
|
|
229
|
+
if not api_key:
|
|
230
|
+
self.skipTest("DATABENTO_API_KEY not found in environment")
|
|
231
|
+
|
|
232
|
+
# Use recent dates that should have data
|
|
233
|
+
start_date = datetime(2025, 1, 2)
|
|
234
|
+
end_date = datetime(2025, 1, 3)
|
|
235
|
+
|
|
236
|
+
# Test with MES continuous futures
|
|
237
|
+
mes_asset = Asset(symbol="MES", asset_type="future")
|
|
238
|
+
result = databento_helper.get_price_data_from_databento(
|
|
239
|
+
api_key=api_key,
|
|
240
|
+
asset=mes_asset,
|
|
241
|
+
start=start_date,
|
|
242
|
+
end=end_date,
|
|
243
|
+
timestep="minute"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Verify result
|
|
247
|
+
self.assertIsNotNone(result, "Should return data for MES futures")
|
|
248
|
+
self.assertIsInstance(result, pd.DataFrame)
|
|
249
|
+
self.assertGreater(len(result), 0, "Should have data rows for MES")
|
|
250
|
+
|
|
251
|
+
# Verify DataFrame structure
|
|
252
|
+
expected_columns = ['open', 'high', 'low', 'close', 'volume']
|
|
253
|
+
for col in expected_columns:
|
|
254
|
+
self.assertIn(col, result.columns, f"DataFrame should have {col} column")
|
|
269
255
|
|
|
270
256
|
def test_continuous_futures_integration_edge_cases(self):
|
|
271
257
|
"""Test edge cases for continuous futures integration with Asset class"""
|
|
@@ -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(
|