lumibot 4.2.3__py3-none-any.whl → 4.2.5__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/strategies/_strategy.py +30 -13
- lumibot/strategies/strategy_executor.py +1 -3
- lumibot/tools/thetadata_helper.py +8 -2
- {lumibot-4.2.3.dist-info → lumibot-4.2.5.dist-info}/METADATA +1 -1
- {lumibot-4.2.3.dist-info → lumibot-4.2.5.dist-info}/RECORD +10 -9
- tests/test_backtesting_data_source_env.py +3 -0
- tests/test_backtesting_datetime_normalization.py +94 -0
- {lumibot-4.2.3.dist-info → lumibot-4.2.5.dist-info}/WHEEL +0 -0
- {lumibot-4.2.3.dist-info → lumibot-4.2.5.dist-info}/licenses/LICENSE +0 -0
- {lumibot-4.2.3.dist-info → lumibot-4.2.5.dist-info}/top_level.txt +0 -0
lumibot/strategies/_strategy.py
CHANGED
|
@@ -124,6 +124,17 @@ class Vars:
|
|
|
124
124
|
|
|
125
125
|
|
|
126
126
|
class _Strategy:
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _normalize_backtest_datetime(value):
|
|
129
|
+
"""Convert backtest boundary datetimes to the LumiBot default timezone."""
|
|
130
|
+
if value is None:
|
|
131
|
+
return None
|
|
132
|
+
aware = to_datetime_aware(value)
|
|
133
|
+
tzinfo = getattr(aware, "tzinfo", None)
|
|
134
|
+
if tzinfo is not None and tzinfo != LUMIBOT_DEFAULT_PYTZ:
|
|
135
|
+
return aware.astimezone(LUMIBOT_DEFAULT_PYTZ)
|
|
136
|
+
return aware
|
|
137
|
+
|
|
127
138
|
@property
|
|
128
139
|
def is_backtesting(self) -> bool:
|
|
129
140
|
"""Boolean flag indicating whether the strategy is running in backtesting mode."""
|
|
@@ -1388,15 +1399,9 @@ class _Strategy:
|
|
|
1388
1399
|
if use_other_option_source and not isinstance(optionsource_class, type):
|
|
1389
1400
|
raise ValueError(f"`optionsource_class` must be a class. You passed in {optionsource_class}")
|
|
1390
1401
|
|
|
1391
|
-
self.verify_backtest_inputs(backtesting_start, backtesting_end)
|
|
1392
|
-
|
|
1393
|
-
if not self.IS_BACKTESTABLE:
|
|
1394
|
-
get_logger(__name__).warning(f"Strategy {name + ' ' if name is not None else ''}cannot be " f"backtested at the moment")
|
|
1395
|
-
return None
|
|
1396
|
-
|
|
1397
1402
|
try:
|
|
1398
|
-
backtesting_start =
|
|
1399
|
-
backtesting_end =
|
|
1403
|
+
backtesting_start = self._normalize_backtest_datetime(backtesting_start)
|
|
1404
|
+
backtesting_end = self._normalize_backtest_datetime(backtesting_end)
|
|
1400
1405
|
except AttributeError:
|
|
1401
1406
|
get_logger(__name__).error(
|
|
1402
1407
|
"`backtesting_start` and `backtesting_end` must be datetime objects. \n"
|
|
@@ -1405,6 +1410,15 @@ class _Strategy:
|
|
|
1405
1410
|
)
|
|
1406
1411
|
return None
|
|
1407
1412
|
|
|
1413
|
+
get_logger(__name__).info("Backtest start = %s", backtesting_start)
|
|
1414
|
+
get_logger(__name__).info("Backtest end = %s", backtesting_end)
|
|
1415
|
+
|
|
1416
|
+
self.verify_backtest_inputs(backtesting_start, backtesting_end)
|
|
1417
|
+
|
|
1418
|
+
if not self.IS_BACKTESTABLE:
|
|
1419
|
+
get_logger(__name__).warning(f"Strategy {name + ' ' if name is not None else ''}cannot be " f"backtested at the moment")
|
|
1420
|
+
return None
|
|
1421
|
+
|
|
1408
1422
|
if BACKTESTING_QUIET_LOGS is not None:
|
|
1409
1423
|
quiet_logs = BACKTESTING_QUIET_LOGS
|
|
1410
1424
|
|
|
@@ -1628,18 +1642,21 @@ class _Strategy:
|
|
|
1628
1642
|
if not isinstance(backtesting_end, datetime.datetime):
|
|
1629
1643
|
raise ValueError(f"`backtesting_end` must be a datetime object. You passed in {backtesting_end}")
|
|
1630
1644
|
|
|
1645
|
+
start_dt = cls._normalize_backtest_datetime(backtesting_start)
|
|
1646
|
+
end_dt = cls._normalize_backtest_datetime(backtesting_end)
|
|
1647
|
+
|
|
1631
1648
|
# Check that backtesting end is after backtesting start
|
|
1632
|
-
if
|
|
1649
|
+
if end_dt <= start_dt:
|
|
1633
1650
|
raise ValueError(
|
|
1634
1651
|
f"`backtesting_end` must be after `backtesting_start`. You passed in "
|
|
1635
|
-
f"{
|
|
1652
|
+
f"{end_dt} and {start_dt}"
|
|
1636
1653
|
)
|
|
1637
1654
|
|
|
1638
1655
|
# Check that backtesting_end is not in the future
|
|
1639
|
-
now = datetime.datetime.now(
|
|
1640
|
-
if
|
|
1656
|
+
now = datetime.datetime.now(end_dt.tzinfo) if end_dt.tzinfo else datetime.datetime.now()
|
|
1657
|
+
if end_dt > now:
|
|
1641
1658
|
raise ValueError(
|
|
1642
|
-
f"`backtesting_end` cannot be in the future. You passed in {
|
|
1659
|
+
f"`backtesting_end` cannot be in the future. You passed in {end_dt}, now is {now}"
|
|
1643
1660
|
)
|
|
1644
1661
|
|
|
1645
1662
|
def send_update_to_cloud(self):
|
|
@@ -13,8 +13,6 @@ import pandas_market_calendars as mcal
|
|
|
13
13
|
from apscheduler.jobstores.memory import MemoryJobStore
|
|
14
14
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
15
15
|
from apscheduler.triggers.cron import CronTrigger
|
|
16
|
-
from termcolor import colored
|
|
17
|
-
|
|
18
16
|
from lumibot.constants import LUMIBOT_DEFAULT_PYTZ
|
|
19
17
|
from lumibot.entities import Asset, Order
|
|
20
18
|
from lumibot.entities import Asset
|
|
@@ -1166,7 +1164,7 @@ class StrategyExecutor(Thread):
|
|
|
1166
1164
|
# For live trading, stop when market closes
|
|
1167
1165
|
return False
|
|
1168
1166
|
|
|
1169
|
-
self.strategy.
|
|
1167
|
+
self.strategy.logger.debug("Sleeping for %s seconds", strategy_sleeptime)
|
|
1170
1168
|
|
|
1171
1169
|
# Run process orders at the market close time first (if not continuous market)
|
|
1172
1170
|
if not is_continuous_market:
|
|
@@ -311,7 +311,13 @@ def get_price_data(
|
|
|
311
311
|
)
|
|
312
312
|
|
|
313
313
|
if cache_file.exists():
|
|
314
|
-
logger.
|
|
314
|
+
logger.debug(
|
|
315
|
+
"\nLoading '%s' pricing data for %s / %s with '%s' timespan from cache file...",
|
|
316
|
+
datastyle,
|
|
317
|
+
asset,
|
|
318
|
+
quote_asset,
|
|
319
|
+
timespan,
|
|
320
|
+
)
|
|
315
321
|
df_cached = load_cache(cache_file)
|
|
316
322
|
if df_cached is not None and not df_cached.empty:
|
|
317
323
|
df_all = df_cached.copy() # Make a copy so we can check the original later for differences
|
|
@@ -372,7 +378,7 @@ def get_price_data(
|
|
|
372
378
|
)
|
|
373
379
|
if not missing_dates:
|
|
374
380
|
if df_all is not None and not df_all.empty:
|
|
375
|
-
logger.
|
|
381
|
+
logger.debug("ThetaData cache HIT for %s %s %s (%d rows).", asset, timespan, datastyle, len(df_all))
|
|
376
382
|
# DEBUG-LOG: Cache hit
|
|
377
383
|
logger.debug(
|
|
378
384
|
"[THETA][DEBUG][CACHE][HIT] asset=%s timespan=%s datastyle=%s rows=%d start=%s end=%s",
|
|
@@ -104,10 +104,10 @@ lumibot/example_strategies/test_broker_functions.py,sha256=wnVS-M_OtzMgaXVBgshVE
|
|
|
104
104
|
lumibot/resources/ThetaTerminal.jar,sha256=K6GeeFcN8-gvyL2x5iq5pzD79KfPJvMK8iiezi3TmNQ,11834389
|
|
105
105
|
lumibot/resources/conf.yaml,sha256=rjB9-10JP7saZ_edjX5bQDGfuc3amOQTUUUr-UiMpNA,597
|
|
106
106
|
lumibot/strategies/__init__.py,sha256=jEZ95K5hG0f595EXYKWwL2_UsnWWk5Pug361PK2My2E,79
|
|
107
|
-
lumibot/strategies/_strategy.py,sha256=
|
|
107
|
+
lumibot/strategies/_strategy.py,sha256=wWx2te98kwMTuusioKR24KsKW8Djs3yhOb2nNmvgbiU,111467
|
|
108
108
|
lumibot/strategies/session_manager.py,sha256=Nze6UYNSPlCsf-tyHvtFqUeL44WSNHjwsKrIepvsyCY,12956
|
|
109
109
|
lumibot/strategies/strategy.py,sha256=toPeL5oIVWmCxBNcfXqIuTCF_EeCfIVj425PrSYImCo,170021
|
|
110
|
-
lumibot/strategies/strategy_executor.py,sha256=
|
|
110
|
+
lumibot/strategies/strategy_executor.py,sha256=AnmXlKD2eMgKXs3TrD1u8T_Zsn_8GnG5KRcM_Pq-JBQ,70749
|
|
111
111
|
lumibot/tools/__init__.py,sha256=oRRoK2NBkfnc0kueAfY0HrWVKgzRBO1hlglVMR4jr5M,1501
|
|
112
112
|
lumibot/tools/alpaca_helpers.py,sha256=nhBS-sv28lZfIQ85szC9El8VHLrCw5a5KbsGOOEjm6w,3147
|
|
113
113
|
lumibot/tools/backtest_cache.py,sha256=A-Juzu0swZI_FP4U7cd7ruYTgJYgV8BPf_WJDI-NtrM,9556
|
|
@@ -132,7 +132,7 @@ lumibot/tools/polygon_helper_async.py,sha256=YHDXa9kmkkn8jh7hToY6GP5etyXS9Tj-uky
|
|
|
132
132
|
lumibot/tools/polygon_helper_polars_optimized.py,sha256=NaIZ-5Av-G2McPEKHyJ-x65W72W_Agnz4lRgvXfQp8c,30415
|
|
133
133
|
lumibot/tools/projectx_helpers.py,sha256=EIemLfbG923T_RBV_i6s6A9xgs7dt0et0oCnhFwdWfA,58299
|
|
134
134
|
lumibot/tools/schwab_helper.py,sha256=CXnYhgsXOIb5MgmIYOp86aLxsBF9oeVrMGrjwl_GEv0,11768
|
|
135
|
-
lumibot/tools/thetadata_helper.py,sha256
|
|
135
|
+
lumibot/tools/thetadata_helper.py,sha256=Guutp_2QAZe22_r6pftCojpTb3DpDp4Ul_szf6N7X1I,80152
|
|
136
136
|
lumibot/tools/types.py,sha256=x-aQBeC6ZTN2-pUyxyo69Q0j5e0c_swdfe06kfrWSVc,1978
|
|
137
137
|
lumibot/tools/yahoo_helper.py,sha256=htcKKkuktatIckVKfLc_ms0X75mXColysQhrZW244z8,19497
|
|
138
138
|
lumibot/tools/yahoo_helper_polars_optimized.py,sha256=g9xBN-ReHSW4Aj9EMU_OncBXVS1HpfL8LTHit9ZxFY4,7417
|
|
@@ -142,7 +142,7 @@ lumibot/traders/trader.py,sha256=KMif3WoZtnSxA0BzoK3kvkTITNELrDFIortx1BYBv8s,967
|
|
|
142
142
|
lumibot/trading_builtins/__init__.py,sha256=vH2QL5zLjL3slfEV1YW-BvQHtEYLCFkIWTZDfh3y8LE,87
|
|
143
143
|
lumibot/trading_builtins/custom_stream.py,sha256=8_XiPT0JzyXrgnXCXoovGGUrWEfnG4ohIYMPfB_Nook,5264
|
|
144
144
|
lumibot/trading_builtins/safe_list.py,sha256=IIjZOHSiZYK25A4WBts0oJaZNOJDsjZL65MOSHhE3Ig,1975
|
|
145
|
-
lumibot-4.2.
|
|
145
|
+
lumibot-4.2.5.dist-info/licenses/LICENSE,sha256=fYhGIyxjyNXACgpNQS3xxpxDOaVOWRVxZMCRbsDv8k0,35130
|
|
146
146
|
tests/__init__.py,sha256=3-VoT-nAuqMfwufd4ceN6fXaHl_zCfDCSXJOTp1ywYQ,393
|
|
147
147
|
tests/conftest.py,sha256=UBw_2fx7r6TZPKus2b1Qxrzmd4bg8EEBnX1vCHUuSVA,3311
|
|
148
148
|
tests/fixtures.py,sha256=wOHQsh1SGHnXe_PGi6kDWI30CS_Righi7Ig7vwSEKT4,9082
|
|
@@ -162,7 +162,8 @@ tests/test_backtesting_broker.py,sha256=rxZGH5cgiWLmNGdI3k9fti3Fp9IOSohq8xD2E3L2
|
|
|
162
162
|
tests/test_backtesting_broker_await_close.py,sha256=WbehY7E4Qet3_Mo7lpfgjmhtI9pnJPIt9mkFI15Dzho,7545
|
|
163
163
|
tests/test_backtesting_broker_time_advance.py,sha256=FCv0nKG8BQlEjNft7kmQYm9M2CsLIZ0b7mWCllOHQxc,6378
|
|
164
164
|
tests/test_backtesting_crypto_cash_unit.py,sha256=4EO9jVajdZNV0M7zSyp4gpR_msZFoM4x5tb-6g-mHO8,11399
|
|
165
|
-
tests/test_backtesting_data_source_env.py,sha256=
|
|
165
|
+
tests/test_backtesting_data_source_env.py,sha256=ZzpF42-tMc8qqETuy_nf43UsSZMbHtS_ivH93ZqV5P0,12460
|
|
166
|
+
tests/test_backtesting_datetime_normalization.py,sha256=n1ObwObTI_V4uQOQw_WIii7Ph_OgPZYIZGbx5Jv_19U,3245
|
|
166
167
|
tests/test_backtesting_flow_control.py,sha256=pBqW-fa-HnZq0apUBltalGMM-vNJ_2A5W2SoJzMK8Mg,7208
|
|
167
168
|
tests/test_backtesting_multileg_unit.py,sha256=h1DPfVuYXXx-uq6KtUjr6_nasZuXPm_5gFat1XxCKIo,6456
|
|
168
169
|
tests/test_backtesting_quiet_logs_complete.py,sha256=x-GfOiqkiUu8pYKCzB0UUacn13Nx_cPRth7_jmPY2Y8,14155
|
|
@@ -280,7 +281,7 @@ tests/backtest/test_thetadata.py,sha256=xWYfC9C4EhbMDb29qyZWHO3sSWaLIPzzvcMbHCt5
|
|
|
280
281
|
tests/backtest/test_thetadata_comprehensive.py,sha256=-gN3xLJcJtlB-k4vlaK82DCZDGDmr0LNZZDzn-aN3l4,26120
|
|
281
282
|
tests/backtest/test_thetadata_vs_polygon.py,sha256=dZqsrOx3u3cz-1onIO6o5BDRjI1ey7U9vIkZupfXoig,22831
|
|
282
283
|
tests/backtest/test_yahoo.py,sha256=2FguUTUMC9_A20eqxnZ17rN3tT9n6hyvJHaL98QKpqY,3443
|
|
283
|
-
lumibot-4.2.
|
|
284
|
-
lumibot-4.2.
|
|
285
|
-
lumibot-4.2.
|
|
286
|
-
lumibot-4.2.
|
|
284
|
+
lumibot-4.2.5.dist-info/METADATA,sha256=lK1f_O0TcWv79IUVPehgpO2s_dQE1jrsj69_LCqeKM8,12092
|
|
285
|
+
lumibot-4.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
286
|
+
lumibot-4.2.5.dist-info/top_level.txt,sha256=otUnUjDFVASauEDiTiAzNgMyqQ1B6jjS3QqqP-WSx38,14
|
|
287
|
+
lumibot-4.2.5.dist-info/RECORD,,
|
|
@@ -75,6 +75,9 @@ class TestBacktestingDataSourceEnv:
|
|
|
75
75
|
# Configure caplog to capture INFO level logs from lumibot.strategies._strategy
|
|
76
76
|
import logging
|
|
77
77
|
caplog.set_level(logging.INFO, logger='lumibot.strategies._strategy')
|
|
78
|
+
polygon_key = os.environ.get("POLYGON_API_KEY")
|
|
79
|
+
if not polygon_key:
|
|
80
|
+
pytest.skip("Polygon API key not configured")
|
|
78
81
|
|
|
79
82
|
with patch.dict(os.environ, {'BACKTESTING_DATA_SOURCE': 'polygon'}):
|
|
80
83
|
# Re-import credentials to pick up env change
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from lumibot.constants import LUMIBOT_DEFAULT_PYTZ
|
|
7
|
+
from lumibot.strategies import Strategy
|
|
8
|
+
from lumibot.strategies._strategy import _Strategy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MinimalStrategy(Strategy):
|
|
12
|
+
"""No-op strategy used for backtest scaffolding."""
|
|
13
|
+
|
|
14
|
+
def initialize(self):
|
|
15
|
+
self.sleeptime = "1D"
|
|
16
|
+
|
|
17
|
+
def on_trading_iteration(self):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DummyDataSource:
|
|
22
|
+
"""Lightweight datasource stub capturing the start/end datetimes."""
|
|
23
|
+
|
|
24
|
+
SOURCE = "dummy"
|
|
25
|
+
|
|
26
|
+
def __init__(self, datetime_start=None, datetime_end=None, **kwargs):
|
|
27
|
+
self.datetime_start = datetime_start
|
|
28
|
+
self.datetime_end = datetime_end
|
|
29
|
+
self._data_store = {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DummyTrader:
|
|
33
|
+
"""Trader stub that records strategies and returns canned results."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, *args, **kwargs):
|
|
36
|
+
self._strategies = []
|
|
37
|
+
|
|
38
|
+
def add_strategy(self, strategy):
|
|
39
|
+
self._strategies.append(strategy)
|
|
40
|
+
|
|
41
|
+
def run_all(self, **_kwargs):
|
|
42
|
+
return {strategy.name: {"dummy": True} for strategy in self._strategies}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _EarlyExit(Exception):
|
|
46
|
+
"""Signal to stop run_backtest after the datasource is constructed."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_verify_backtest_inputs_accepts_mixed_timezones():
|
|
50
|
+
"""Regression: verify_backtest_inputs must not crash on naive vs aware inputs."""
|
|
51
|
+
naive_start = datetime.datetime(2025, 1, 1)
|
|
52
|
+
aware_end = datetime.datetime(2025, 9, 30, tzinfo=datetime.timezone.utc)
|
|
53
|
+
|
|
54
|
+
# Should not raise
|
|
55
|
+
_Strategy.verify_backtest_inputs(naive_start, aware_end)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_run_backtest_normalizes_mixed_timezones():
|
|
59
|
+
"""Strategy.run_backtest should normalize naive/aware datetimes before validation."""
|
|
60
|
+
naive_start = datetime.datetime(2025, 1, 1)
|
|
61
|
+
aware_end = datetime.datetime(2025, 9, 30, tzinfo=datetime.timezone.utc)
|
|
62
|
+
|
|
63
|
+
captured = {}
|
|
64
|
+
|
|
65
|
+
class CapturingDataSource(DummyDataSource):
|
|
66
|
+
def __init__(self, datetime_start=None, datetime_end=None, **kwargs):
|
|
67
|
+
super().__init__(datetime_start=datetime_start, datetime_end=datetime_end, **kwargs)
|
|
68
|
+
captured["start"] = self.datetime_start
|
|
69
|
+
captured["end"] = self.datetime_end
|
|
70
|
+
|
|
71
|
+
def broker_factory(data_source, *args, **kwargs):
|
|
72
|
+
captured["data_source"] = data_source
|
|
73
|
+
raise _EarlyExit
|
|
74
|
+
|
|
75
|
+
with patch("lumibot.strategies._strategy.BacktestingBroker", side_effect=broker_factory), \
|
|
76
|
+
patch("lumibot.strategies._strategy.Trader", DummyTrader):
|
|
77
|
+
with pytest.raises(_EarlyExit):
|
|
78
|
+
MinimalStrategy.run_backtest(
|
|
79
|
+
CapturingDataSource,
|
|
80
|
+
backtesting_start=naive_start,
|
|
81
|
+
backtesting_end=aware_end,
|
|
82
|
+
show_plot=False,
|
|
83
|
+
show_tearsheet=False,
|
|
84
|
+
show_indicators=False,
|
|
85
|
+
show_progress_bar=False,
|
|
86
|
+
save_logfile=False,
|
|
87
|
+
save_stats_file=False,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
assert "start" in captured and captured["start"].tzinfo is not None
|
|
91
|
+
assert "end" in captured and captured["end"].tzinfo is not None
|
|
92
|
+
assert captured["start"].tzinfo.zone == LUMIBOT_DEFAULT_PYTZ.zone
|
|
93
|
+
assert captured["end"].tzinfo.zone == LUMIBOT_DEFAULT_PYTZ.zone
|
|
94
|
+
assert captured["start"].tzinfo.zone == captured["end"].tzinfo.zone
|
|
File without changes
|
|
File without changes
|
|
File without changes
|