lumibot 4.1.3__py3-none-any.whl → 4.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of lumibot might be problematic. Click here for more details.

Files changed (163) hide show
  1. lumibot/backtesting/__init__.py +19 -5
  2. lumibot/backtesting/backtesting_broker.py +98 -18
  3. lumibot/backtesting/databento_backtesting.py +5 -686
  4. lumibot/backtesting/databento_backtesting_pandas.py +738 -0
  5. lumibot/backtesting/databento_backtesting_polars.py +860 -546
  6. lumibot/backtesting/fix_debug.py +37 -0
  7. lumibot/backtesting/thetadata_backtesting.py +9 -355
  8. lumibot/backtesting/thetadata_backtesting_pandas.py +1167 -0
  9. lumibot/brokers/alpaca.py +8 -1
  10. lumibot/brokers/schwab.py +12 -2
  11. lumibot/credentials.py +13 -0
  12. lumibot/data_sources/__init__.py +5 -8
  13. lumibot/data_sources/data_source.py +6 -2
  14. lumibot/data_sources/data_source_backtesting.py +30 -0
  15. lumibot/data_sources/databento_data.py +5 -390
  16. lumibot/data_sources/databento_data_pandas.py +440 -0
  17. lumibot/data_sources/databento_data_polars.py +15 -9
  18. lumibot/data_sources/pandas_data.py +30 -17
  19. lumibot/data_sources/polars_data.py +986 -0
  20. lumibot/data_sources/polars_mixin.py +472 -96
  21. lumibot/data_sources/polygon_data_polars.py +5 -0
  22. lumibot/data_sources/yahoo_data.py +9 -2
  23. lumibot/data_sources/yahoo_data_polars.py +5 -0
  24. lumibot/entities/__init__.py +15 -0
  25. lumibot/entities/asset.py +5 -28
  26. lumibot/entities/bars.py +89 -20
  27. lumibot/entities/data.py +29 -6
  28. lumibot/entities/data_polars.py +668 -0
  29. lumibot/entities/position.py +38 -4
  30. lumibot/strategies/_strategy.py +2 -1
  31. lumibot/strategies/strategy.py +61 -49
  32. lumibot/tools/backtest_cache.py +284 -0
  33. lumibot/tools/databento_helper.py +35 -35
  34. lumibot/tools/databento_helper_polars.py +738 -775
  35. lumibot/tools/futures_roll.py +251 -0
  36. lumibot/tools/indicators.py +135 -104
  37. lumibot/tools/polars_utils.py +142 -0
  38. lumibot/tools/thetadata_helper.py +1068 -134
  39. {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/METADATA +9 -1
  40. {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/RECORD +71 -147
  41. tests/backtest/test_databento.py +37 -6
  42. tests/backtest/test_databento_comprehensive_trading.py +8 -4
  43. tests/backtest/test_databento_parity.py +4 -2
  44. tests/backtest/test_debug_avg_fill_price.py +1 -1
  45. tests/backtest/test_example_strategies.py +11 -1
  46. tests/backtest/test_futures_edge_cases.py +3 -3
  47. tests/backtest/test_futures_single_trade.py +2 -2
  48. tests/backtest/test_futures_ultra_simple.py +2 -2
  49. tests/backtest/test_polars_lru_eviction.py +470 -0
  50. tests/backtest/test_yahoo.py +42 -0
  51. tests/test_asset.py +4 -4
  52. tests/test_backtest_cache_manager.py +149 -0
  53. tests/test_backtesting_data_source_env.py +6 -0
  54. tests/test_continuous_futures_resolution.py +60 -48
  55. tests/test_data_polars_parity.py +160 -0
  56. tests/test_databento_asset_validation.py +23 -5
  57. tests/test_databento_backtesting.py +1 -1
  58. tests/test_databento_backtesting_polars.py +312 -192
  59. tests/test_databento_data.py +220 -463
  60. tests/test_databento_live.py +10 -10
  61. tests/test_futures_roll.py +38 -0
  62. tests/test_indicator_subplots.py +101 -0
  63. tests/test_market_infinite_loop_bug.py +77 -3
  64. tests/test_polars_resample.py +67 -0
  65. tests/test_polygon_helper.py +46 -0
  66. tests/test_thetadata_backwards_compat.py +97 -0
  67. tests/test_thetadata_helper.py +222 -23
  68. tests/test_thetadata_pandas_verification.py +186 -0
  69. lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
  70. lumibot/__pycache__/constants.cpython-312.pyc +0 -0
  71. lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
  72. lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
  73. lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
  74. lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
  75. lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
  76. lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
  77. lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
  78. lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
  79. lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
  80. lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
  81. lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
  82. lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
  83. lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
  84. lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
  85. lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
  86. lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
  87. lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
  88. lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
  89. lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
  90. lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
  91. lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
  92. lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
  93. lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
  94. lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
  95. lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
  96. lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
  97. lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
  98. lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
  99. lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
  100. lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
  101. lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
  102. lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
  103. lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
  104. lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
  105. lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
  106. lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
  107. lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
  108. lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
  109. lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
  110. lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
  111. lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
  112. lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
  113. lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
  114. lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
  115. lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
  116. lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
  117. lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
  118. lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
  119. lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
  120. lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
  121. lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
  122. lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
  123. lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
  124. lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
  125. lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
  126. lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
  127. lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
  128. lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  129. lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
  130. lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  131. lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
  132. lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
  133. lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
  134. lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  135. lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
  136. lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
  137. lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
  138. lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
  139. lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
  140. lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
  141. lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
  142. lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
  143. lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
  144. lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
  145. lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
  146. lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
  147. lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
  148. lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
  149. lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
  150. lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
  151. lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
  152. lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
  153. lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
  154. lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
  155. lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
  156. lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
  157. lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
  158. lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
  159. lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
  160. lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
  161. {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/WHEEL +0 -0
  162. {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/licenses/LICENSE +0 -0
  163. {lumibot-4.1.3.dist-info → lumibot-4.2.1.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,11 @@ This implementation:
7
7
  4. Efficient caching with parquet files
8
8
  5. Vectorized operations only
9
9
  """
10
+ # NOTE: This module is intentionally disabled. The DataBento Polars migration only
11
+ # supports Polars for DataBento; other data sources must use the pandas implementations.
12
+ raise RuntimeError('Yahoo/Polygon Polars backends are not production-ready; use the pandas data sources instead.')
13
+
14
+
10
15
 
11
16
  from datetime import datetime, timedelta
12
17
  from decimal import Decimal
@@ -5,6 +5,7 @@ from .bar import Bar
5
5
  from .bars import Bars as _BarsBase
6
6
  from .chains import Chains
7
7
  from .data import Data as _DataBase
8
+ from .data_polars import DataPolars
8
9
  from .dataline import Dataline
9
10
  from .order import Order
10
11
  from .position import Position
@@ -14,3 +15,17 @@ from .trading_fee import TradingFee
14
15
  # Use base implementations directly
15
16
  Bars = _BarsBase
16
17
  Data = _DataBase
18
+ __all__ = [
19
+ "Asset",
20
+ "AssetsMapping",
21
+ "Bar",
22
+ "Bars",
23
+ "Chains",
24
+ "Data",
25
+ "DataPolars",
26
+ "Dataline",
27
+ "Order",
28
+ "Position",
29
+ "Quote",
30
+ "TradingFee",
31
+ ]
lumibot/entities/asset.py CHANGED
@@ -715,35 +715,12 @@ class Asset:
715
715
  # logger = logging.getLogger(__name__)
716
716
  # logger.info(f"[CONTRACT RESOLUTION] symbol={self.symbol}, reference_date={reference_date}, month={reference_date.month}, day={reference_date.day}")
717
717
 
718
- current_month = reference_date.month
719
- current_year = reference_date.year
720
- current_day = reference_date.day
718
+ from lumibot.tools import futures_roll
721
719
 
722
- # Use quarterly contracts (Mar, Jun, Sep, Dec) with a mid-month roll rule
723
- if current_month == 12 and current_day >= 15:
724
- target_month = 3
725
- target_year = current_year + 1
726
- elif current_month >= 10:
727
- target_month = 12
728
- target_year = current_year
729
- elif current_month == 9 and current_day >= 15:
730
- target_month = 12
731
- target_year = current_year
732
- elif current_month >= 7:
733
- target_month = 9
734
- target_year = current_year
735
- elif current_month == 6 and current_day >= 15:
736
- target_month = 9
737
- target_year = current_year
738
- elif current_month >= 4:
739
- target_month = 6
740
- target_year = current_year
741
- elif current_month == 3 and current_day >= 15:
742
- target_month = 6
743
- target_year = current_year
744
- else:
745
- target_month = 3
746
- target_year = current_year
720
+ target_year, target_month = futures_roll.determine_contract_year_month(
721
+ self.symbol,
722
+ reference_date,
723
+ )
747
724
 
748
725
  month_code = FUTURES_MONTH_CODES.get(target_month, "U")
749
726
  base_contract = f"{self.symbol}{month_code}"
lumibot/entities/bars.py CHANGED
@@ -179,8 +179,9 @@ class Bars:
179
179
  self.quote = quote
180
180
  self._raw = raw
181
181
  self._return_polars = return_polars
182
- # Cache for on-demand conversions to avoid repeated expensive copies
182
+ # Caches for on-demand conversions to avoid repeated expensive copies
183
183
  self._polars_cache = None
184
+ self._pandas_cache = None
184
185
  self._tzinfo = self._normalize_tzinfo(tzinfo)
185
186
 
186
187
  # Check if empty
@@ -204,9 +205,20 @@ class Bars:
204
205
  pl.col("close").pct_change().alias("return")
205
206
  ])
206
207
 
208
+ if "datetime" in df.columns and self._tzinfo is not None:
209
+ target_tz = getattr(self._tzinfo, "zone", None) or getattr(self._tzinfo, "key", None)
210
+ if target_tz:
211
+ current_dtype = df.schema.get("datetime")
212
+ current_tz = getattr(current_dtype, "time_zone", None)
213
+ if current_tz != target_tz:
214
+ df = df.with_columns(
215
+ pl.col("datetime").dt.convert_time_zone(target_tz)
216
+ )
217
+
207
218
  if return_polars:
208
219
  # Keep as polars
209
220
  self._df = df
221
+ self._pandas_cache = None
210
222
  else:
211
223
  # Convert to pandas and track the conversion
212
224
  tracker = PolarsConversionTracker()
@@ -219,6 +231,7 @@ class Bars:
219
231
  self._df = self._df.set_index(col_name)
220
232
  break
221
233
  self._apply_timezone()
234
+ self._pandas_cache = None
222
235
  else:
223
236
  # Already pandas, keep it as is
224
237
  self._df = df
@@ -231,19 +244,38 @@ class Bars:
231
244
  self._df["return"] = df["close"].pct_change()
232
245
 
233
246
  self._apply_timezone()
247
+ if self._return_polars:
248
+ self._pandas_cache = self._df
249
+ self._df = self._convert_pandas_to_polars(self._df)
250
+ self._polars_cache = None
234
251
 
235
252
  @property
236
253
  def df(self):
237
- """Return the DataFrame"""
238
- return self._df
254
+ """Return the active DataFrame representation based on return_polars flag."""
255
+ return self.polars_df if self._return_polars else self.pandas_df
239
256
 
240
257
  @df.setter
241
258
  def df(self, value):
242
- """Allow setting the DataFrame"""
259
+ """Allow setting the DataFrame while keeping caches in sync."""
243
260
  self._df = value
244
- # Invalidate cached converted forms when df changes
245
261
  self._polars_cache = None
246
- self._apply_timezone()
262
+ self._pandas_cache = None
263
+
264
+ if isinstance(value, pd.DataFrame):
265
+ self._apply_timezone()
266
+ if self._return_polars:
267
+ self._pandas_cache = value
268
+ self._df = self._convert_pandas_to_polars(value)
269
+ elif isinstance(value, pl.DataFrame) and not self._return_polars:
270
+ tracker = PolarsConversionTracker()
271
+ tracker.track_conversion(self.asset.symbol if hasattr(self.asset, 'symbol') else str(self.asset))
272
+ pandas_df = value.to_pandas()
273
+ for col_name in ['datetime', 'timestamp', 'date', 'time']:
274
+ if col_name in pandas_df.columns:
275
+ pandas_df = pandas_df.set_index(col_name)
276
+ break
277
+ self._df = pandas_df
278
+ self._apply_timezone()
247
279
 
248
280
  @property
249
281
  def polars_df(self):
@@ -254,12 +286,29 @@ class Bars:
254
286
  # Convert pandas to polars once and cache
255
287
  if self._polars_cache is not None:
256
288
  return self._polars_cache
257
- if hasattr(self._df, 'index') and getattr(self._df.index, 'name', None):
258
- self._polars_cache = pl.from_pandas(self._df.reset_index())
259
- else:
260
- self._polars_cache = pl.from_pandas(self._df)
289
+ self._polars_cache = self._convert_pandas_to_polars(self._df)
261
290
  return self._polars_cache
262
291
 
292
+ @property
293
+ def pandas_df(self):
294
+ """Return as Pandas DataFrame, converting on demand if required."""
295
+ if isinstance(self._df, pd.DataFrame):
296
+ return self._df
297
+ if self._pandas_cache is not None:
298
+ return self._pandas_cache
299
+
300
+ tracker = PolarsConversionTracker()
301
+ tracker.track_conversion(self.asset.symbol if hasattr(self.asset, 'symbol') else str(self.asset))
302
+
303
+ pandas_df = self._df.to_pandas()
304
+ for col_name in ['datetime', 'timestamp', 'date', 'time']:
305
+ if col_name in pandas_df.columns:
306
+ pandas_df = pandas_df.set_index(col_name)
307
+ break
308
+
309
+ self._pandas_cache = self._apply_timezone(pandas_df)
310
+ return self._pandas_cache
311
+
263
312
  def __repr__(self):
264
313
  return repr(self.df)
265
314
 
@@ -268,7 +317,12 @@ class Bars:
268
317
 
269
318
  def __len__(self):
270
319
  """Return the number of bars (rows) in the DataFrame"""
271
- return len(self.df)
320
+ if isinstance(self._df, pl.DataFrame):
321
+ return self._df.height
322
+ if isinstance(self._df, pd.DataFrame):
323
+ return len(self._df)
324
+ df = self.df
325
+ return df.height if isinstance(df, pl.DataFrame) else len(df)
272
326
 
273
327
  @property
274
328
  def empty(self):
@@ -285,24 +339,39 @@ class Bars:
285
339
  return pytz.timezone(tzinfo)
286
340
  return tzinfo
287
341
 
288
- def _apply_timezone(self):
289
- if not isinstance(self._df, pd.DataFrame):
290
- return
291
- if not isinstance(self._df.index, pd.DatetimeIndex):
292
- return
342
+ def _apply_timezone(self, df=None):
343
+ target_df = self._df if df is None else df
344
+ if not isinstance(target_df, pd.DataFrame):
345
+ return target_df
346
+ if not isinstance(target_df.index, pd.DatetimeIndex):
347
+ return target_df
293
348
 
294
349
  tz = self._tzinfo or LUMIBOT_DEFAULT_PYTZ
295
350
  if isinstance(tz, str):
296
351
  tz = pytz.timezone(tz)
297
352
 
298
353
  try:
299
- if self._df.index.tz is None:
300
- self._df.index = self._df.index.tz_localize(tz)
354
+ if target_df.index.tz is None:
355
+ target_df.index = target_df.index.tz_localize(tz)
301
356
  else:
302
- self._df.index = self._df.index.tz_convert(tz)
357
+ target_df.index = target_df.index.tz_convert(tz)
303
358
  self._tzinfo = tz
304
359
  except Exception:
305
- pass
360
+ return target_df
361
+
362
+ if df is None:
363
+ self._df = target_df
364
+ return target_df
365
+
366
+ def _convert_pandas_to_polars(self, pandas_df: pd.DataFrame) -> pl.DataFrame:
367
+ """Convert a pandas DataFrame (possibly with datetime index) to Polars."""
368
+ df_to_convert = pandas_df.copy()
369
+ if isinstance(df_to_convert.index, pd.DatetimeIndex):
370
+ df_to_convert = df_to_convert.reset_index()
371
+ first_col = df_to_convert.columns[0]
372
+ if first_col != "datetime":
373
+ df_to_convert = df_to_convert.rename(columns={first_col: "datetime"})
374
+ return pl.from_pandas(df_to_convert)
306
375
 
307
376
  @classmethod
308
377
  def parse_bar_list(cls, bar_list, source, asset):
lumibot/entities/data.py CHANGED
@@ -411,6 +411,14 @@ class Data:
411
411
 
412
412
  length = kwargs.get("length", 1)
413
413
  timeshift = kwargs.get("timeshift", 0)
414
+
415
+ if isinstance(timeshift, datetime.timedelta):
416
+ if self.timestep == "day":
417
+ timeshift = int(timeshift.total_seconds() / (24 * 3600))
418
+ else:
419
+ timeshift = int(timeshift.total_seconds() / 60)
420
+ kwargs["timeshift"] = timeshift
421
+
414
422
  data_index = i + 1 - length - timeshift
415
423
  is_data = data_index >= 0
416
424
  if not is_data:
@@ -437,8 +445,8 @@ class Data:
437
445
  The number of periods to get the last price.
438
446
  timestep : str
439
447
  The frequency of the data to get the last price.
440
- timeshift : int
441
- The number of periods to shift the data.
448
+ timeshift : int | datetime.timedelta
449
+ The number of periods to shift the data, or a timedelta that will be converted to periods.
442
450
 
443
451
  Returns
444
452
  -------
@@ -462,8 +470,8 @@ class Data:
462
470
  The number of periods to get the last price.
463
471
  timestep : str
464
472
  The frequency of the data to get the last price.
465
- timeshift : int
466
- The number of periods to shift the data.
473
+ timeshift : int | datetime.timedelta
474
+ The number of periods to shift the data, or a timedelta that will be converted to periods.
467
475
 
468
476
  Returns
469
477
  -------
@@ -547,12 +555,27 @@ class Data:
547
555
 
548
556
  """
549
557
 
550
- # Get bars.
558
+ if isinstance(timeshift, datetime.timedelta):
559
+ if self.timestep == "day":
560
+ timeshift = int(timeshift.total_seconds() / (24 * 3600))
561
+ else:
562
+ timeshift = int(timeshift.total_seconds() / 60)
563
+
551
564
  end_row = self.get_iter_count(dt) - timeshift
552
- start_row = end_row - length
553
565
 
566
+ data_len = len(next(iter(self.datalines.values())).dataline) if self.datalines else 0
567
+ if end_row > data_len:
568
+ end_row = data_len
569
+ if end_row < 0:
570
+ end_row = 0
571
+
572
+ start_row = end_row - length
554
573
  if start_row < 0:
555
574
  start_row = 0
575
+ if start_row > end_row:
576
+ start_row = end_row
577
+ if start_row == end_row and end_row > 0:
578
+ start_row = max(0, end_row - 1)
556
579
 
557
580
  # Cast both start_row and end_row to int
558
581
  start_row = int(start_row)