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
@@ -6,11 +6,25 @@ from .interactive_brokers_rest_backtesting import InteractiveBrokersRESTBacktest
6
6
  from .pandas_backtesting import PandasDataBacktesting
7
7
  from .polygon_backtesting import PolygonDataBacktesting
8
8
  from .thetadata_backtesting import ThetaDataBacktesting
9
+ from .thetadata_backtesting_pandas import ThetaDataBacktestingPandas
9
10
  from .yahoo_backtesting import YahooDataBacktesting
10
11
 
11
- # Import DataBento backtesting
12
- # Polars version (NEW DEFAULT - faster performance)
13
- from lumibot.data_sources.databento_data_polars_backtesting import DataBentoDataPolarsBacktesting as DataBentoDataBacktesting
12
+ from .databento_backtesting import DataBentoDataBacktesting
13
+ from .databento_backtesting_pandas import DataBentoDataBacktestingPandas
14
+ from .databento_backtesting_polars import DataBentoDataBacktestingPolars
14
15
 
15
- # Pandas version (stable fallback, kept for compatibility)
16
- from .databento_backtesting import DataBentoDataBacktesting as DataBentoDataBacktestingPandas
16
+ __all__ = [
17
+ "AlpacaBacktesting",
18
+ "AlphaVantageBacktesting",
19
+ "BacktestingBroker",
20
+ "CcxtBacktesting",
21
+ "InteractiveBrokersRESTBacktesting",
22
+ "PandasDataBacktesting",
23
+ "PolygonDataBacktesting",
24
+ "ThetaDataBacktesting",
25
+ "ThetaDataBacktestingPandas",
26
+ "YahooDataBacktesting",
27
+ "DataBentoDataBacktesting",
28
+ "DataBentoDataBacktestingPandas",
29
+ "DataBentoDataBacktestingPolars",
30
+ ]
@@ -1,3 +1,4 @@
1
+ import math
1
2
  import traceback
2
3
  import threading
3
4
  from collections import OrderedDict
@@ -1353,17 +1354,21 @@ class BacktestingBroker(Broker):
1353
1354
 
1354
1355
  # Get the OHLCV data for the asset if we're using the YAHOO, CCXT data source
1355
1356
  data_source_name = self.data_source.SOURCE.upper()
1356
- if data_source_name in ["CCXT", "YAHOO", "ALPACA", "DATABENTO"]:
1357
- # Default to backing up one minute so fills use the next bar, consistent with other sources.
1357
+ if data_source_name in ["CCXT", "YAHOO", "ALPACA", "DATABENTO", "DATABENTO_POLARS"]:
1358
+ # Negative deltas here are intentional: _pull_source_symbol_bars subtracts the offset, so
1359
+ # passing -1 minute yields an effective +1 minute guard that keeps us on the previously
1360
+ # completed bar. See tests/*_lookahead for regression coverage.
1358
1361
  timeshift = timedelta(minutes=-1)
1359
- if data_source_name == "DATABENTO":
1360
- # DataBento mimics Polygon by requesting two bars to guard against gaps.
1362
+ if data_source_name in {"DATABENTO", "DATABENTO_POLARS"}:
1363
+ # DataBento feeds can skip minutes around maintenance windows. Giving it a two-minute
1364
+ # cushion mirrors the legacy Polygon behaviour and avoids falling through gaps.
1361
1365
  timeshift = timedelta(minutes=-2)
1362
1366
  elif data_source_name == "YAHOO":
1363
- # Yahoo uses day bars; shift one day instead to mirror legacy behavior.
1367
+ # Yahoo daily bars are stamped at the close (16:00). A one-day backstep keeps fills on
1368
+ # the previous session so we never peek at the in-progress bar.
1364
1369
  timeshift = timedelta(days=-1)
1365
1370
  elif data_source_name == "ALPACA":
1366
- # Alpaca minute bars are aligned to the current iteration already.
1371
+ # Alpaca minute bars line up with our clock already; no offset needed.
1367
1372
  timeshift = None
1368
1373
 
1369
1374
  ohlc = self.data_source.get_historical_prices(
@@ -1373,6 +1378,23 @@ class BacktestingBroker(Broker):
1373
1378
  timeshift=timeshift,
1374
1379
  )
1375
1380
 
1381
+ if (
1382
+ ohlc is None
1383
+ or getattr(ohlc, "df", None) is None
1384
+ or (hasattr(ohlc.df, "empty") and ohlc.df.empty)
1385
+ ):
1386
+ if strategy is not None:
1387
+ display_symbol = getattr(order.asset, "symbol", order.asset)
1388
+ order_identifier = getattr(order, "identifier", None)
1389
+ if order_identifier is None:
1390
+ order_identifier = getattr(order, "id", "<unknown>")
1391
+ strategy.log_message(
1392
+ f"[DIAG] No historical bars returned for {display_symbol} at {self.datetime}; "
1393
+ f"pending {order.order_type} id={order_identifier}",
1394
+ color="yellow",
1395
+ )
1396
+ continue
1397
+
1376
1398
  # Handle both pandas and polars DataFrames
1377
1399
  if hasattr(ohlc.df, 'index'): # pandas
1378
1400
  dt = ohlc.df.index[-1]
@@ -1406,6 +1428,16 @@ class BacktestingBroker(Broker):
1406
1428
  )
