lumibot 4.0.23__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/__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.0.23.dist-info → lumibot-4.1.0.dist-info}/METADATA +1 -2
- {lumibot-4.0.23.dist-info → lumibot-4.1.0.dist-info}/RECORD +160 -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 +73 -86
- 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.0.dist-info}/LICENSE +0 -0
- {lumibot-4.0.23.dist-info → lumibot-4.1.0.dist-info}/WHEEL +0 -0
- {lumibot-4.0.23.dist-info → lumibot-4.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared utilities for handling DataBento continuous futures roll logic.
|
|
3
|
+
|
|
4
|
+
This module centralizes symbol resolution and roll schedule computation so that
|
|
5
|
+
both the pandas and polars implementations stay in sync.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
12
|
+
from typing import Callable, Dict, Iterable, List, Tuple
|
|
13
|
+
|
|
14
|
+
import pytz
|
|
15
|
+
|
|
16
|
+
from lumibot.constants import LUMIBOT_DEFAULT_PYTZ
|
|
17
|
+
from lumibot.entities import Asset
|
|
18
|
+
|
|
19
|
+
# Number of calendar days before expiration to roll into the next contract.
|
|
20
|
+
# This defaults to 7 (~5 business days) but can be overridden with an env var.
|
|
21
|
+
ROLL_DAYS_BEFORE_EXPIRATION = int(os.getenv("LUMIBOT_FUTURES_ROLL_DAYS", "7"))
|
|
22
|
+
|
|
23
|
+
# Caches used for symbol resolution so repeated lookups are cheap.
|
|
24
|
+
_DATETIME_NORMALIZATION_CACHE: Dict[float, datetime] = {}
|
|
25
|
+
_SYMBOL_RESOLUTION_CACHE: Dict[Tuple[str, str, float], str] = {}
|
|
26
|
+
|
|
27
|
+
NY_TZ = pytz.timezone("America/New_York")
|
|
28
|
+
UTC = timezone.utc
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _ensure_tz(dt: datetime) -> datetime:
|
|
32
|
+
"""Ensure a datetime is timezone-aware, defaulting to the platform TZ."""
|
|
33
|
+
if dt.tzinfo is None:
|
|
34
|
+
return LUMIBOT_DEFAULT_PYTZ.localize(dt)
|
|
35
|
+
return dt
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _normalize_reference_datetime(dt: datetime) -> datetime:
|
|
39
|
+
"""Normalize datetimes for use in caches when resolving symbols."""
|
|
40
|
+
if dt is None:
|
|
41
|
+
return dt
|
|
42
|
+
|
|
43
|
+
cache_key = dt.timestamp() if hasattr(dt, "timestamp") else None
|
|
44
|
+
if cache_key is not None and cache_key in _DATETIME_NORMALIZATION_CACHE:
|
|
45
|
+
return _DATETIME_NORMALIZATION_CACHE[cache_key]
|
|
46
|
+
|
|
47
|
+
if dt.tzinfo is not None:
|
|
48
|
+
normalized = dt.astimezone(LUMIBOT_DEFAULT_PYTZ).replace(tzinfo=None)
|
|
49
|
+
else:
|
|
50
|
+
normalized = dt
|
|
51
|
+
|
|
52
|
+
if cache_key is not None:
|
|
53
|
+
_DATETIME_NORMALIZATION_CACHE[cache_key] = normalized
|
|
54
|
+
|
|
55
|
+
return normalized
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_symbol_for_datetime(asset: Asset, dt: datetime) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Resolve the continuous futures symbol for a specific datetime using the
|
|
61
|
+
asset's roll rules.
|
|
62
|
+
"""
|
|
63
|
+
dt_norm = _normalize_reference_datetime(dt)
|
|
64
|
+
cache_key = (
|
|
65
|
+
asset.symbol,
|
|
66
|
+
asset.asset_type,
|
|
67
|
+
dt_norm.timestamp() if dt_norm is not None else float("inf"),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if cache_key in _SYMBOL_RESOLUTION_CACHE:
|
|
71
|
+
return _SYMBOL_RESOLUTION_CACHE[cache_key]
|
|
72
|
+
|
|
73
|
+
variants = asset.resolve_continuous_futures_contract_variants(reference_date=dt_norm)
|
|
74
|
+
contract = variants[2] # two-digit year variant
|
|
75
|
+
|
|
76
|
+
# DataBento prefers the short year format (single digit); reuse helper.
|
|
77
|
+
month_code = contract[len(asset.symbol)]
|
|
78
|
+
year_char = contract[-1]
|
|
79
|
+
resolved_symbol = f"{asset.symbol}{month_code}{year_char}"
|
|
80
|
+
|
|
81
|
+
_SYMBOL_RESOLUTION_CACHE[cache_key] = resolved_symbol
|
|
82
|
+
return resolved_symbol
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def resolve_symbols_for_range(asset: Asset, start: datetime, end: datetime) -> List[str]:
|
|
86
|
+
"""
|
|
87
|
+
Resolve the list of DataBento contract symbols required to cover a datetime range.
|
|
88
|
+
"""
|
|
89
|
+
if start is None or end is None:
|
|
90
|
+
return []
|
|
91
|
+
|
|
92
|
+
start_ref = _normalize_reference_datetime(start)
|
|
93
|
+
end_ref = _normalize_reference_datetime(end)
|
|
94
|
+
|
|
95
|
+
if start_ref is None or end_ref is None:
|
|
96
|
+
return [
|
|
97
|
+
resolve_symbol_for_datetime(asset, _ensure_tz(start)),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
symbols: List[str] = []
|
|
101
|
+
seen = set()
|
|
102
|
+
cursor = start_ref
|
|
103
|
+
step = timedelta(days=45) # ensures we hop across quarter rolls
|
|
104
|
+
|
|
105
|
+
while cursor <= end_ref + timedelta(days=45):
|
|
106
|
+
symbol = resolve_symbol_for_datetime(asset, cursor)
|
|
107
|
+
if symbol not in seen:
|
|
108
|
+
seen.add(symbol)
|
|
109
|
+
symbols.append(symbol)
|
|
110
|
+
cursor += step
|
|
111
|
+
|
|
112
|
+
# Ensure the last contract covers the end reference.
|
|
113
|
+
final_symbol = resolve_symbol_for_datetime(asset, end_ref)
|
|
114
|
+
if final_symbol not in seen:
|
|
115
|
+
symbols.append(final_symbol)
|
|
116
|
+
|
|
117
|
+
return symbols
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _parse_expiration(definition: Dict) -> datetime:
|
|
121
|
+
"""
|
|
122
|
+
Parse the expiration field from a DataBento instrument definition.
|
|
123
|
+
"""
|
|
124
|
+
expiration = (
|
|
125
|
+
definition.get("expiration")
|
|
126
|
+
or definition.get("maturity_date")
|
|
127
|
+
or definition.get("last_trade_date")
|
|
128
|
+
)
|
|
129
|
+
if expiration is None:
|
|
130
|
+
raise ValueError("Instrument definition missing expiration information")
|
|
131
|
+
|
|
132
|
+
if isinstance(expiration, datetime):
|
|
133
|
+
dt_local = expiration
|
|
134
|
+
else:
|
|
135
|
+
expiration_str = str(expiration)
|
|
136
|
+
# Handle ISO strings with optional timezone offset.
|
|
137
|
+
if "T" in expiration_str:
|
|
138
|
+
expiration_str = expiration_str.replace("Z", "+00:00")
|
|
139
|
+
dt_local = datetime.fromisoformat(expiration_str)
|
|
140
|
+
else:
|
|
141
|
+
dt_local = datetime.strptime(expiration_str, "%Y-%m-%d")
|
|
142
|
+
|
|
143
|
+
if dt_local.tzinfo is None:
|
|
144
|
+
dt_local = NY_TZ.localize(dt_local)
|
|
145
|
+
else:
|
|
146
|
+
dt_local = dt_local.astimezone(NY_TZ)
|
|
147
|
+
|
|
148
|
+
# Futures generally stop trading in the afternoon; rolling on midnight is fine.
|
|
149
|
+
return dt_local
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def build_roll_schedule(
|
|
153
|
+
asset: Asset,
|
|
154
|
+
start: datetime,
|
|
155
|
+
end: datetime,
|
|
156
|
+
definition_provider: Callable[[str], Dict],
|
|
157
|
+
roll_days: int = ROLL_DAYS_BEFORE_EXPIRATION,
|
|
158
|
+
) -> List[Tuple[str, datetime, datetime]]:
|
|
159
|
+
"""
|
|
160
|
+
Build a list of (symbol, start_utc, end_utc) windows indicating which contract
|
|
161
|
+
should be used at each point in time.
|
|
162
|
+
"""
|
|
163
|
+
if roll_days < 0:
|
|
164
|
+
raise ValueError("roll_days must be non-negative")
|
|
165
|
+
|
|
166
|
+
start = _ensure_tz(start)
|
|
167
|
+
end = _ensure_tz(end)
|
|
168
|
+
symbols = resolve_symbols_for_range(asset, start, end)
|
|
169
|
+
|
|
170
|
+
if not symbols:
|
|
171
|
+
return []
|
|
172
|
+
|
|
173
|
+
schedule: List[Tuple[str, datetime, datetime]] = []
|
|
174
|
+
current_start = datetime.min.replace(tzinfo=UTC)
|
|
175
|
+
|
|
176
|
+
for idx, symbol in enumerate(symbols):
|
|
177
|
+
definition = definition_provider(symbol)
|
|
178
|
+
if not definition:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
expiration_local = _parse_expiration(definition)
|
|
182
|
+
roll_local = expiration_local - timedelta(days=roll_days)
|
|
183
|
+
roll_local = max(roll_local, start)
|
|
184
|
+
roll_utc = roll_local.astimezone(UTC)
|
|
185
|
+
|
|
186
|
+
if idx < len(symbols) - 1:
|
|
187
|
+
end_utc = roll_utc
|
|
188
|
+
else:
|
|
189
|
+
end_utc = datetime.max.replace(tzinfo=UTC)
|
|
190
|
+
|
|
191
|
+
schedule.append((symbol, current_start, end_utc))
|
|
192
|
+
current_start = roll_utc
|
|
193
|
+
|
|
194
|
+
if not schedule:
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
start_utc = start.astimezone(UTC)
|
|
198
|
+
end_utc = end.astimezone(UTC)
|
|
199
|
+
|
|
200
|
+
clipped: List[Tuple[str, datetime, datetime]] = []
|
|
201
|
+
for symbol, window_start, window_end in schedule:
|
|
202
|
+
s = max(window_start, start_utc)
|
|
203
|
+
e = min(window_end, end_utc)
|
|
204
|
+
if e <= s:
|
|
205
|
+
continue
|
|
206
|
+
clipped.append((symbol, s, e))
|
|
207
|
+
|
|
208
|
+
if not clipped:
|
|
209
|
+
clipped.append((schedule[-1][0], start_utc, end_utc))
|
|
210
|
+
else:
|
|
211
|
+
last_symbol, s, e = clipped[-1]
|
|
212
|
+
if e < end_utc:
|
|
213
|
+
clipped[-1] = (last_symbol, s, end_utc)
|
|
214
|
+
|
|
215
|
+
return clipped
|
|
216
|
+
|
lumibot/tools/lumibot_logger.py
CHANGED
|
@@ -656,11 +656,18 @@ def _ensure_handlers_configured():
|
|
|
656
656
|
|
|
657
657
|
# Determine the effective file (root) log level and console level
|
|
658
658
|
if is_backtesting:
|
|
659
|
-
console_level = logging.ERROR
|
|
660
659
|
backtesting_quiet = os.environ.get("BACKTESTING_QUIET_LOGS")
|
|
661
660
|
if backtesting_quiet is None:
|
|
662
661
|
backtesting_quiet = "true"
|
|
663
|
-
|
|
662
|
+
|
|
663
|
+
if backtesting_quiet.lower() == "true":
|
|
664
|
+
# Quiet mode: only ERROR+ messages to console and file
|
|
665
|
+
console_level = logging.ERROR
|
|
666
|
+
effective_log_level = logging.ERROR
|
|
667
|
+
else:
|
|
668
|
+
# Verbose mode: respect LUMIBOT_LOG_LEVEL for both console and file
|
|
669
|
+
console_level = log_level
|
|
670
|
+
effective_log_level = log_level
|
|
664
671
|
else:
|
|
665
672
|
console_level = log_level
|
|
666
673
|
effective_log_level = log_level
|
|
@@ -851,27 +858,35 @@ def set_log_level(level: str):
|
|
|
851
858
|
# Get the actual lumibot root logger
|
|
852
859
|
root_logger = logging.getLogger("lumibot")
|
|
853
860
|
root_logger.setLevel(log_level)
|
|
854
|
-
|
|
855
|
-
# Update handlers
|
|
861
|
+
|
|
862
|
+
# Update handlers with respect to backtesting quiet logs setting
|
|
856
863
|
is_backtesting = os.environ.get("IS_BACKTESTING", "").lower() == "true"
|
|
857
|
-
|
|
864
|
+
|
|
858
865
|
if is_backtesting:
|
|
859
|
-
#
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
866
|
+
# Check if quiet logs are enabled
|
|
867
|
+
backtesting_quiet = os.environ.get("BACKTESTING_QUIET_LOGS")
|
|
868
|
+
if backtesting_quiet is None:
|
|
869
|
+
backtesting_quiet = "true"
|
|
870
|
+
|
|
871
|
+
if backtesting_quiet.lower() == "true":
|
|
872
|
+
# Quiet mode: console stays at ERROR, but allow file handlers to use requested level
|
|
873
|
+
root_logger.setLevel(log_level)
|
|
874
|
+
for handler in root_logger.handlers:
|
|
875
|
+
if isinstance(handler, logging.StreamHandler) and not isinstance(handler, logging.FileHandler):
|
|
876
|
+
handler.setLevel(logging.ERROR) # Console: quiet
|
|
877
|
+
else:
|
|
878
|
+
handler.setLevel(log_level) # File handlers: verbose
|
|
879
|
+
else:
|
|
880
|
+
# Verbose mode: respect requested level for all handlers
|
|
881
|
+
root_logger.setLevel(log_level)
|
|
882
|
+
for handler in root_logger.handlers:
|
|
883
|
+
handler.setLevel(log_level)
|
|
869
884
|
else:
|
|
870
|
-
#
|
|
885
|
+
# Live trading: set everything normally
|
|
871
886
|
root_logger.setLevel(log_level)
|
|
872
887
|
for handler in root_logger.handlers:
|
|
873
888
|
handler.setLevel(log_level)
|
|
874
|
-
|
|
889
|
+
|
|
875
890
|
# Update all existing loggers in our registry
|
|
876
891
|
for logger in _logger_registry.values():
|
|
877
892
|
logger.setLevel(log_level)
|
lumibot/tools/polygon_helper.py
CHANGED
|
@@ -142,6 +142,35 @@ def get_price_data_from_polygon(
|
|
|
142
142
|
if not missing_dates:
|
|
143
143
|
if df_all is not None:
|
|
144
144
|
df_all = df_all.dropna(how="all")
|
|
145
|
+
# Filter cached data to requested date range before returning
|
|
146
|
+
if not df_all.empty:
|
|
147
|
+
# For daily data, use date-based filtering (timestamps vary by provider)
|
|
148
|
+
# For intraday data, use precise datetime filtering
|
|
149
|
+
if timespan == "day":
|
|
150
|
+
# Convert index to dates for comparison
|
|
151
|
+
import pandas as pd
|
|
152
|
+
df_dates = pd.to_datetime(df_all.index).date
|
|
153
|
+
start_date = start.date() if hasattr(start, 'date') else start
|
|
154
|
+
end_date = end.date() if hasattr(end, 'date') else end
|
|
155
|
+
mask = (df_dates >= start_date) & (df_dates <= end_date)
|
|
156
|
+
df_all = df_all[mask]
|
|
157
|
+
else:
|
|
158
|
+
# Intraday: use precise datetime filtering
|
|
159
|
+
import datetime as dt
|
|
160
|
+
import pytz
|
|
161
|
+
from lumibot import LUMIBOT_DEFAULT_PYTZ
|
|
162
|
+
|
|
163
|
+
# Convert date to datetime if needed
|
|
164
|
+
if isinstance(start, dt.date) and not isinstance(start, dt.datetime):
|
|
165
|
+
start = dt.datetime.combine(start, dt.time.min)
|
|
166
|
+
if isinstance(end, dt.date) and not isinstance(end, dt.datetime):
|
|
167
|
+
end = dt.datetime.combine(end, dt.time.max)
|
|
168
|
+
|
|
169
|
+
if start.tzinfo is None:
|
|
170
|
+
start = LUMIBOT_DEFAULT_PYTZ.localize(start).astimezone(pytz.UTC)
|
|
171
|
+
if end.tzinfo is None:
|
|
172
|
+
end = LUMIBOT_DEFAULT_PYTZ.localize(end).astimezone(pytz.UTC)
|
|
173
|
+
df_all = df_all[(df_all.index >= start) & (df_all.index <= end)]
|
|
145
174
|
return df_all
|
|
146
175
|
|
|
147
176
|
# Create a PolygonClient and get the symbol for the asset.
|
|
@@ -209,6 +238,42 @@ def get_price_data_from_polygon(
|
|
|
209
238
|
else:
|
|
210
239
|
df_all_output = df_all_full.copy()
|
|
211
240
|
df_all_output = df_all_output.dropna(how="all")
|
|
241
|
+
|
|
242
|
+
# Filter cached data to requested date range before returning
|
|
243
|
+
if not df_all_output.empty:
|
|
244
|
+
# For daily data, use date-based filtering (timestamps vary by provider)
|
|
245
|
+
# For intraday data, use precise datetime filtering
|
|
246
|
+
if timespan == "day":
|
|
247
|
+
# Convert index to dates for comparison
|
|
248
|
+
import pandas as pd
|
|
249
|
+
df_dates = pd.to_datetime(df_all_output.index).date
|
|
250
|
+
start_date = start.date() if hasattr(start, 'date') else start
|
|
251
|
+
end_date = end.date() if hasattr(end, 'date') else end
|
|
252
|
+
mask = (df_dates >= start_date) & (df_dates <= end_date)
|
|
253
|
+
df_all_output = df_all_output[mask]
|
|
254
|
+
else:
|
|
255
|
+
# Intraday: use precise datetime filtering
|
|
256
|
+
import datetime as dt
|
|
257
|
+
import pytz
|
|
258
|
+
from lumibot import LUMIBOT_DEFAULT_PYTZ
|
|
259
|
+
|
|
260
|
+
# Convert date to datetime if needed
|
|
261
|
+
if isinstance(start, dt.date) and not isinstance(start, dt.datetime):
|
|
262
|
+
start = dt.datetime.combine(start, dt.time.min)
|
|
263
|
+
if isinstance(end, dt.date) and not isinstance(end, dt.datetime):
|
|
264
|
+
end = dt.datetime.combine(end, dt.time.max)
|
|
265
|
+
|
|
266
|
+
# Handle datetime objects with midnight time (users often pass datetime(YYYY, MM, DD))
|
|
267
|
+
if isinstance(end, dt.datetime) and end.time() == dt.time.min:
|
|
268
|
+
# Convert end-of-period midnight to end-of-day
|
|
269
|
+
end = dt.datetime.combine(end.date(), dt.time.max)
|
|
270
|
+
|
|
271
|
+
if start.tzinfo is None:
|
|
272
|
+
start = LUMIBOT_DEFAULT_PYTZ.localize(start).astimezone(pytz.UTC)
|
|
273
|
+
if end.tzinfo is None:
|
|
274
|
+
end = LUMIBOT_DEFAULT_PYTZ.localize(end).astimezone(pytz.UTC)
|
|
275
|
+
df_all_output = df_all_output[(df_all_output.index >= start) & (df_all_output.index <= end)]
|
|
276
|
+
|
|
212
277
|
return df_all_output
|
|
213
278
|
|
|
214
279
|
|