lumibot 4.2.0__py3-none-any.whl → 4.2.2__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.

@@ -184,7 +184,7 @@ class ThetaDataBacktestingPandas(PandasData):
184
184
  asset: Optional[Asset] = None, # DEBUG-LOG: Added for logging
185
185
  ) -> Optional[pd.DataFrame]:
186
186
  # DEBUG-LOG: Method entry with full parameter context
187
- logger.info(
187
+ logger.debug(
188
188
  "[THETA][DEBUG][PANDAS][FINALIZE][ENTRY] asset=%s current_dt=%s requested_length=%s timeshift=%s input_shape=%s input_columns=%s input_index_type=%s input_has_tz=%s input_index_sample=%s",
189
189
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN',
190
190
  current_dt.isoformat() if hasattr(current_dt, 'isoformat') else current_dt,
@@ -199,7 +199,7 @@ class ThetaDataBacktestingPandas(PandasData):
199
199
 
200
200
  if pandas_df is None or pandas_df.empty:
201
201
  # DEBUG-LOG: Early return for empty input
202
- logger.info(
202
+ logger.debug(
203
203
  "[THETA][DEBUG][PANDAS][FINALIZE][EMPTY_INPUT] asset=%s returning_none_or_empty=True",
204
204
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN'
205
205
  )
@@ -212,7 +212,7 @@ class ThetaDataBacktestingPandas(PandasData):
212
212
  frame.index = pd.to_datetime(frame.index)
213
213
 
214
214
  # DEBUG-LOG: Timezone state before localization
215
- logger.info(
215
+ logger.debug(
216
216
  "[THETA][DEBUG][PANDAS][FINALIZE][TZ_CHECK] asset=%s frame_index_tz=%s target_tz=%s needs_localization=%s frame_shape=%s",
217
217
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN',
218
218
  frame.index.tz,
@@ -227,7 +227,7 @@ class ThetaDataBacktestingPandas(PandasData):
227
227
  normalized_for_cutoff = localized_index.normalize()
228
228
 
229
229
  # DEBUG-LOG: After localization
230
- logger.info(
230
+ logger.debug(
231
231
  "[THETA][DEBUG][PANDAS][FINALIZE][LOCALIZED] asset=%s localized_index_tz=%s localized_sample=%s",
232
232
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN',
233
233
  localized_index.tz,
@@ -238,7 +238,7 @@ class ThetaDataBacktestingPandas(PandasData):
238
238
  cutoff_mask = normalized_for_cutoff <= cutoff
239
239
 
240
240
  # DEBUG-LOG: Cutoff filtering state
241
- logger.info(
241
+ logger.debug(
242
242
  "[THETA][DEBUG][PANDAS][FINALIZE][CUTOFF] asset=%s cutoff=%s cutoff_mask_true=%s cutoff_mask_false=%s",
243
243
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN',
244
244
  cutoff,
@@ -249,7 +249,7 @@ class ThetaDataBacktestingPandas(PandasData):
249
249
  if timeshift and not isinstance(timeshift, int):
250
250
  cutoff_mask &= normalized_for_cutoff <= (cutoff - timeshift)
251
251
  # DEBUG-LOG: After timeshift adjustment
252
- logger.info(
252
+ logger.debug(
253
253
  "[THETA][DEBUG][PANDAS][FINALIZE][TIMESHIFT_ADJUSTED] asset=%s timeshift=%s new_cutoff=%s cutoff_mask_true=%s",
254
254
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN',
255
255
  timeshift,
@@ -262,7 +262,7 @@ class ThetaDataBacktestingPandas(PandasData):
262
262
  normalized_for_cutoff = normalized_for_cutoff[cutoff_mask]
263
263
 
264
264
  # DEBUG-LOG: After cutoff filtering
265
- logger.info(
265
+ logger.debug(
266
266
  "[THETA][DEBUG][PANDAS][FINALIZE][AFTER_CUTOFF] asset=%s shape=%s index_range=%s",
267
267
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN',
268
268
  frame.shape,
@@ -280,7 +280,7 @@ class ThetaDataBacktestingPandas(PandasData):
280
280
  raw_frame = frame.copy()
281
281
 
282
282
  # DEBUG-LOG: After normalization
283
- logger.info(
283
+ logger.debug(
284
284
  "[THETA][DEBUG][PANDAS][FINALIZE][NORMALIZED_INDEX] asset=%s shape=%s index_sample=%s",
285
285
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN',
286
286
  frame.shape,
@@ -291,7 +291,7 @@ class ThetaDataBacktestingPandas(PandasData):
291
291
  target_index = pd.date_range(end=expected_last_dt, periods=requested_length, freq="D", tz=self.tzinfo)
292
292
 
293
293
  # DEBUG-LOG: Target index details
294
- logger.info(
294
+ logger.debug(
295
295
  "[THETA][DEBUG][PANDAS][FINALIZE][TARGET_INDEX] asset=%s target_length=%s target_range=%s",
296
296
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN',
297
297
  len(target_index),
@@ -304,7 +304,7 @@ class ThetaDataBacktestingPandas(PandasData):
304
304
  frame = frame.reindex(target_index)
305
305
 
306
306
  # DEBUG-LOG: After reindex
307
- logger.info(
307
+ logger.debug(
308
308
  "[THETA][DEBUG][PANDAS][FINALIZE][AFTER_REINDEX] asset=%s shape=%s columns=%s",
309
309
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN',
310
310
  frame.shape,
@@ -318,7 +318,7 @@ class ThetaDataBacktestingPandas(PandasData):
318
318
  placeholder_mask = frame.isna().all(axis=1)
319
319
 
320
320
  # DEBUG-LOG: Placeholder mask computation
321
- logger.info(
321
+ logger.debug(
322
322
  "[THETA][DEBUG][PANDAS][FINALIZE][PLACEHOLDER_MASK] asset=%s placeholder_true=%s placeholder_false=%s value_columns=%s",
323
323
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN',
324
324
  int(placeholder_mask.sum()) if hasattr(placeholder_mask, 'sum') else 'N/A',
@@ -359,7 +359,7 @@ class ThetaDataBacktestingPandas(PandasData):
359
359
  # DEBUG-LOG: Final missing flag state
360
360
  try:
361
361
  missing_count = int(frame["missing"].sum())
362
- logger.info(
362
+ logger.debug(
363
363
  "[THETA][DEBUG][PANDAS][FINALIZE][MISSING_FINAL] asset=%s missing_true=%s missing_false=%s total_rows=%s",
364
364
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN',
365
365
  missing_count,
@@ -367,14 +367,14 @@ class ThetaDataBacktestingPandas(PandasData):
367
367
  len(frame)
368
368
  )
369
369
  except Exception as e:
370
- logger.info(
370
+ logger.debug(
371
371
  "[THETA][DEBUG][PANDAS][FINALIZE][MISSING_FINAL] asset=%s error=%s",
372
372
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN',
373
373
  str(e)
374
374
  )
375
375
 
376
376
  # DEBUG-LOG: Return value
377
- logger.info(
377
+ logger.debug(
378
378
  "[THETA][DEBUG][PANDAS][FINALIZE][RETURN] asset=%s shape=%s columns=%s index_range=%s",
379
379
  getattr(asset, 'symbol', asset) if asset else 'UNKNOWN',
380
380
  frame.shape,
@@ -451,7 +451,7 @@ class ThetaDataBacktestingPandas(PandasData):
451
451
  existing_end = existing_meta.get("end")
452
452
 
453
453
  # DEBUG-LOG: Cache validation entry
454
- logger.info(
454
+ logger.debug(
455
455
  "[DEBUG][BACKTEST][THETA][DEBUG][PANDAS][CACHE_VALIDATION][ENTRY] asset=%s timestep=%s | "
456
456
  "REQUESTED: start=%s start_threshold=%s end_requirement=%s length=%d | "
457
457
  "EXISTING: start=%s end=%s rows=%d",
@@ -472,7 +472,7 @@ class ThetaDataBacktestingPandas(PandasData):
472
472
  )
473
473
 
474
474
  # DEBUG-LOG: Start validation result
475
- logger.info(
475
+ logger.debug(
476
476
  "[DEBUG][BACKTEST][THETA][DEBUG][PANDAS][START_VALIDATION] asset=%s | "
477
477
  "start_ok=%s | "
478
478
  "existing_start=%s start_threshold=%s | "
@@ -489,7 +489,7 @@ class ThetaDataBacktestingPandas(PandasData):
489
489
  end_ok = True
490
490
 
491
491
  # DEBUG-LOG: End validation entry
492
- logger.info(
492
+ logger.debug(
493
493
  "[DEBUG][BACKTEST][THETA][DEBUG][PANDAS][END_VALIDATION][ENTRY] asset=%s | "
494
494
  "end_requirement=%s existing_end=%s tail_placeholder=%s",
495
495
  asset_separated.symbol if hasattr(asset_separated, 'symbol') else str(asset_separated),
@@ -501,7 +501,7 @@ class ThetaDataBacktestingPandas(PandasData):
501
501
  if end_requirement is not None:
502
502
  if existing_end is None:
503
503
  end_ok = False
504
- logger.info(
504
+ logger.debug(
505
505
  "[DEBUG][BACKTEST][THETA][DEBUG][PANDAS][END_VALIDATION][RESULT] asset=%s | "
506
506
  "end_ok=FALSE | reason=existing_end_is_None",
507
507
  asset_separated.symbol if hasattr(asset_separated, 'symbol') else str(asset_separated)
@@ -520,7 +520,7 @@ class ThetaDataBacktestingPandas(PandasData):
520
520
 
521
521
  if existing_end_cmp > end_requirement_cmp:
522
522
  end_ok = True
523
- logger.info(
523
+ logger.debug(
524
524
  "[DEBUG][BACKTEST][THETA][DEBUG][PANDAS][END_VALIDATION][RESULT] asset=%s | "
525
525
  "end_ok=TRUE | reason=existing_end_exceeds_requirement | "
526
526
  "existing_end=%s end_requirement=%s ts_unit=%s",
@@ -535,7 +535,7 @@ class ThetaDataBacktestingPandas(PandasData):
535
535
  placeholder_empty_fetch = tail_placeholder and existing_meta.get("empty_fetch")
536
536
  end_ok = (not tail_placeholder) or placeholder_on_weekend or placeholder_empty_fetch
537
537
 
538
- logger.info(
538
+ logger.debug(
539
539
  "[DEBUG][BACKTEST][THETA][DEBUG][PANDAS][END_VALIDATION][EXACT_MATCH] asset=%s | "
540
540
  "existing_end == end_requirement | "
541
541
  "weekday=%s placeholder_on_weekend=%s placeholder_empty_fetch=%s | "
@@ -549,7 +549,7 @@ class ThetaDataBacktestingPandas(PandasData):
549
549
  )
550
550
  else:
551
551
  end_ok = False
552
- logger.info(
552
+ logger.debug(
553
553
  "[DEBUG][BACKTEST][THETA][DEBUG][PANDAS][END_VALIDATION][RESULT] asset=%s | "
554
554
  "end_ok=FALSE | reason=existing_end_less_than_requirement | "
555
555
  "existing_end=%s end_requirement=%s ts_unit=%s",
@@ -566,7 +566,7 @@ class ThetaDataBacktestingPandas(PandasData):
566
566
  )
567
567
 
568
568
  # DEBUG-LOG: Final cache decision
569
- logger.info(
569
+ logger.debug(
570
570
  "[DEBUG][BACKTEST][THETA][DEBUG][PANDAS][CACHE_DECISION] asset=%s | "
571
571
  "cache_covers=%s | "
572
572
  "start_ok=%s rows_ok=%s (existing=%d >= requested=%d) end_ok=%s",
@@ -586,7 +586,7 @@ class ThetaDataBacktestingPandas(PandasData):
586
586
  and expiration_dt == end_requirement
587
587
  and not existing_meta.get("expiration_notice")
588
588
  ):
589
- logger.info(
589
+ logger.debug(
590
590
  "[THETA][DEBUG][THETADATA-PANDAS] Reusing cached data for %s/%s through option expiry %s.",
591
591
  asset_separated,
592
592
  quote_asset,
@@ -702,7 +702,7 @@ class ThetaDataBacktestingPandas(PandasData):
702
702
  and expiration_dt == end_requirement
703
703
  )
704
704
  if expired_reason:
705
- logger.info(
705
+ logger.debug(
706
706
  "[THETA][DEBUG][THETADATA-PANDAS] No new OHLC rows for %s/%s (%s); option expired on %s. Keeping cached data.",
707
707
  asset_separated,
708
708
  quote_asset,
@@ -851,9 +851,8 @@ class ThetaDataBacktestingPandas(PandasData):
851
851
  bars = self._parse_source_symbol_bars(response, asset, quote=quote)
852
852
  final_df = getattr(bars, "df", None)
853
853
  final_rows = len(final_df) if final_df is not None else 0
854
- message = (
855
- "[THETA][DEBUG][FETCH][THETA][DEBUG][PANDAS][FINAL] asset=%s quote=%s length=%s timestep=%s timeshift=%s current_dt=%s rows=%s"
856
- ) % (
854
+ logger.debug(
855
+ "[THETA][DEBUG][FETCH][THETA][DEBUG][PANDAS][FINAL] asset=%s quote=%s length=%s timestep=%s timeshift=%s current_dt=%s rows=%s",
857
856
  getattr(asset, "symbol", asset) if not isinstance(asset, str) else asset,
858
857
  getattr(quote, "symbol", quote),
859
858
  length,
@@ -862,8 +861,6 @@ class ThetaDataBacktestingPandas(PandasData):
862
861
  current_dt,
863
862
  final_rows,
864
863
  )
865
- logger.warning(message)
866
- print(message)
867
864
  return bars
868
865
 
869
866
  def get_last_price(self, asset, timestep="minute", quote=None, exchange=None, **kwargs) -> Union[float, Decimal, None]:
@@ -893,7 +890,7 @@ class ThetaDataBacktestingPandas(PandasData):
893
890
  return super().get_last_price(asset=asset, quote=quote, exchange=exchange)
894
891
  closes = close_series.dropna()
895
892
  if closes.empty:
896
- logger.warning(
893
+ logger.debug(
897
894
  "[THETA][DEBUG][THETADATA-PANDAS] get_last_price found no valid closes for %s/%s; returning None (likely expired).",
898
895
  asset,
899
896
  quote or Asset("USD", "forex"),
@@ -957,10 +954,9 @@ class ThetaDataBacktestingPandas(PandasData):
957
954
  return_polars=False,
958
955
  )
959
956
  if bars is None or getattr(bars, "df", None) is None or bars.df.empty:
960
- message = (
957
+ logger.debug(
961
958
  "[THETA][DEBUG][FETCH][THETA][DEBUG][PANDAS] asset=%s quote=%s length=%s timestep=%s timeshift=%s current_dt=%s "
962
- "rows=0 first_ts=None last_ts=None columns=None"
963
- ) % (
959
+ "rows=0 first_ts=None last_ts=None columns=None",
964
960
  getattr(asset, "symbol", asset) if not isinstance(asset, str) else asset,
965
961
  getattr(quote, "symbol", quote),
966
962
  length,
@@ -968,8 +964,6 @@ class ThetaDataBacktestingPandas(PandasData):
968
964
  timeshift,
969
965
  current_dt,
970
966
  )
971
- logger.warning(message)
972
- print(message)
973
967
  return bars
974
968
 
975
969
  df = bars.df
@@ -981,10 +975,10 @@ class ThetaDataBacktestingPandas(PandasData):
981
975
  else:
982
976
  first_ts = df.index[0]
983
977
  last_ts = df.index[-1]
984
- message = (
978
+
979
+ logger.debug(
985
980
  "[THETA][DEBUG][FETCH][THETA][DEBUG][PANDAS] asset=%s quote=%s length=%s timestep=%s timeshift=%s current_dt=%s rows=%s "
986
- "first_ts=%s last_ts=%s columns=%s"
987
- ) % (
981
+ "first_ts=%s last_ts=%s columns=%s",
988
982
  getattr(asset, "symbol", asset) if not isinstance(asset, str) else asset,
989
983
  getattr(quote, "symbol", quote),
990
984
  length,
@@ -996,8 +990,6 @@ class ThetaDataBacktestingPandas(PandasData):
996
990
  last_ts,
997
991
  columns,
998
992
  )
999
- logger.warning(message)
1000
- print(message)
1001
993
  return bars
1002
994
 
1003
995
  def get_quote(self, asset, timestep="minute", quote=None, exchange=None, **kwargs):
@@ -1026,7 +1018,7 @@ class ThetaDataBacktestingPandas(PandasData):
1026
1018
 
1027
1019
  # [INSTRUMENTATION] Log full asset details for options
1028
1020
  if hasattr(asset, 'asset_type') and asset.asset_type == Asset.AssetType.OPTION:
1029
- logger.info(
1021
+ logger.debug(
1030
1022
  "[THETA][DEBUG][QUOTE][THETA][DEBUG][PANDAS][OPTION_REQUEST] symbol=%s expiration=%s strike=%s right=%s current_dt=%s timestep=%s",
1031
1023
  asset.symbol,
1032
1024
  asset.expiration,
@@ -1036,7 +1028,7 @@ class ThetaDataBacktestingPandas(PandasData):
1036
1028
  timestep
1037
1029
  )
1038
1030
  else:
1039
- logger.info(
1031
+ logger.debug(
1040
1032
  "[THETA][DEBUG][QUOTE][THETA][DEBUG][PANDAS][REQUEST] asset=%s current_dt=%s timestep=%s",
1041
1033
  getattr(asset, "symbol", asset) if not isinstance(asset, str) else asset,
1042
1034
  dt.isoformat() if hasattr(dt, 'isoformat') else dt,
@@ -1066,7 +1058,7 @@ class ThetaDataBacktestingPandas(PandasData):
1066
1058
  if isinstance(df.index, pd.DatetimeIndex) and df.index.tz is not None:
1067
1059
  tz_info = str(df.index.tz)
1068
1060
 
1069
- logger.info(
1061
+ logger.debug(
1070
1062
  "[THETA][DEBUG][QUOTE][THETA][DEBUG][PANDAS][DATAFRAME_STATE] asset=%s | total_rows=%d | timestep=%s | index_type=%s | timezone=%s",
1071
1063
  getattr(asset, "symbol", asset),
1072
1064
  len(df),
@@ -1079,7 +1071,7 @@ class ThetaDataBacktestingPandas(PandasData):
1079
1071
  if isinstance(df.index, pd.DatetimeIndex):
1080
1072
  first_dt_str = df.index[0].isoformat() if hasattr(df.index[0], 'isoformat') else str(df.index[0])
1081
1073
  last_dt_str = df.index[-1].isoformat() if hasattr(df.index[-1], 'isoformat') else str(df.index[-1])
1082
- logger.info(
1074
+ logger.debug(
1083
1075
  "[THETA][DEBUG][QUOTE][THETA][DEBUG][PANDAS][DATETIME_RANGE] asset=%s | first_dt=%s | last_dt=%s | tz=%s",
1084
1076
  getattr(asset, "symbol", asset),
1085
1077
  first_dt_str,
@@ -1089,12 +1081,12 @@ class ThetaDataBacktestingPandas(PandasData):
1089
1081
 
1090
1082
  # CRITICAL: Show tail with explicit datetime index to catch time-travel bug
1091
1083
  if debug_enabled and len(available_cols) > 0:
1092
- logger.info(
1084
+ logger.debug(
1093
1085
  "[THETA][DEBUG][QUOTE][THETA][DEBUG][PANDAS][DATAFRAME_HEAD] asset=%s | first_5_rows (with datetime index):\n%s",
1094
1086
  getattr(asset, "symbol", asset),
1095
1087
  head_df[available_cols].to_string()
1096
1088
  )
1097
- logger.info(
1089
+ logger.debug(
1098
1090
  "[THETA][DEBUG][QUOTE][THETA][DEBUG][PANDAS][DATAFRAME_TAIL] asset=%s | last_5_rows (with datetime index):\n%s",
1099
1091
  getattr(asset, "symbol", asset),
1100
1092
  tail_df[available_cols].to_string()
@@ -1102,18 +1094,18 @@ class ThetaDataBacktestingPandas(PandasData):
1102
1094
 
1103
1095
  # Show tail datetime values explicitly
1104
1096
  tail_datetimes = [dt.isoformat() if hasattr(dt, 'isoformat') else str(dt) for dt in tail_df.index]
1105
- logger.info(
1097
+ logger.debug(
1106
1098
  "[THETA][DEBUG][QUOTE][THETA][DEBUG][PANDAS][TAIL_DATETIMES] asset=%s | tail_index=%s",
1107
1099
  getattr(asset, "symbol", asset),
1108
1100
  tail_datetimes
1109
1101
  )
1110
1102
  else:
1111
- logger.info(
1103
+ logger.debug(
1112
1104
  "[THETA][DEBUG][QUOTE][THETA][DEBUG][PANDAS][DATAFRAME_STATE] asset=%s | EMPTY_DATAFRAME",
1113
1105
  getattr(asset, "symbol", asset)
1114
1106
  )
1115
1107
  else:
1116
- logger.info(
1108
+ logger.debug(
1117
1109
  "[THETA][DEBUG][QUOTE][THETA][DEBUG][PANDAS][DATAFRAME_STATE] asset=%s | NO_DATA_FOUND_IN_STORE",
1118
1110
  getattr(asset, "symbol", asset)
1119
1111
  )
@@ -1121,9 +1113,8 @@ class ThetaDataBacktestingPandas(PandasData):
1121
1113
  quote_obj = super().get_quote(asset=asset, quote=quote, exchange=exchange)
1122
1114
 
1123
1115
  # [INSTRUMENTATION] Final quote result with all details
1124
- message = (
1125
- "[THETA][DEBUG][QUOTE][THETA][DEBUG][PANDAS][RESULT] asset=%s quote=%s current_dt=%s bid=%s ask=%s mid=%s last=%s source=%s"
1126
- ) % (
1116
+ logger.debug(
1117
+ "[THETA][DEBUG][QUOTE][THETA][DEBUG][PANDAS][RESULT] asset=%s quote=%s current_dt=%s bid=%s ask=%s mid=%s last=%s source=%s",
1127
1118
  getattr(asset, "symbol", asset) if not isinstance(asset, str) else asset,
1128
1119
  getattr(quote, "symbol", quote),
1129
1120
  dt,
@@ -1133,8 +1124,6 @@ class ThetaDataBacktestingPandas(PandasData):
1133
1124
  getattr(quote_obj, "last_price", None) if quote_obj else None,
1134
1125
  getattr(quote_obj, "source", None) if quote_obj else None,
1135
1126
  )
1136
- logger.warning(message)
1137
- print(message)
1138
1127
  return quote_obj
1139
1128
 
1140
1129
  def get_chains(self, asset):
@@ -26,6 +26,17 @@ CONNECTION_MAX_RETRIES = 60
26
26
  BOOT_GRACE_PERIOD = 5.0
27
27
  MAX_RESTART_ATTEMPTS = 3
28
28
 
29
+
30
+ def _resolve_asset_folder(asset_obj: Asset) -> str:
31
+ asset_type = getattr(asset_obj, "asset_type", None) or "stock"
32
+ asset_key = str(asset_type).strip().lower()
33
+ return asset_key
34
+
35
+
36
+ def _normalize_folder_component(value: str, fallback: str) -> str:
37
+ normalized = str(value or "").strip().lower().replace(" ", "_")
38
+ return normalized or fallback
39
+
29
40
  # Global process tracking for ThetaTerminal
30
41
  THETA_DATA_PROCESS = None
31
42
  THETA_DATA_PID = None
@@ -98,7 +109,7 @@ def append_missing_markers(
98
109
  CONNECTION_DIAGNOSTICS["placeholder_writes"] = CONNECTION_DIAGNOSTICS.get("placeholder_writes", 0) + len(rows)
99
110
 
100
111
  # DEBUG-LOG: Placeholder injection
101
- logger.info(
112
+ logger.debug(
102
113
  "[THETA][DEBUG][PLACEHOLDER][INJECT] count=%d dates=%s",
103
114
  len(rows),
104
115
  ", ".join(sorted({d.isoformat() for d in missing_dates}))
@@ -114,7 +125,7 @@ def append_missing_markers(
114
125
  else:
115
126
  df_all = pd.concat([df_all, placeholder_df]).sort_index()
116
127
  df_all = df_all[~df_all.index.duplicated(keep="last")]
117
- logger.info(
128
+ logger.debug(
118
129
  "[THETA][DEBUG][THETADATA-CACHE] recorded %d placeholder day(s): %s",
119
130
  len(rows),
120
131
  ", ".join(sorted({d.isoformat() for d in missing_dates})),
@@ -140,7 +151,7 @@ def remove_missing_markers(
140
151
  if mask.any():
141
152
  removed_dates = sorted({ts.date().isoformat() for ts in df_all.index[mask]})
142
153
  df_all = df_all.loc[~mask]
143
- logger.info(
154
+ logger.debug(
144
155
  "[THETA][DEBUG][THETADATA-CACHE] cleared %d placeholder row(s) for dates: %s",
145
156
  mask.sum(),
146
157
  ", ".join(removed_dates),
@@ -274,7 +285,7 @@ def get_price_data(
274
285
  try:
275
286
  fetched_remote = cache_manager.ensure_local_file(cache_file, payload=remote_payload)
276
287
  if fetched_remote:
277
- logger.info(
288
+ logger.debug(
278
289
  "[THETA][DEBUG][CACHE][REMOTE_DOWNLOAD] asset=%s timespan=%s datastyle=%s cache_file=%s",
279
290
  asset,
280
291
  timespan,
@@ -282,7 +293,7 @@ def get_price_data(
282
293
  cache_file,
283
294
  )
284
295
  except Exception as exc:
285
- logger.exception(
296
+ logger.debug(
286
297
  "[THETA][DEBUG][CACHE][REMOTE_DOWNLOAD_ERROR] asset=%s cache_file=%s error=%s",
287
298
  asset,
288
299
  cache_file,
@@ -290,7 +301,7 @@ def get_price_data(
290
301
  )
291
302
 
292
303
  # DEBUG-LOG: Cache file check
293
- logger.info(
304
+ logger.debug(
294
305
  "[THETA][DEBUG][CACHE][CHECK] asset=%s timespan=%s datastyle=%s cache_file=%s exists=%s",
295
306
  asset,
296
307
  timespan,
@@ -311,7 +322,7 @@ def get_price_data(
311
322
  placeholder_rows = int(df_all["missing"].sum())
312
323
 
313
324
  # DEBUG-LOG: Cache load result
314
- logger.info(
325
+ logger.debug(
315
326
  "[THETA][DEBUG][CACHE][LOADED] asset=%s cached_rows=%d placeholder_rows=%d real_rows=%d",
316
327
  asset,
317
328
  cached_rows,
@@ -329,7 +340,7 @@ def get_price_data(
329
340
  )
330
341
 
331
342
  # Check if we need to get more data
332
- logger.info(
343
+ logger.debug(
333
344
  "[THETA][DEBUG][CACHE][DECISION_START] asset=%s | "
334
345
  "calling get_missing_dates(start=%s, end=%s)",
335
346
  asset.symbol if hasattr(asset, 'symbol') else str(asset),
@@ -339,7 +350,7 @@ def get_price_data(
339
350
 
340
351
  missing_dates = get_missing_dates(df_all, asset, start, end)
341
352
 
342
- logger.info(
353
+ logger.debug(
343
354
  "[THETA][DEBUG][CACHE][DECISION_RESULT] asset=%s | "
344
355
  "missing_dates=%d | "
345
356
  "decision=%s",
@@ -363,7 +374,7 @@ def get_price_data(
363
374
  if df_all is not None and not df_all.empty:
364
375
  logger.info("ThetaData cache HIT for %s %s %s (%d rows).", asset, timespan, datastyle, len(df_all))
365
376
  # DEBUG-LOG: Cache hit
366
- logger.info(
377
+ logger.debug(
367
378
  "[THETA][DEBUG][CACHE][HIT] asset=%s timespan=%s datastyle=%s rows=%d start=%s end=%s",
368
379
  asset,
369
380
  timespan,
@@ -390,7 +401,7 @@ def get_price_data(
390
401
  # DEBUG-LOG: Entry to intraday filter
391
402
  rows_before_any_filter = len(df_all)
392
403
  max_ts_before_any_filter = df_all.index.max() if len(df_all) > 0 else None
393
- logger.info(
404
+ logger.debug(
394
405
  "[THETA][DEBUG][FILTER][INTRADAY_ENTRY] asset=%s | "
395
406
  "rows_before=%d max_ts_before=%s | "
396
407
  "start_param=%s end_param=%s dt_param=%s dt_type=%s",
@@ -406,13 +417,13 @@ def get_price_data(
406
417
  # Convert date to datetime if needed
407
418
  if isinstance(start, datetime_module.date) and not isinstance(start, datetime_module.datetime):
408
419
  start = datetime_module.datetime.combine(start, datetime_module.time.min)
409
- logger.info(
420
+ logger.debug(
410
421
  "[THETA][DEBUG][FILTER][DATE_CONVERSION] converted start from date to datetime: %s",
411
422
  start.isoformat()
412
423
  )
413
424
  if isinstance(end, datetime_module.date) and not isinstance(end, datetime_module.datetime):
414
425
  end = datetime_module.datetime.combine(end, datetime_module.time.max)
415
- logger.info(
426
+ logger.debug(
416
427
  "[THETA][DEBUG][FILTER][DATE_CONVERSION] converted end from date to datetime: %s",
417
428
  end.isoformat()
418
429
  )
@@ -421,20 +432,20 @@ def get_price_data(
421
432
  if isinstance(end, datetime_module.datetime) and end.time() == datetime_module.time.min:
422
433
  # Convert end-of-period midnight to end-of-day
423
434
  end = datetime_module.datetime.combine(end.date(), datetime_module.time.max)
424
- logger.info(
435
+ logger.debug(
425
436
  "[THETA][DEBUG][FILTER][MIDNIGHT_FIX] converted end from midnight to end-of-day: %s",
426
437
  end.isoformat()
427
438
  )
428
439
 
429
440
  if start.tzinfo is None:
430
441
  start = LUMIBOT_DEFAULT_PYTZ.localize(start).astimezone(pytz.UTC)
431
- logger.info(
442
+ logger.debug(
432
443
  "[THETA][DEBUG][FILTER][TZ_LOCALIZE] localized start to UTC: %s",
433
444
  start.isoformat()
434
445
  )
435
446
  if end.tzinfo is None:
436
447
  end = LUMIBOT_DEFAULT_PYTZ.localize(end).astimezone(pytz.UTC)
437
- logger.info(
448
+ logger.debug(
438
449
  "[THETA][DEBUG][FILTER][TZ_LOCALIZE] localized end to UTC: %s",
439
450
  end.isoformat()
440
451
  )
@@ -445,7 +456,7 @@ def get_price_data(
445
456
  #
446
457
  # NEW APPROACH: Always return full [start, end] range from cache
447
458
  # Let Data/DataPolars.get_bars() handle look-ahead bias protection
448
- logger.info(
459
+ logger.debug(
449
460
  "[THETA][DEBUG][FILTER][NO_DT_FILTER] asset=%s | "
450
461
  "using end=%s for upper bound (dt parameter ignored for cache retrieval)",
451
462
  asset.symbol if hasattr(asset, 'symbol') else str(asset),
@@ -455,7 +466,7 @@ def get_price_data(
455
466
 
456
467
  # DEBUG-LOG: After date range filtering, before missing removal
457
468
  if df_all is not None and not df_all.empty:
458
- logger.info(
469
+ logger.debug(
459
470
  "[THETA][DEBUG][FILTER][AFTER] asset=%s rows=%d first_ts=%s last_ts=%s dt_filter=%s",
460
471
  asset,
461
472
  len(df_all),
@@ -470,7 +481,7 @@ def get_price_data(
470
481
 
471
482
  # DEBUG-LOG: Before pandas return
472
483
  if df_all is not None and not df_all.empty:
473
- logger.info(
484
+ logger.debug(
474
485
  "[THETA][DEBUG][RETURN][PANDAS] asset=%s rows=%d first_ts=%s last_ts=%s",
475
486
  asset,
476
487
  len(df_all),
@@ -482,7 +493,7 @@ def get_price_data(
482
493
  logger.info("ThetaData cache MISS for %s %s %s; fetching %d interval(s) from ThetaTerminal.", asset, timespan, datastyle, len(missing_dates))
483
494
 
484
495
  # DEBUG-LOG: Cache miss
485
- logger.info(
496
+ logger.debug(
486
497
  "[THETA][DEBUG][CACHE][MISS] asset=%s timespan=%s datastyle=%s missing_intervals=%d first=%s last=%s",
487
498
  asset,
488
499
  timespan,
@@ -542,7 +553,7 @@ def get_price_data(
542
553
  and all(day > asset.expiration for day in requested_dates)
543
554
  )
544
555
  if expired_range:
545
- logger.info(
556
+ logger.debug(
546
557
  "[THETA][DEBUG][THETADATA-EOD] Option %s expired on %s; cache reuse for range %s -> %s.",
547
558
  asset,
548
559
  asset.expiration,
@@ -550,7 +561,7 @@ def get_price_data(
550
561
  fetch_end,
551
562
  )
552
563
  else:
553
- logger.warning(
564
+ logger.debug(
554
565
  "[THETA][DEBUG][THETADATA-EOD] No rows returned for %s between %s and %s; recording placeholders.",
555
566
  asset,
556
567
  fetch_start,
@@ -680,7 +691,7 @@ def get_price_data(
680
691
  and chunk_end.date() >= asset.expiration
681
692
  )
682
693
  if expired_chunk:
683
- logger.info(
694
+ logger.debug(
684
695
  "[THETA][DEBUG][THETADATA] Option %s considered expired on %s; reusing cached data between %s and %s.",
685
696
  asset,
686
697
  asset.expiration,
@@ -785,7 +796,11 @@ def get_trading_dates(asset: Asset, start: datetime, end: datetime):
785
796
  def build_cache_filename(asset: Asset, timespan: str, datastyle: str = "ohlc"):
786
797
  """Helper function to create the cache filename for a given asset and timespan"""
787
798
 
788
- lumibot_cache_folder = Path(LUMIBOT_CACHE_FOLDER) / CACHE_SUBFOLDER
799
+ provider_root = Path(LUMIBOT_CACHE_FOLDER) / CACHE_SUBFOLDER
800
+ asset_folder = _resolve_asset_folder(asset)
801
+ timespan_folder = _normalize_folder_component(timespan, "unknown")
802
+ datastyle_folder = _normalize_folder_component(datastyle, "default")
803
+ base_folder = provider_root / asset_folder / timespan_folder / datastyle_folder
789
804
 
790
805
  # If It's an option then also add the expiration date, strike price and right to the filename
791
806
  if asset.asset_type == "option":
@@ -799,7 +814,7 @@ def build_cache_filename(asset: Asset, timespan: str, datastyle: str = "ohlc"):
799
814
  uniq_str = asset.symbol
800
815
 
801
816
  cache_filename = f"{asset.asset_type}_{uniq_str}_{timespan}_{datastyle}.parquet"
802
- cache_file = lumibot_cache_folder / cache_filename
817
+ cache_file = base_folder / cache_filename
803
818
  return cache_file
804
819
 
805
820
 
@@ -848,7 +863,7 @@ def get_missing_dates(df_all, asset, start, end):
848
863
  A list of dates that we need to get data for
849
864
  """
850
865
  # DEBUG-LOG: Entry to get_missing_dates
851
- logger.info(
866
+ logger.debug(
852
867
  "[THETA][DEBUG][CACHE][MISSING_DATES_CHECK] asset=%s | "
853
868
  "start=%s end=%s | "
854
869
  "cache_rows=%d",
@@ -860,7 +875,7 @@ def get_missing_dates(df_all, asset, start, end):
860
875
 
861
876
  trading_dates = get_trading_dates(asset, start, end)
862
877
 
863
- logger.info(
878
+ logger.debug(
864
879
  "[THETA][DEBUG][CACHE][TRADING_DATES] asset=%s | "
865
880
  "trading_dates_count=%d first=%s last=%s",
866
881
  asset.symbol if hasattr(asset, 'symbol') else str(asset),
@@ -870,7 +885,7 @@ def get_missing_dates(df_all, asset, start, end):
870
885
  )
871
886
 
872
887
  if df_all is None or not len(df_all):
873
- logger.info(
888
+ logger.debug(
874
889
  "[THETA][DEBUG][CACHE][EMPTY] asset=%s | "
875
890
  "cache is EMPTY -> all %d trading days are missing",
876
891
  asset.symbol if hasattr(asset, 'symbol') else str(asset),
@@ -886,7 +901,7 @@ def get_missing_dates(df_all, asset, start, end):
886
901
  cached_first = min(dates) if len(dates) > 0 else None
887
902
  cached_last = max(dates) if len(dates) > 0 else None
888
903
 
889
- logger.info(
904
+ logger.debug(
890
905
  "[THETA][DEBUG][CACHE][CACHED_DATES] asset=%s | "
891
906
  "cached_dates_count=%d first=%s last=%s",
892
907
  asset.symbol if hasattr(asset, 'symbol') else str(asset),
@@ -904,7 +919,7 @@ def get_missing_dates(df_all, asset, start, end):
904
919
  after_expiry_filter = len(missing_dates)
905
920
 
906
921
  if before_expiry_filter != after_expiry_filter:
907
- logger.info(
922
+ logger.debug(
908
923
  "[THETA][DEBUG][CACHE][OPTION_EXPIRY_FILTER] asset=%s | "
909
924
  "filtered %d dates after expiration=%s | "
910
925
  "missing_dates: %d -> %d",
@@ -915,7 +930,7 @@ def get_missing_dates(df_all, asset, start, end):
915
930
  after_expiry_filter
916
931
  )
917
932
 
918
- logger.info(
933
+ logger.debug(
919
934
  "[THETA][DEBUG][CACHE][MISSING_RESULT] asset=%s | "
920
935
  "missing_dates_count=%d | "
921
936
  "first_missing=%s last_missing=%s",
@@ -931,7 +946,7 @@ def get_missing_dates(df_all, asset, start, end):
931
946
  def load_cache(cache_file):
932
947
  """Load the data from the cache file and return a DataFrame with a DateTimeIndex"""
933
948
  # DEBUG-LOG: Start loading cache
934
- logger.info(
949
+ logger.debug(
935
950
  "[THETA][DEBUG][CACHE][LOAD_START] cache_file=%s | "
936
951
  "exists=%s size_bytes=%d",
937
952
  cache_file.name,
@@ -940,7 +955,7 @@ def load_cache(cache_file):
940
955
  )
941
956
 
942
957
  if not cache_file.exists():
943
- logger.info(
958
+ logger.debug(
944
959
  "[THETA][DEBUG][CACHE][LOAD_MISSING] cache_file=%s | returning=None",
945
960
  cache_file.name,
946
961
  )
@@ -949,7 +964,7 @@ def load_cache(cache_file):
949
964
  df = pd.read_parquet(cache_file, engine='pyarrow')
950
965
 
951
966
  rows_after_read = len(df)
952
- logger.info(
967
+ logger.debug(
953
968
  "[THETA][DEBUG][CACHE][LOAD_READ] cache_file=%s | "
954
969
  "rows_read=%d columns=%s",
955
970
  cache_file.name,
@@ -969,7 +984,7 @@ def load_cache(cache_file):
969
984
  if df.index.tzinfo is None:
970
985
  # Set the timezone to UTC
971
986
  df.index = df.index.tz_localize("UTC")
972
- logger.info(
987
+ logger.debug(
973
988
  "[THETA][DEBUG][CACHE][LOAD_TZ] cache_file=%s | "
974
989
  "localized index to UTC",
975
990
  cache_file.name
@@ -981,7 +996,7 @@ def load_cache(cache_file):
981
996
  max_ts = df.index.max() if len(df) > 0 else None
982
997
  placeholder_count = int(df["missing"].sum()) if "missing" in df.columns else 0
983
998
 
984
- logger.info(
999
+ logger.debug(
985
1000
  "[THETA][DEBUG][CACHE][LOAD_SUCCESS] cache_file=%s | "
986
1001
  "total_rows=%d real_rows=%d placeholders=%d | "
987
1002
  "min_ts=%s max_ts=%s",
@@ -999,7 +1014,7 @@ def load_cache(cache_file):
999
1014
  def update_cache(cache_file, df_all, df_cached, missing_dates=None, remote_payload=None):
1000
1015
  """Update the cache file with the new data and optional placeholder markers."""
1001
1016
  # DEBUG-LOG: Entry to update_cache
1002
- logger.info(
1017
+ logger.debug(
1003
1018
  "[THETA][DEBUG][CACHE][UPDATE_ENTRY] cache_file=%s | "
1004
1019
  "df_all_rows=%d df_cached_rows=%d missing_dates=%d",
1005
1020
  cache_file.name,
@@ -1010,13 +1025,13 @@ def update_cache(cache_file, df_all, df_cached, missing_dates=None, remote_paylo
1010
1025
 
1011
1026
  if df_all is None or len(df_all) == 0:
1012
1027
  if not missing_dates:
1013
- logger.info(
1028
+ logger.debug(
1014
1029
  "[THETA][DEBUG][CACHE][UPDATE_SKIP] cache_file=%s | "
1015
1030
  "df_all is empty and no missing_dates, skipping cache update",
1016
1031
  cache_file.name
1017
1032
  )
1018
1033
  return
1019
- logger.info(
1034
+ logger.debug(
1020
1035
  "[THETA][DEBUG][CACHE][UPDATE_PLACEHOLDERS_ONLY] cache_file=%s | "
1021
1036
  "df_all is empty, writing %d placeholders",
1022
1037
  cache_file.name,
@@ -1026,7 +1041,7 @@ def update_cache(cache_file, df_all, df_cached, missing_dates=None, remote_paylo
1026
1041
  else:
1027
1042
  df_working = ensure_missing_column(df_all.copy())
1028
1043
  if missing_dates:
1029
- logger.info(
1044
+ logger.debug(
1030
1045
  "[THETA][DEBUG][CACHE][UPDATE_APPEND_PLACEHOLDERS] cache_file=%s | "
1031
1046
  "appending %d placeholders to %d existing rows",
1032
1047
  cache_file.name,
@@ -1036,7 +1051,7 @@ def update_cache(cache_file, df_all, df_cached, missing_dates=None, remote_paylo
1036
1051
  df_working = append_missing_markers(df_working, missing_dates)
1037
1052
 
1038
1053
  if df_working is None or len(df_working) == 0:
1039
- logger.info(
1054
+ logger.debug(
1040
1055
  "[THETA][DEBUG][CACHE][UPDATE_SKIP_EMPTY] cache_file=%s | "
1041
1056
  "df_working is empty after processing, skipping write",
1042
1057
  cache_file.name
@@ -1048,7 +1063,7 @@ def update_cache(cache_file, df_all, df_cached, missing_dates=None, remote_paylo
1048
1063
  df_cached_cmp = ensure_missing_column(df_cached.copy())
1049
1064
 
1050
1065
  if df_cached_cmp is not None and df_working.equals(df_cached_cmp):
1051
- logger.info(
1066
+ logger.debug(
1052
1067
  "[THETA][DEBUG][CACHE][UPDATE_NO_CHANGES] cache_file=%s | "
1053
1068
  "df_working equals df_cached (rows=%d), skipping write",
1054
1069
  cache_file.name,
@@ -1069,7 +1084,7 @@ def update_cache(cache_file, df_all, df_cached, missing_dates=None, remote_paylo
1069
1084
  return None
1070
1085
  return value.isoformat() if hasattr(value, "isoformat") else value
1071
1086
 
1072
- logger.info(
1087
+ logger.debug(
1073
1088
  "[THETA][DEBUG][CACHE][UPDATE_WRITE] cache_file=%s | "
1074
1089
  "total_rows=%d real_rows=%d placeholders=%d | "
1075
1090
  "min_ts=%s max_ts=%s",
@@ -1083,7 +1098,7 @@ def update_cache(cache_file, df_all, df_cached, missing_dates=None, remote_paylo
1083
1098
 
1084
1099
  df_to_save.to_parquet(cache_file, engine="pyarrow", compression="snappy")
1085
1100
 
1086
- logger.info(
1101
+ logger.debug(
1087
1102
  "[THETA][DEBUG][CACHE][UPDATE_SUCCESS] cache_file=%s written successfully",
1088
1103
  cache_file.name
1089
1104
  )
@@ -1093,7 +1108,7 @@ def update_cache(cache_file, df_all, df_cached, missing_dates=None, remote_paylo
1093
1108
  try:
1094
1109
  cache_manager.on_local_update(cache_file, payload=remote_payload)
1095
1110
  except Exception as exc:
1096
- logger.exception(
1111
+ logger.debug(
1097
1112
  "[THETA][DEBUG][CACHE][REMOTE_UPLOAD_ERROR] cache_file=%s error=%s",
1098
1113
  cache_file,
1099
1114
  exc,
@@ -1469,7 +1484,7 @@ def get_request(url: str, headers: dict, querystring: dict, username: str, passw
1469
1484
  CONNECTION_DIAGNOSTICS["network_requests"] += 1
1470
1485
 
1471
1486
  # DEBUG-LOG: API request
1472
- logger.info(
1487
+ logger.debug(
1473
1488
  "[THETA][DEBUG][API][REQUEST] url=%s params=%s",
1474
1489
  request_url if next_page_url else url,
1475
1490
  request_params if request_params else querystring
@@ -1480,7 +1495,7 @@ def get_request(url: str, headers: dict, querystring: dict, username: str, passw
1480
1495
  if response.status_code == 472:
1481
1496
  logger.warning(f"No data available for request: {response.text[:200]}")
1482
1497
  # DEBUG-LOG: API response - no data
1483
- logger.info(
1498
+ logger.debug(
1484
1499
  "[THETA][DEBUG][API][RESPONSE] status=472 result=NO_DATA"
1485
1500
  )
1486
1501
  return None
@@ -1488,7 +1503,7 @@ def get_request(url: str, headers: dict, querystring: dict, username: str, passw
1488
1503
  elif response.status_code != 200:
1489
1504
  logger.warning(f"Non-200 status code {response.status_code}: {response.text[:200]}")
1490
1505
  # DEBUG-LOG: API response - error
1491
- logger.info(
1506
+ logger.debug(
1492
1507
  "[THETA][DEBUG][API][RESPONSE] status=%d result=ERROR",
1493
1508
  response.status_code
1494
1509
  )
@@ -1498,7 +1513,7 @@ def get_request(url: str, headers: dict, querystring: dict, username: str, passw
1498
1513
 
1499
1514
  # DEBUG-LOG: API response - success
1500
1515
  response_rows = len(json_resp.get("response", [])) if isinstance(json_resp.get("response"), list) else 0
1501
- logger.info(
1516
+ logger.debug(
1502
1517
  "[THETA][DEBUG][API][RESPONSE] status=200 rows=%d has_next_page=%s",
1503
1518
  response_rows,
1504
1519
  bool(json_resp.get("header", {}).get("next_page"))
@@ -1524,7 +1539,7 @@ def get_request(url: str, headers: dict, querystring: dict, username: str, passw
1524
1539
  logger.warning(f"Exception during request (attempt {counter + 1}): {e}")
1525
1540
  check_connection(username=username, password=password, wait_for_connection=True)
1526
1541
  if counter == 0:
1527
- logger.info("[THETA][DEBUG][API][WAIT] Allowing ThetaTerminal to initialize for 5s before retry.")
1542
+ logger.debug("[THETA][DEBUG][API][WAIT] Allowing ThetaTerminal to initialize for 5s before retry.")
1528
1543
  time.sleep(5)
1529
1544
 
1530
1545
  counter += 1
@@ -1609,7 +1624,7 @@ def get_historical_eod_data(asset: Asset, start_dt: datetime, end_dt: datetime,
1609
1624
  headers = {"Accept": "application/json"}
1610
1625
 
1611
1626
  # DEBUG-LOG: EOD data request
1612
- logger.info(
1627
+ logger.debug(
1613
1628
  "[THETA][DEBUG][EOD][REQUEST] asset=%s start=%s end=%s datastyle=%s",
1614
1629
  asset,
1615
1630
  start_date,
@@ -1622,7 +1637,7 @@ def get_historical_eod_data(asset: Asset, start_dt: datetime, end_dt: datetime,
1622
1637
  username=username, password=password)
1623
1638
  if json_resp is None:
1624
1639
  # DEBUG-LOG: EOD data response - no data
1625
- logger.info(
1640
+ logger.debug(
1626
1641
  "[THETA][DEBUG][EOD][RESPONSE] asset=%s result=NO_DATA",
1627
1642
  asset
1628
1643
  )
@@ -1630,7 +1645,7 @@ def get_historical_eod_data(asset: Asset, start_dt: datetime, end_dt: datetime,
1630
1645
 
1631
1646
  # DEBUG-LOG: EOD data response - success
1632
1647
  response_rows = len(json_resp.get("response", [])) if isinstance(json_resp.get("response"), list) else 0
1633
- logger.info(
1648
+ logger.debug(
1634
1649
  "[THETA][DEBUG][EOD][RESPONSE] asset=%s rows=%d",
1635
1650
  asset,
1636
1651
  response_rows
@@ -1786,7 +1801,7 @@ def get_historical_data(asset: Asset, start_dt: datetime, end_dt: datetime, ivl:
1786
1801
  headers = {"Accept": "application/json"}
1787
1802
 
1788
1803
  # DEBUG-LOG: Intraday data request
1789
- logger.info(
1804
+ logger.debug(
1790
1805
  "[THETA][DEBUG][INTRADAY][REQUEST] asset=%s start=%s end=%s ivl=%d datastyle=%s include_after_hours=%s",
1791
1806
  asset,
1792
1807
  start_date,
@@ -1802,7 +1817,7 @@ def get_historical_data(asset: Asset, start_dt: datetime, end_dt: datetime, ivl:
1802
1817
  username=username, password=password)
1803
1818
  if json_resp is None:
1804
1819
  # DEBUG-LOG: Intraday data response - no data
1805
- logger.info(
1820
+ logger.debug(
1806
1821
  "[THETA][DEBUG][INTRADAY][RESPONSE] asset=%s result=NO_DATA",
1807
1822
  asset
1808
1823
  )
@@ -1810,7 +1825,7 @@ def get_historical_data(asset: Asset, start_dt: datetime, end_dt: datetime, ivl:
1810
1825
 
1811
1826
  # DEBUG-LOG: Intraday data response - success
1812
1827
  response_rows = len(json_resp.get("response", [])) if isinstance(json_resp.get("response"), list) else 0
1813
- logger.info(
1828
+ logger.debug(
1814
1829
  "[THETA][DEBUG][INTRADAY][RESPONSE] asset=%s rows=%d",
1815
1830
  asset,
1816
1831
  response_rows
@@ -1969,7 +1984,7 @@ def get_chains_cached(
1969
1984
  Retrieve option chain with caching (MATCHES POLYGON PATTERN).
1970
1985
 
1971
1986
  This function follows the EXACT same caching strategy as Polygon:
1972
- 1. Check cache: LUMIBOT_CACHE_FOLDER/thetadata/option_chains/{symbol}_{date}.parquet
1987
+ 1. Check cache: LUMIBOT_CACHE_FOLDER/thetadata/<asset-type>/option_chains/{symbol}_{date}.parquet
1973
1988
  2. Reuse files within RECENT_FILE_TOLERANCE_DAYS (default 7 days)
1974
1989
  3. If not found, fetch from ThetaData and save to cache
1975
1990
  4. Use pyarrow engine with snappy compression
@@ -2006,7 +2021,7 @@ def get_chains_cached(
2006
2021
  return None
2007
2022
 
2008
2023
  # 2) Build cache folder path
2009
- chain_folder = Path(LUMIBOT_CACHE_FOLDER) / "thetadata" / "option_chains"
2024
+ chain_folder = Path(LUMIBOT_CACHE_FOLDER) / "thetadata" / _resolve_asset_folder(asset) / "option_chains"
2010
2025
  chain_folder.mkdir(parents=True, exist_ok=True)
2011
2026
 
2012
2027
  # 3) Check for recent cached file (within RECENT_FILE_TOLERANCE_DAYS)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumibot
3
- Version: 4.2.0
3
+ Version: 4.2.2
4
4
  Summary: Backtesting and Trading Library, Made by Lumiwealth
5
5
  Home-page: https://github.com/Lumiwealth/lumibot
6
6
  Author: Robert Grzesik
@@ -14,7 +14,7 @@ lumibot/backtesting/interactive_brokers_rest_backtesting.py,sha256=5HJ_sPX0uOUg-
14
14
  lumibot/backtesting/pandas_backtesting.py,sha256=m-NvT4o-wFQjaZft6TXULzeZBrskO_7Z-jfy9AIkyAY,388
15
15
  lumibot/backtesting/polygon_backtesting.py,sha256=u9kif_2_7k0P4-KDvbHhaMfSoBVejUUX7fh9H3PCVE0,12350
16
16
  lumibot/backtesting/thetadata_backtesting.py,sha256=Xcz5f-4zTkKgWWcktNzItH2vrr8CysIMQWKKqLwugbA,345
17
- lumibot/backtesting/thetadata_backtesting_pandas.py,sha256=rglyUgZc0bNt9IWuPvmJJNXyU80JvQvRiUXWCzR1oDA,51994
17
+ lumibot/backtesting/thetadata_backtesting_pandas.py,sha256=14XMsbQCa3uE_iS2nvTlGUXkn9kvI0cDSE8mqdKnDEg,51750
18
18
  lumibot/backtesting/yahoo_backtesting.py,sha256=LT2524mGlrUSq1YSRnUqGW4-Xcq4USgRv2EhnV_zfs4,502
19
19
  lumibot/brokers/__init__.py,sha256=MGWKHeH3mqseYRL7u-KX1Jp2x9EaFO4Ol8sfNSxzu1M,404
20
20
  lumibot/brokers/alpaca.py,sha256=VQ17idfqiEFb2JCqqdMGmbvF789L7_PpsCbudiFRzmg,61595
@@ -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=RoUIW-JRnuVJzfz42Djhan7cPRa8F8cisQdcMjKpTPM,79366
135
+ lumibot/tools/thetadata_helper.py,sha256=-FJm_NXSBJoyYLcdNQXGytMbmr-wx7F1gItnRnBUWf0,80072
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.0.dist-info/licenses/LICENSE,sha256=fYhGIyxjyNXACgpNQS3xxpxDOaVOWRVxZMCRbsDv8k0,35130
145
+ lumibot-4.2.2.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
@@ -157,7 +157,7 @@ tests/test_apscheduler_warnings.py,sha256=08lzprPjKq_KAIy-_gMo2zZATpo7VPvmg_qnmS
157
157
  tests/test_asset.py,sha256=qk9giu-z3kPoFRXL6Wi0-Ly1mb7YpUNtViuLUMjaEhY,7659
158
158
  tests/test_asset_auto_expiry.py,sha256=aa0JVbAIHPKupQ6gMDk5QaDWDXV1xqHMdX513WjwWNQ,13716
159
159
  tests/test_auto_market_inference.py,sha256=NFauxzT9ZKDSjrkgHWLgcrLfJMeIj04uWvSKlZVIwqs,1717
160
- tests/test_backtest_cache_manager.py,sha256=Lkm_xnKX2L-_wS2wGZ57AghPDeZUeoS9BxcuDvwdNnw,4871
160
+ tests/test_backtest_cache_manager.py,sha256=4bZkbv-PZVFRTuKinyLFeAl03tR8rwWV81xHLsvkphI,5120
161
161
  tests/test_backtesting_broker.py,sha256=rxZGH5cgiWLmNGdI3k9fti3Fp9IOSohq8xD2E3L2UdY,13194
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
@@ -234,8 +234,8 @@ tests/test_quiet_logs_requirements.py,sha256=YoUooSVLrFL8TlWPfxEiqxvSj4d8z6-qg58
234
234
  tests/test_session_manager.py,sha256=1qygN3aQ2Xe2uh4BMPm0E3V8KXLFNGq5qdL8KkZjef4,11632
235
235
  tests/test_strategy_methods.py,sha256=j9Mhr6nnG1fkiVQXnx7gLjzGbeQmwt0UbJr_4plD36o,12539
236
236
  tests/test_thetadata_backwards_compat.py,sha256=RzNLhNZNJZ2hPkEDyG-T_4mRRXh5XqavK6r-OjfRASQ,3306
237
- tests/test_thetadata_helper.py,sha256=lNUhA9VuzCZaGKuN86Zkpq3fi2Qk_R0iv1P5JdkOXNA,57966
238
- tests/test_thetadata_pandas_verification.py,sha256=MkGIci52W3gez4C6SI4SpcFlrDXmagxikNeOLmXt0yA,6547
237
+ tests/test_thetadata_helper.py,sha256=pcEPu-9kQYp4cn5xmhU1-28DfT-GRu_nUuUMb1xi7nA,58088
238
+ tests/test_thetadata_pandas_verification.py,sha256=MWUecqBY6FGFslWLRo_C5blGbom_unmXCZikAfZXLks,6553
239
239
  tests/test_tradier.py,sha256=iCEM2FTxJSzJ2oLNaRqSx05XaX_DCiMzLx1aEYPANko,33280
240
240
  tests/test_tradier_data.py,sha256=1jTxDzQtzaC42CQJVXMRMElBwExy1mVci3NFfKjjVH0,13363
241
241
  tests/test_tradingfee.py,sha256=2CBJgdU-73Ae4xuys-QkbCtpDTL9hwOUkRnCgLm4OmE,163
@@ -280,7 +280,7 @@ tests/backtest/test_thetadata.py,sha256=xWYfC9C4EhbMDb29qyZWHO3sSWaLIPzzvcMbHCt5
280
280
  tests/backtest/test_thetadata_comprehensive.py,sha256=-gN3xLJcJtlB-k4vlaK82DCZDGDmr0LNZZDzn-aN3l4,26120
281
281
  tests/backtest/test_thetadata_vs_polygon.py,sha256=dZqsrOx3u3cz-1onIO6o5BDRjI1ey7U9vIkZupfXoig,22831
282
282
  tests/backtest/test_yahoo.py,sha256=2FguUTUMC9_A20eqxnZ17rN3tT9n6hyvJHaL98QKpqY,3443
283
- lumibot-4.2.0.dist-info/METADATA,sha256=fLMcodhb7BQl9rONrvfEQMxIhXx9lo5T-iRftdS2qDk,12092
284
- lumibot-4.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
285
- lumibot-4.2.0.dist-info/top_level.txt,sha256=otUnUjDFVASauEDiTiAzNgMyqQ1B6jjS3QqqP-WSx38,14
286
- lumibot-4.2.0.dist-info/RECORD,,
283
+ lumibot-4.2.2.dist-info/METADATA,sha256=yjZcnAmbXlQQj4ZEDPZNoWndZHusYMdY8nluNGVQP-0,12092
284
+ lumibot-4.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
285
+ lumibot-4.2.2.dist-info/top_level.txt,sha256=otUnUjDFVASauEDiTiAzNgMyqQ1B6jjS3QqqP-WSx38,14
286
+ lumibot-4.2.2.dist-info/RECORD,,
@@ -67,7 +67,7 @@ def _build_settings(prefix: str = "prod/cache") -> BacktestCacheSettings:
67
67
  def test_remote_key_uses_relative_cache_path(tmp_path, monkeypatch):
68
68
  cache_root = tmp_path / "cache"
69
69
  cache_root.mkdir()
70
- local_file = cache_root / "thetadata" / "bars" / "spy.parquet"
70
+ local_file = cache_root / "thetadata" / "stock" / "minute" / "ohlc" / "stock_SPY_minute_ohlc.parquet"
71
71
  local_file.parent.mkdir(parents=True, exist_ok=True)
72
72
 
73
73
  monkeypatch.setattr(backtest_cache, "LUMIBOT_CACHE_FOLDER", cache_root)
@@ -76,17 +76,17 @@ def test_remote_key_uses_relative_cache_path(tmp_path, monkeypatch):
76
76
  manager = BacktestCacheManager(settings, client_factory=lambda settings: StubS3Client())
77
77
 
78
78
  remote_key = manager.remote_key_for(local_file)
79
- assert remote_key == "stage/cache/v3/thetadata/bars/spy.parquet"
79
+ assert remote_key == "stage/cache/v3/thetadata/stock/minute/ohlc/stock_SPY_minute_ohlc.parquet"
80
80
 
81
81
 
82
82
  def test_ensure_local_file_downloads_from_s3(tmp_path, monkeypatch):
83
83
  cache_root = tmp_path / "cache"
84
84
  cache_root.mkdir()
85
- local_file = cache_root / "thetadata" / "bars" / "spy.parquet"
85
+ local_file = cache_root / "thetadata" / "stock" / "minute" / "ohlc" / "stock_SPY_minute_ohlc.parquet"
86
86
 
87
87
  monkeypatch.setattr(backtest_cache, "LUMIBOT_CACHE_FOLDER", cache_root)
88
88
 
89
- remote_key = "stage/cache/v3/thetadata/bars/spy.parquet"
89
+ remote_key = "stage/cache/v3/thetadata/stock/minute/ohlc/stock_SPY_minute_ohlc.parquet"
90
90
  objects = {("test-bucket", remote_key): b"cached-data"}
91
91
 
92
92
  stub = StubS3Client(objects)
@@ -101,7 +101,7 @@ def test_ensure_local_file_downloads_from_s3(tmp_path, monkeypatch):
101
101
  def test_ensure_local_file_handles_missing_remote(tmp_path, monkeypatch):
102
102
  cache_root = tmp_path / "cache"
103
103
  cache_root.mkdir()
104
- local_file = cache_root / "thetadata" / "bars" / "spy.parquet"
104
+ local_file = cache_root / "thetadata" / "stock" / "minute" / "ohlc" / "stock_SPY_minute_ohlc.parquet"
105
105
 
106
106
  monkeypatch.setattr(backtest_cache, "LUMIBOT_CACHE_FOLDER", cache_root)
107
107
 
@@ -116,13 +116,13 @@ def test_ensure_local_file_handles_missing_remote(tmp_path, monkeypatch):
116
116
  def test_on_local_update_uploads_file(tmp_path, monkeypatch):
117
117
  cache_root = tmp_path / "cache"
118
118
  cache_root.mkdir()
119
- local_file = cache_root / "thetadata" / "bars" / "spy.parquet"
119
+ local_file = cache_root / "thetadata" / "stock" / "minute" / "ohlc" / "stock_SPY_minute_ohlc.parquet"
120
120
  local_file.parent.mkdir(parents=True, exist_ok=True)
121
121
  local_file.write_bytes(b"new-data")
122
122
 
123
123
  monkeypatch.setattr(backtest_cache, "LUMIBOT_CACHE_FOLDER", cache_root)
124
124
 
125
- remote_key = "stage/cache/v3/thetadata/bars/spy.parquet"
125
+ remote_key = "stage/cache/v3/thetadata/stock/minute/ohlc/stock_SPY_minute_ohlc.parquet"
126
126
  stub = StubS3Client({("test-bucket", remote_key): b"old"})
127
127
  manager = BacktestCacheManager(_build_settings(prefix="stage/cache"), client_factory=lambda s: stub)
128
128
 
@@ -324,13 +324,13 @@ def test_get_trading_dates():
324
324
  def test_build_cache_filename(mocker, tmpdir, datastyle):
325
325
  asset = Asset("SPY")
326
326
  timespan = "1D"
327
- mocker.patch.object(thetadata_helper, "LUMIBOT_CACHE_FOLDER", tmpdir)
328
- expected = tmpdir / "thetadata" / f"stock_SPY_1D_{datastyle}.parquet"
327
+ mocker.patch.object(thetadata_helper, "LUMIBOT_CACHE_FOLDER", str(tmpdir))
328
+ expected = tmpdir / "thetadata" / "stock" / "1d" / datastyle / f"stock_SPY_1D_{datastyle}.parquet"
329
329
  assert thetadata_helper.build_cache_filename(asset, timespan, datastyle) == expected
330
330
 
331
331
  expire_date = datetime.date(2023, 8, 1)
332
332
  option_asset = Asset("SPY", asset_type="option", expiration=expire_date, strike=100, right="CALL")
333
- expected = tmpdir / "thetadata" / f"option_SPY_230801_100_CALL_1D_{datastyle}.parquet"
333
+ expected = tmpdir / "thetadata" / "option" / "1d" / datastyle / f"option_SPY_230801_100_CALL_1D_{datastyle}.parquet"
334
334
  assert thetadata_helper.build_cache_filename(option_asset, timespan, datastyle) == expected
335
335
 
336
336
  # Bad option asset with no expiration
@@ -427,8 +427,8 @@ def test_missing_dates():
427
427
  ],
428
428
  )
429
429
  def test_update_cache(mocker, tmpdir, df_all, df_cached, datastyle):
430
- mocker.patch.object(thetadata_helper, "LUMIBOT_CACHE_FOLDER", tmpdir)
431
- cache_file = Path(tmpdir / "thetadata" / f"stock_SPY_1D_{datastyle}.parquet")
430
+ mocker.patch.object(thetadata_helper, "LUMIBOT_CACHE_FOLDER", str(tmpdir))
431
+ cache_file = thetadata_helper.build_cache_filename(Asset("SPY"), "1D", datastyle)
432
432
 
433
433
  # Empty DataFrame of df_all, don't write cache file
434
434
  thetadata_helper.update_cache(cache_file, df_all, df_cached)
@@ -550,8 +550,9 @@ def test_get_price_data_invokes_remote_cache_manager(tmp_path, monkeypatch):
550
550
  )
551
551
  def test_load_data_from_cache(mocker, tmpdir, df_cached, datastyle):
552
552
  # Setup some basics
553
- mocker.patch.object(thetadata_helper, "LUMIBOT_CACHE_FOLDER", tmpdir)
554
- cache_file = Path(tmpdir / "thetadata" / f"stock_SPY_1D_{datastyle}.parquet")
553
+ mocker.patch.object(thetadata_helper, "LUMIBOT_CACHE_FOLDER", str(tmpdir))
554
+ asset = Asset("SPY")
555
+ cache_file = thetadata_helper.build_cache_filename(asset, "1D", datastyle)
555
556
 
556
557
  # No cache file should return None (not raise)
557
558
  assert thetadata_helper.load_cache(cache_file) is None
@@ -1371,8 +1372,8 @@ class TestThetaDataChainsCaching:
1371
1372
 
1372
1373
  # CLEAR CACHE to ensure first call downloads fresh data
1373
1374
  # This prevents cache pollution from previous tests in the suite
1374
- # Chains are stored in: LUMIBOT_CACHE_FOLDER / "thetadata" / "option_chains"
1375
- chain_folder = Path(LUMIBOT_CACHE_FOLDER) / "thetadata" / "option_chains"
1375
+ # Chains are stored in: LUMIBOT_CACHE_FOLDER / "thetadata" / "option" / "option_chains"
1376
+ chain_folder = Path(LUMIBOT_CACHE_FOLDER) / "thetadata" / "option" / "option_chains"
1376
1377
  if chain_folder.exists():
1377
1378
  # Delete all AAPL chain cache files
1378
1379
  for cache_file in chain_folder.glob("AAPL_*.parquet"):
@@ -44,7 +44,7 @@ def count_cache_files():
44
44
  cache_dir = get_cache_dir()
45
45
  if not cache_dir.exists():
46
46
  return 0
47
- return len(list(cache_dir.glob("*.parquet")))
47
+ return sum(1 for _ in cache_dir.rglob("*.parquet"))
48
48
 
49
49
 
50
50
  class WeeklyMomentumOptionsStrategy(Strategy):