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.

Files changed (161) hide show
  1. lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
  2. lumibot/__pycache__/constants.cpython-312.pyc +0 -0
  3. lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
  4. lumibot/backtesting/__init__.py +6 -5
  5. lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
  6. lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
  7. lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
  8. lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
  9. lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
  10. lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
  11. lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
  12. lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
  13. lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
  14. lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
  15. lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
  16. lumibot/backtesting/backtesting_broker.py +209 -9
  17. lumibot/backtesting/databento_backtesting.py +145 -24
  18. lumibot/backtesting/thetadata_backtesting.py +63 -42
  19. lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
  20. lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
  21. lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
  22. lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
  23. lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
  24. lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
  25. lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
  26. lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
  27. lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
  28. lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
  29. lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
  30. lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
  31. lumibot/brokers/alpaca.py +11 -1
  32. lumibot/brokers/tradeovate.py +475 -0
  33. lumibot/components/grok_news_helper.py +284 -0
  34. lumibot/components/options_helper.py +90 -34
  35. lumibot/credentials.py +3 -0
  36. lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
  37. lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
  38. lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
  39. lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
  40. lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
  41. lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
  42. lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
  43. lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
  44. lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
  45. lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
  46. lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
  47. lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
  48. lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
  49. lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
  50. lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
  51. lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
  52. lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
  53. lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
  54. lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
  55. lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
  56. lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
  57. lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
  58. lumibot/data_sources/data_source_backtesting.py +3 -5
  59. lumibot/data_sources/databento_data_polars_backtesting.py +194 -48
  60. lumibot/data_sources/pandas_data.py +6 -3
  61. lumibot/data_sources/polars_mixin.py +126 -21
  62. lumibot/data_sources/tradeovate_data.py +80 -0
  63. lumibot/data_sources/tradier_data.py +2 -1
  64. lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
  65. lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
  66. lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
  67. lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
  68. lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
  69. lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
  70. lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
  71. lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
  72. lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
  73. lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
  74. lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
  75. lumibot/entities/asset.py +8 -0
  76. lumibot/entities/order.py +1 -1
  77. lumibot/entities/quote.py +14 -0
  78. lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  79. lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
  80. lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  81. lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
  82. lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
  83. lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
  84. lumibot/strategies/_strategy.py +95 -27
  85. lumibot/strategies/strategy.py +5 -6
  86. lumibot/strategies/strategy_executor.py +2 -2
  87. lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  88. lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
  89. lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
  90. lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
  91. lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
  92. lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
  93. lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
  94. lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
  95. lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
  96. lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
  97. lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
  98. lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
  99. lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
  100. lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
  101. lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
  102. lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
  103. lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
  104. lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
  105. lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
  106. lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
  107. lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
  108. lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
  109. lumibot/tools/databento_helper.py +384 -133
  110. lumibot/tools/databento_helper_polars.py +218 -156
  111. lumibot/tools/databento_roll.py +216 -0
  112. lumibot/tools/lumibot_logger.py +32 -17
  113. lumibot/tools/polygon_helper.py +65 -0
  114. lumibot/tools/thetadata_helper.py +588 -70
  115. lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
  116. lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
  117. lumibot/traders/trader.py +1 -1
  118. lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
  119. lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
  120. lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
  121. lumibot-4.1.1.data/data/ThetaTerminal.jar +0 -0
  122. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/METADATA +1 -2
  123. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/RECORD +161 -44
  124. tests/backtest/check_timing_offset.py +198 -0
  125. tests/backtest/check_volume_spike.py +112 -0
  126. tests/backtest/comprehensive_comparison.py +166 -0
  127. tests/backtest/debug_comparison.py +91 -0
  128. tests/backtest/diagnose_price_difference.py +97 -0
  129. tests/backtest/direct_api_comparison.py +203 -0
  130. tests/backtest/profile_thetadata_vs_polygon.py +255 -0
  131. tests/backtest/root_cause_analysis.py +109 -0
  132. tests/backtest/test_accuracy_verification.py +244 -0
  133. tests/backtest/test_daily_data_timestamp_comparison.py +801 -0
  134. tests/backtest/test_databento.py +4 -0
  135. tests/backtest/test_databento_comprehensive_trading.py +564 -0
  136. tests/backtest/test_debug_avg_fill_price.py +112 -0
  137. tests/backtest/test_dividends.py +8 -3
  138. tests/backtest/test_example_strategies.py +54 -47
  139. tests/backtest/test_futures_edge_cases.py +451 -0
  140. tests/backtest/test_futures_single_trade.py +270 -0
  141. tests/backtest/test_futures_ultra_simple.py +191 -0
  142. tests/backtest/test_index_data_verification.py +348 -0
  143. tests/backtest/test_polygon.py +45 -24
  144. tests/backtest/test_thetadata.py +246 -60
  145. tests/backtest/test_thetadata_comprehensive.py +729 -0
  146. tests/backtest/test_thetadata_vs_polygon.py +557 -0
  147. tests/backtest/test_yahoo.py +1 -2
  148. tests/conftest.py +20 -0
  149. tests/test_backtesting_data_source_env.py +249 -0
  150. tests/test_backtesting_quiet_logs_complete.py +10 -11
  151. tests/test_databento_helper.py +76 -90
  152. tests/test_databento_timezone_fixes.py +21 -4
  153. tests/test_get_historical_prices.py +6 -6
  154. tests/test_options_helper.py +162 -40
  155. tests/test_polygon_helper.py +21 -13
  156. tests/test_quiet_logs_requirements.py +5 -5
  157. tests/test_thetadata_helper.py +487 -171
  158. tests/test_yahoo_data.py +125 -0
  159. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/LICENSE +0 -0
  160. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/WHEEL +0 -0
  161. {lumibot-4.0.23.dist-info → lumibot-4.1.1.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
+
@@ -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
- effective_log_level = logging.ERROR if backtesting_quiet.lower() == "true" else log_level
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 but respect console handler's ERROR level during backtesting
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
- # During backtesting, we need special handling to ensure console stays quiet
860
- # Set root logger to allow messages through (for file logging)
861
- root_logger.setLevel(log_level)
862
-
863
- # But ensure ALL console handlers stay at ERROR level
864
- for handler in root_logger.handlers:
865
- if isinstance(handler, logging.StreamHandler):
866
- handler.setLevel(logging.ERROR)
867
-
868
- # Don't update individual logger levels - this would bypass handler filtering
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
- # For live trading, set everything normally
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)
@@ -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