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.
- lumibot/backtesting/thetadata_backtesting_pandas.py +43 -54
- lumibot/tools/thetadata_helper.py +73 -58
- {lumibot-4.2.0.dist-info → lumibot-4.2.2.dist-info}/METADATA +1 -1
- {lumibot-4.2.0.dist-info → lumibot-4.2.2.dist-info}/RECORD +10 -10
- tests/test_backtest_cache_manager.py +7 -7
- tests/test_thetadata_helper.py +10 -9
- tests/test_thetadata_pandas_verification.py +1 -1
- {lumibot-4.2.0.dist-info → lumibot-4.2.2.dist-info}/WHEEL +0 -0
- {lumibot-4.2.0.dist-info → lumibot-4.2.2.dist-info}/licenses/LICENSE +0 -0
- {lumibot-4.2.0.dist-info → lumibot-4.2.2.dist-info}/top_level.txt +0 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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)
|
|
@@ -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=
|
|
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
|
|
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.
|
|
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=
|
|
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=
|
|
238
|
-
tests/test_thetadata_pandas_verification.py,sha256=
|
|
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.
|
|
284
|
-
lumibot-4.2.
|
|
285
|
-
lumibot-4.2.
|
|
286
|
-
lumibot-4.2.
|
|
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" / "
|
|
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/
|
|
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" / "
|
|
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/
|
|
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" / "
|
|
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" / "
|
|
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/
|
|
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
|
|
tests/test_thetadata_helper.py
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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"):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|