1407
1429
  # Check if we got any ohlc data
1408
1430
  if ohlc is None or ohlc.empty:
1431
+ if strategy is not None:
1432
+ display_symbol = getattr(order.asset, "symbol", order.asset)
1433
+ order_identifier = getattr(order, "identifier", None)
1434
+ if order_identifier is None:
1435
+ order_identifier = getattr(order, "id", "<unknown>")
1436
+ strategy.log_message(
1437
+ f"[DIAG] No pandas bars for {display_symbol} at {self.datetime}; "
1438
+ f"canceling {order.order_type} id={order_identifier}",
1439
+ color="yellow",
1440
+ )
1409
1441
  self.cancel_order(order)
1410
1442
  continue
1411
1443
 
@@ -1502,41 +1534,89 @@ class BacktestingBroker(Broker):
1502
1534
  strategy=strategy,
1503
1535
  )
1504
1536
  else:
1537
+ if strategy is not None:
1538
+ display_symbol = getattr(order.asset, "symbol", order.asset)
1539
+ order_identifier = getattr(order, "identifier", None)
1540
+ if order_identifier is None:
1541
+ order_identifier = getattr(order, "id", "<unknown>")
1542
+ detail = (
1543
+ f"limit={order.limit_price}, high={high}, low={low}"
1544
+ if order.order_type == Order.OrderType.LIMIT
1545
+ else f"type={order.order_type}, high={high}, low={low}, stop={getattr(order, 'stop_price', None)}"
1546
+ )
1547
+ strategy.log_message(
1548
+ f"[DIAG] Order remained open for {display_symbol} ({detail}) "
1549
+ f"id={order_identifier} at {self.datetime}",
1550
+ color="yellow",
1551
+ )
1505
1552
  continue
1506
1553
 
1507
1554
  # After handling all pending orders, cash settle any residual expired contracts.
1508
1555
  self.process_expired_option_contracts(strategy)
1509
1556
 
1557
+ def _coerce_price(self, value):
1558
+ """Convert numeric inputs to float when possible for safe comparisons."""
1559
+ if value is None:
1560
+ return None
1561
+ try:
1562
+ return float(value)
1563
+ except (TypeError, ValueError):
1564
+ return value
1565
+
1566
+ def _is_invalid_price(self, value):
1567
+ """Determine whether a price is unusable (None or NaN)."""
1568
+ if value is None:
1569
+ return True
1570
+ if isinstance(value, float) and math.isnan(value):
1571
+ return True
1572
+ return False
1573
+
1510
1574
  def limit_order(self, limit_price, side, open_, high, low):
1511
1575
  """Limit order logic."""
1576
+ open_val = self._coerce_price(open_)
1577
+ high_val = self._coerce_price(high)
1578
+ low_val = self._coerce_price(low)
1579
+ limit_val = self._coerce_price(limit_price)
1580
+
1581
+ if any(self._is_invalid_price(val) for val in (open_val, high_val, low_val, limit_val)):
1582
+ return None
1583
+
1512
1584
  # Gap Up case: Limit wasn't triggered by previous candle but current candle opens higher, fill it now
1513
- if side == "sell" and limit_price <= open_:
1514
- return open_
1585
+ if side == "sell" and limit_val <= open_val:
1586
+ return open_val
1515
1587
 
1516
1588
  # Gap Down case: Limit wasn't triggered by previous candle but current candle opens lower, fill it now
1517
- if side == "buy" and limit_price >= open_:
1518
- return open_
1589
+ if side == "buy" and limit_val >= open_val:
1590
+ return open_val
1519
1591
 
1520
1592
  # Current candle triggered limit normally
1521
- if low <= limit_price <= high:
1522
- return limit_price
1593
+ if low_val <= limit_val <= high_val:
1594
+ return limit_val
1523
1595
 
1524
1596
  # Limit has not been met
1525
1597
  return None
1526
1598
 
1527
1599
  def stop_order(self, stop_price, side, open_, high, low):
1528
1600
  """Stop order logic."""
1601
+ open_val = self._coerce_price(open_)
1602
+ high_val = self._coerce_price(high)
1603
+ low_val = self._coerce_price(low)
1604
+ stop_val = self._coerce_price(stop_price)
1605
+
1606
+ if any(self._is_invalid_price(val) for val in (open_val, high_val, low_val, stop_val)):
1607
+ return None
1608
+
1529
1609
  # Gap Down case: Stop wasn't triggered by previous candle but current candle opens lower, fill it now
1530
- if side == "sell" and stop_price >= open_:
1531
- return open_
1610
+ if side == "sell" and stop_val >= open_val:
1611
+ return open_val
1532
1612
 
1533
1613
  # Gap Up case: Stop wasn't triggered by previous candle but current candle opens higher, fill it now
1534
- if side == "buy" and stop_price <= open_:
1535
- return open_
1614
+ if side == "buy" and stop_val <= open_val:
1615
+ return open_val
1536
1616
 
1537
1617
  # Current candle triggered stop normally
1538
- if low <= stop_price <= high:
1539
- return stop_price
1618
+ if low_val <= stop_val <= high_val:
1619
+ return stop_val
1540
1620
 
1541
1621
  # Stop has not been met
1542
1622
  return None