lumibot 4.0.23__tar.gz → 4.2.9__tar.gz

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 (290) hide show
  1. {lumibot-4.0.23/lumibot.egg-info → lumibot-4.2.9}/PKG-INFO +21 -3
  2. {lumibot-4.0.23 → lumibot-4.2.9}/README.md +7 -0
  3. lumibot-4.2.9/lumibot/backtesting/__init__.py +30 -0
  4. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/backtesting/backtesting_broker.py +307 -27
  5. lumibot-4.2.9/lumibot/backtesting/databento_backtesting.py +7 -0
  6. lumibot-4.0.23/lumibot/backtesting/databento_backtesting.py → lumibot-4.2.9/lumibot/backtesting/databento_backtesting_pandas.py +234 -38
  7. lumibot-4.2.9/lumibot/backtesting/databento_backtesting_polars.py +991 -0
  8. lumibot-4.2.9/lumibot/backtesting/fix_debug.py +37 -0
  9. lumibot-4.2.9/lumibot/backtesting/thetadata_backtesting.py +12 -0
  10. lumibot-4.2.9/lumibot/backtesting/thetadata_backtesting_pandas.py +1168 -0
  11. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/brokers/alpaca.py +19 -2
  12. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/brokers/schwab.py +12 -2
  13. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/components/options_helper.py +176 -57
  14. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/credentials.py +16 -0
  15. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/__init__.py +5 -8
  16. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/data_source.py +6 -2
  17. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/data_source_backtesting.py +33 -5
  18. lumibot-4.2.9/lumibot/data_sources/databento_data.py +7 -0
  19. lumibot-4.0.23/lumibot/data_sources/databento_data.py → lumibot-4.2.9/lumibot/data_sources/databento_data_pandas.py +77 -29
  20. lumibot-4.0.23/lumibot/data_sources/databento_data_polars_live.py → lumibot-4.2.9/lumibot/data_sources/databento_data_polars.py +15 -9
  21. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/pandas_data.py +36 -20
  22. lumibot-4.2.9/lumibot/data_sources/polars_data.py +986 -0
  23. lumibot-4.2.9/lumibot/data_sources/polars_mixin.py +853 -0
  24. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/polygon_data_polars.py +5 -0
  25. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/tradier_data.py +2 -1
  26. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/yahoo_data.py +9 -2
  27. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/yahoo_data_polars.py +5 -0
  28. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/entities/__init__.py +15 -0
  29. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/entities/asset.py +13 -28
  30. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/entities/bars.py +89 -20
  31. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/entities/data.py +29 -6
  32. lumibot-4.2.9/lumibot/entities/data_polars.py +668 -0
  33. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/entities/order.py +1 -1
  34. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/entities/position.py +38 -4
  35. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/entities/quote.py +14 -0
  36. lumibot-4.2.9/lumibot/resources/ThetaTerminal.jar +0 -0
  37. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/strategies/_strategy.py +154 -41
  38. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/strategies/strategy.py +66 -55
  39. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/strategies/strategy_executor.py +3 -5
  40. lumibot-4.2.9/lumibot/tools/backtest_cache.py +284 -0
  41. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/ccxt_data_store.py +1 -1
  42. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/databento_helper.py +427 -145
  43. lumibot-4.2.9/lumibot/tools/databento_helper_polars.py +1257 -0
  44. lumibot-4.2.9/lumibot/tools/futures_roll.py +251 -0
  45. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/indicators.py +135 -104
  46. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/lumibot_logger.py +32 -17
  47. lumibot-4.2.9/lumibot/tools/polars_utils.py +142 -0
  48. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/polygon_helper.py +65 -0
  49. lumibot-4.2.9/lumibot/tools/thetadata_helper.py +2348 -0
  50. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/traders/trader.py +1 -1
  51. {lumibot-4.0.23 → lumibot-4.2.9/lumibot.egg-info}/PKG-INFO +21 -3
  52. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot.egg-info/SOURCES.txt +33 -2
  53. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot.egg-info/requires.txt +1 -1
  54. {lumibot-4.0.23 → lumibot-4.2.9}/setup.py +6 -3
  55. lumibot-4.2.9/tests/backtest/profile_thetadata_vs_polygon.py +255 -0
  56. lumibot-4.2.9/tests/backtest/test_accuracy_verification.py +244 -0
  57. lumibot-4.2.9/tests/backtest/test_daily_data_timestamp_comparison.py +801 -0
  58. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_databento.py +42 -7
  59. lumibot-4.2.9/tests/backtest/test_databento_comprehensive_trading.py +547 -0
  60. lumibot-4.2.9/tests/backtest/test_databento_parity.py +105 -0
  61. lumibot-4.2.9/tests/backtest/test_debug_avg_fill_price.py +112 -0
  62. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_dividends.py +8 -3
  63. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_example_strategies.py +64 -47
  64. lumibot-4.2.9/tests/backtest/test_futures_edge_cases.py +484 -0
  65. lumibot-4.2.9/tests/backtest/test_futures_single_trade.py +270 -0
  66. lumibot-4.2.9/tests/backtest/test_futures_ultra_simple.py +191 -0
  67. lumibot-4.2.9/tests/backtest/test_index_data_verification.py +348 -0
  68. lumibot-4.2.9/tests/backtest/test_polars_lru_eviction.py +470 -0
  69. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_polygon.py +45 -24
  70. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_thetadata.py +246 -60
  71. lumibot-4.2.9/tests/backtest/test_thetadata_comprehensive.py +729 -0
  72. lumibot-4.2.9/tests/backtest/test_thetadata_vs_polygon.py +557 -0
  73. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_yahoo.py +43 -2
  74. {lumibot-4.0.23 → lumibot-4.2.9}/tests/conftest.py +20 -0
  75. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_asset.py +4 -4
  76. lumibot-4.2.9/tests/test_backtest_cache_manager.py +149 -0
  77. lumibot-4.2.9/tests/test_backtesting_data_source_env.py +292 -0
  78. lumibot-4.2.9/tests/test_backtesting_datetime_normalization.py +94 -0
  79. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_backtesting_quiet_logs_complete.py +10 -11
  80. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_continuous_futures_resolution.py +60 -48
  81. lumibot-4.2.9/tests/test_data_polars_parity.py +160 -0
  82. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_databento_asset_validation.py +23 -5
  83. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_databento_backtesting.py +45 -1
  84. lumibot-4.2.9/tests/test_databento_backtesting_polars.py +321 -0
  85. lumibot-4.2.9/tests/test_databento_data.py +250 -0
  86. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_databento_helper.py +82 -91
  87. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_databento_live.py +10 -10
  88. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_databento_timezone_fixes.py +21 -4
  89. lumibot-4.2.9/tests/test_futures_roll.py +38 -0
  90. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_get_historical_prices.py +6 -6
  91. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_indicator_subplots.py +101 -0
  92. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_market_infinite_loop_bug.py +77 -3
  93. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_options_helper.py +207 -43
  94. lumibot-4.2.9/tests/test_polars_resample.py +67 -0
  95. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_polygon_helper.py +67 -13
  96. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_projectx_timestep_alias.py +1 -2
  97. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_quiet_logs_requirements.py +5 -5
  98. lumibot-4.2.9/tests/test_strategy_price_guard.py +50 -0
  99. lumibot-4.2.9/tests/test_thetadata_backwards_compat.py +97 -0
  100. lumibot-4.2.9/tests/test_thetadata_helper.py +1718 -0
  101. lumibot-4.2.9/tests/test_thetadata_pandas_verification.py +186 -0
  102. lumibot-4.0.23/lumibot/backtesting/__init__.py +0 -15
  103. lumibot-4.0.23/lumibot/backtesting/databento_backtesting_polars.py +0 -677
  104. lumibot-4.0.23/lumibot/backtesting/thetadata_backtesting.py +0 -337
  105. lumibot-4.0.23/lumibot/data_sources/databento_data_polars_backtesting.py +0 -490
  106. lumibot-4.0.23/lumibot/data_sources/polars_mixin.py +0 -372
  107. lumibot-4.0.23/lumibot/tools/databento_helper_polars.py +0 -1225
  108. lumibot-4.0.23/lumibot/tools/thetadata_helper.py +0 -617
  109. lumibot-4.0.23/tests/test_databento_backtesting_polars.py +0 -201
  110. lumibot-4.0.23/tests/test_databento_data.py +0 -493
  111. lumibot-4.0.23/tests/test_thetadata_helper.py +0 -965
  112. {lumibot-4.0.23 → lumibot-4.2.9}/LICENSE +0 -0
  113. {lumibot-4.0.23 → lumibot-4.2.9}/MANIFEST.in +0 -0
  114. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/__init__.py +0 -0
  115. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/backtesting/alpaca_backtesting.py +0 -0
  116. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/backtesting/alpha_vantage_backtesting.py +0 -0
  117. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/backtesting/ccxt_backtesting.py +0 -0
  118. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/backtesting/interactive_brokers_rest_backtesting.py +0 -0
  119. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/backtesting/pandas_backtesting.py +0 -0
  120. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/backtesting/polygon_backtesting.py +0 -0
  121. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/backtesting/yahoo_backtesting.py +0 -0
  122. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/brokers/__init__.py +0 -0
  123. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/brokers/bitunix.py +0 -0
  124. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/brokers/broker.py +0 -0
  125. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/brokers/ccxt.py +0 -0
  126. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/brokers/example_broker.py +0 -0
  127. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/brokers/interactive_brokers.py +0 -0
  128. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/brokers/interactive_brokers_rest.py +0 -0
  129. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/brokers/projectx.py +0 -0
  130. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/brokers/tradier.py +0 -0
  131. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/brokers/tradovate.py +0 -0
  132. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/components/__init__.py +0 -0
  133. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/components/configs_helper.py +0 -0
  134. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/components/drift_rebalancer_logic.py +0 -0
  135. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/components/grok_helper.py +0 -0
  136. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/components/perplexity_helper.py +0 -0
  137. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/components/quiver_helper.py +0 -0
  138. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/components/vix_helper.py +0 -0
  139. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/constants.py +0 -0
  140. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/alpaca_data.py +0 -0
  141. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/alpha_vantage_data.py +0 -0
  142. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/bitunix_data.py +0 -0
  143. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/ccxt_backtesting_data.py +0 -0
  144. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/ccxt_data.py +0 -0
  145. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/example_broker_data.py +0 -0
  146. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/exceptions.py +0 -0
  147. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/interactive_brokers_data.py +0 -0
  148. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/interactive_brokers_rest_data.py +0 -0
  149. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/projectx_data.py +0 -0
  150. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/schwab_data.py +0 -0
  151. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/data_sources/tradovate_data.py +0 -0
  152. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/entities/bar.py +0 -0
  153. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/entities/chains.py +0 -0
  154. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/entities/dataline.py +0 -0
  155. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/entities/trading_fee.py +0 -0
  156. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/__init__.py +0 -0
  157. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/bitunix_futures_example.py +0 -0
  158. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/ccxt_backtesting_example.py +0 -0
  159. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/classic_60_40.py +0 -0
  160. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/classic_60_40_config.py +0 -0
  161. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/crypto_50_50.py +0 -0
  162. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/crypto_50_50_config.py +0 -0
  163. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/crypto_important_functions.py +0 -0
  164. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/drift_rebalancer.py +0 -0
  165. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/forex_hold_to_expiry.py +0 -0
  166. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/futures_hold_to_expiry.py +0 -0
  167. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/lifecycle_logger.py +0 -0
  168. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/options_hold_to_expiry.py +0 -0
  169. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/schedule_function.py +0 -0
  170. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/simple_start_single_file.py +0 -0
  171. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/stock_bracket.py +0 -0
  172. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/stock_buy_and_hold.py +0 -0
  173. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/stock_diversified_leverage.py +0 -0
  174. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/stock_limit_and_trailing_stops.py +0 -0
  175. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/stock_momentum.py +0 -0
  176. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/stock_oco.py +0 -0
  177. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/strangle.py +0 -0
  178. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/example_strategies/test_broker_functions.py +0 -0
  179. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/resources/conf.yaml +0 -0
  180. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/strategies/__init__.py +0 -0
  181. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/strategies/session_manager.py +0 -0
  182. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/__init__.py +0 -0
  183. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/alpaca_helpers.py +0 -0
  184. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/bitunix_helpers.py +0 -0
  185. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/black_scholes.py +0 -0
  186. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/debugers.py +0 -0
  187. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/decorators.py +0 -0
  188. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/futures_symbols.py +0 -0
  189. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/helpers.py +0 -0
  190. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/lumibot_time.py +0 -0
  191. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/pandas.py +0 -0
  192. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/polygon_helper_async.py +0 -0
  193. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/polygon_helper_polars_optimized.py +0 -0
  194. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/projectx_helpers.py +0 -0
  195. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/schwab_helper.py +0 -0
  196. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/types.py +0 -0
  197. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/yahoo_helper.py +0 -0
  198. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/tools/yahoo_helper_polars_optimized.py +0 -0
  199. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/traders/__init__.py +0 -0
  200. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/traders/debug_log_trader.py +0 -0
  201. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/trading_builtins/__init__.py +0 -0
  202. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/trading_builtins/custom_stream.py +0 -0
  203. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot/trading_builtins/safe_list.py +0 -0
  204. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot.egg-info/dependency_links.txt +0 -0
  205. {lumibot-4.0.23 → lumibot-4.2.9}/lumibot.egg-info/top_level.txt +0 -0
  206. {lumibot-4.0.23 → lumibot-4.2.9}/pyproject.toml +0 -0
  207. {lumibot-4.0.23 → lumibot-4.2.9}/setup.cfg +0 -0
  208. {lumibot-4.0.23 → lumibot-4.2.9}/tests/__init__.py +0 -0
  209. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/__init__.py +0 -0
  210. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/conftest.py +0 -0
  211. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/performance_tracker.py +0 -0
  212. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_backtesting_broker_processing.py +0 -0
  213. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_buy_hold_quiet_logs_full_run.py +0 -0
  214. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_crypto_cash_regressions.py +0 -0
  215. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_failing_backtest.py +0 -0
  216. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_multileg_backtest.py +0 -0
  217. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_pandas_backtest.py +0 -0
  218. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_passing_trader_into_backtest.py +0 -0
  219. {lumibot-4.0.23 → lumibot-4.2.9}/tests/backtest/test_strategy_executor.py +0 -0
  220. {lumibot-4.0.23 → lumibot-4.2.9}/tests/fixtures.py +0 -0
  221. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_alpaca.py +0 -0
  222. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_alpaca_auth_fix.py +0 -0
  223. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_alpaca_backtesting.py +0 -0
  224. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_alpaca_data.py +0 -0
  225. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_alpaca_helpers.py +0 -0
  226. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_alpaca_multileg_fix.py +0 -0
  227. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_alpaca_oauth.py +0 -0
  228. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_apscheduler_warnings.py +0 -0
  229. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_asset_auto_expiry.py +0 -0
  230. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_auto_market_inference.py +0 -0
  231. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_backtesting_broker.py +0 -0
  232. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_backtesting_broker_await_close.py +0 -0
  233. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_backtesting_broker_time_advance.py +0 -0
  234. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_backtesting_crypto_cash_unit.py +0 -0
  235. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_backtesting_flow_control.py +0 -0
  236. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_backtesting_multileg_unit.py +0 -0
  237. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_bars_aggregate_frequency_normalization.py +0 -0
  238. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_bars_aggregation_timeunits.py +0 -0
  239. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_bars_frequency_flex.py +0 -0
  240. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_botspot_handler.py +0 -0
  241. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_botspot_logger.py +0 -0
  242. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_broker_bitunix.py +0 -0
  243. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_broker_cleanup.py +0 -0
  244. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_broker_initialization.py +0 -0
  245. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_brokers_handle_crypto.py +0 -0
  246. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_cash.py +0 -0
  247. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_ccxt.py +0 -0
  248. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_ccxt_store.py +0 -0
  249. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_configs_helper.py +0 -0
  250. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_continuous_futures.py +0 -0
  251. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_continuous_futures_integration.py +0 -0
  252. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_data_source.py +0 -0
  253. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_databento_auto_expiry_integration.py +0 -0
  254. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_drift_rebalancer.py +0 -0
  255. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_futures_integration.py +0 -0
  256. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_helpers.py +0 -0
  257. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_integration_tests.py +0 -0
  258. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_interactive_brokers.py +0 -0
  259. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_live_trading_resilience.py +0 -0
  260. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_logger_env_vars.py +0 -0
  261. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_logging.py +0 -0
  262. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_lumibot_logger.py +0 -0
  263. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_mes_symbols.py +0 -0
  264. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_momentum.py +0 -0
  265. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_order.py +0 -0
  266. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_order_serialization.py +0 -0
  267. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_pandas_data.py +0 -0
  268. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_position_serialization.py +0 -0
  269. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_projectx.py +0 -0
  270. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_projectx_bracket_helpers.py +0 -0
  271. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_projectx_bracket_lifecycle_unit.py +0 -0
  272. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_projectx_data.py +0 -0
  273. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_projectx_datetime_columns.py +0 -0
  274. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_projectx_datetime_index.py +0 -0
  275. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_projectx_helpers.py +0 -0
  276. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_projectx_lifecycle.py +0 -0
  277. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_projectx_lifecycle_unit.py +0 -0
  278. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_projectx_live_flow.py +0 -0
  279. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_projectx_url_mappings.py +0 -0
  280. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_quiet_logs_buy_and_hold.py +0 -0
  281. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_quiet_logs_comprehensive.py +0 -0
  282. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_quiet_logs_functionality.py +0 -0
  283. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_session_manager.py +0 -0
  284. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_strategy_methods.py +0 -0
  285. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_tradier.py +0 -0
  286. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_tradier_data.py +0 -0
  287. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_tradingfee.py +0 -0
  288. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_tradovate.py +0 -0
  289. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_unified_logger.py +0 -0
  290. {lumibot-4.0.23 → lumibot-4.2.9}/tests/test_vix_helper.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: lumibot
3
- Version: 4.0.23
3
+ Version: 4.2.9
4
4
  Summary: Backtesting and Trading Library, Made by Lumiwealth
5
5
  Home-page: https://github.com/Lumiwealth/lumibot
6
6
  Author: Robert Grzesik
@@ -43,7 +43,6 @@ Requires-Dist: psycopg2-binary
43
43
  Requires-Dist: exchange_calendars>=4.6.0
44
44
  Requires-Dist: duckdb
45
45
  Requires-Dist: tabulate
46
- Requires-Dist: thetadata==0.9.11
47
46
  Requires-Dist: databento>=0.42.0
48
47
  Requires-Dist: holidays
49
48
  Requires-Dist: psutil
@@ -52,6 +51,18 @@ Requires-Dist: schwab-py>=1.5.0
52
51
  Requires-Dist: Flask>=2.3
53
52
  Requires-Dist: free-proxy
54
53
  Requires-Dist: requests-oauthlib
54
+ Requires-Dist: boto3>=1.40.64
55
+ Dynamic: author
56
+ Dynamic: author-email
57
+ Dynamic: classifier
58
+ Dynamic: description
59
+ Dynamic: description-content-type
60
+ Dynamic: home-page
61
+ Dynamic: license
62
+ Dynamic: license-file
63
+ Dynamic: requires-dist
64
+ Dynamic: requires-python
65
+ Dynamic: summary
55
66
 
56
67
  [![CI Status](https://github.com/Lumiwealth/lumibot/actions/workflows/cicd.yaml/badge.svg?branch=dev)](https://github.com/Lumiwealth/lumibot/actions/workflows/cicd.yaml)
57
68
  [![Coverage](https://raw.githubusercontent.com/Lumiwealth/lumibot/badge/coverage.svg)](https://github.com/Lumiwealth/lumibot/actions/workflows/cicd.yaml)
@@ -136,6 +147,13 @@ To run an individual test file, you can run the following command:
136
147
  pytest tests/test_asset.py
137
148
  ```
138
149
 
150
+ ## Remote Cache Configuration
151
+
152
+ Lumibot can mirror its local parquet caches to AWS S3 when you enable the new
153
+ backtest cache manager. The feature is optional and defaults to local storage.
154
+ To configure the environment variables, understand the key naming convention,
155
+ and follow the manual validation checklist, review `docs/remote_cache.md`.
156
+
139
157
  ### Showing Code Coverage
140
158
 
141
159
  To show code coverage, you can run the following command:
@@ -81,6 +81,13 @@ To run an individual test file, you can run the following command:
81
81
  pytest tests/test_asset.py
82
82
  ```
83
83
 
84
+ ## Remote Cache Configuration
85
+
86
+ Lumibot can mirror its local parquet caches to AWS S3 when you enable the new
87
+ backtest cache manager. The feature is optional and defaults to local storage.
88
+ To configure the environment variables, understand the key naming convention,
89
+ and follow the manual validation checklist, review `docs/remote_cache.md`.
90
+
84
91
  ### Showing Code Coverage
85
92
 
86
93
  To show code coverage, you can run the following command:
@@ -0,0 +1,30 @@
1
+ from .alpaca_backtesting import AlpacaBacktesting
2
+ from .alpha_vantage_backtesting import AlphaVantageBacktesting
3
+ from .backtesting_broker import BacktestingBroker
4
+ from .ccxt_backtesting import CcxtBacktesting
5
+ from .interactive_brokers_rest_backtesting import InteractiveBrokersRESTBacktesting
6
+ from .pandas_backtesting import PandasDataBacktesting
7
+ from .polygon_backtesting import PolygonDataBacktesting
8
+ from .thetadata_backtesting import ThetaDataBacktesting
9
+ from .thetadata_backtesting_pandas import ThetaDataBacktestingPandas
10
+ from .yahoo_backtesting import YahooDataBacktesting
11
+
12
+ from .databento_backtesting import DataBentoDataBacktesting
13
+ from .databento_backtesting_pandas import DataBentoDataBacktestingPandas
14
+ from .databento_backtesting_polars import DataBentoDataBacktestingPolars
15
+
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
@@ -17,6 +18,91 @@ from lumibot.trading_builtins import CustomStream
17
18
  logger = get_logger(__name__)
18
19
 
19
20
 
21
+ # Typical initial margin requirements for common futures contracts
22
+ # Used for backtesting to simulate margin deduction/release
23
+ TYPICAL_FUTURES_MARGINS = {
24
+ # CME Micro E-mini Futures
25
+ "MES": 1300, # Micro E-mini S&P 500 (~$1,300)
26
+ "MNQ": 1700, # Micro E-mini Nasdaq-100 (~$1,700)
27
+ "MYM": 1100, # Micro E-mini Dow (~$1,100)
28
+ "M2K": 800, # Micro E-mini Russell 2000 (~$800)
29
+ "MCL": 1500, # Micro Crude Oil (~$1,500)
30
+ "MGC": 1200, # Micro Gold (~$1,200)
31
+
32
+ # CME Standard E-mini Futures
33
+ "ES": 13000, # E-mini S&P 500 (~$13,000)
34
+ "NQ": 17000, # E-mini Nasdaq-100 (~$17,000)
35
+ "YM": 11000, # E-mini Dow (~$11,000)
36
+ "RTY": 8000, # E-mini Russell 2000 (~$8,000)
37
+
38
+ # CME Full-Size Futures
39
+ "CL": 8000, # Crude Oil (~$8,000)
40
+ "GC": 10000, # Gold (~$10,000)
41
+ "SI": 14000, # Silver (~$14,000)
42
+ "NG": 3000, # Natural Gas (~$3,000)
43
+ "HG": 4000, # Copper (~$4,000)
44
+
45
+ # CME Currency Futures
46
+ "6E": 2500, # Euro FX (~$2,500)
47
+ "6J": 3000, # Japanese Yen (~$3,000)
48
+ "6B": 2800, # British Pound (~$2,800)
49
+ "6C": 2000, # Canadian Dollar (~$2,000)
50
+
51
+ # CME Interest Rate Futures
52
+ "ZB": 4000, # 30-Year T-Bond (~$4,000)
53
+ "ZN": 2000, # 10-Year T-Note (~$2,000)
54
+ "ZF": 1500, # 5-Year T-Note (~$1,500)
55
+ "ZT": 800, # 2-Year T-Note (~$800)
56
+
57
+ # CME Agricultural Futures
58
+ "ZC": 2000, # Corn (~$2,000)
59
+ "ZS": 3000, # Soybeans (~$3,000)
60
+ "ZW": 2500, # Wheat (~$2,500)
61
+ "ZL": 1500, # Soybean Oil (~$1,500)
62
+
63
+ # Default for unknown futures
64
+ "DEFAULT": 5000, # Conservative default
65
+ }
66
+
67
+
68
+ def get_futures_margin_requirement(asset: Asset) -> float:
69
+ """
70
+ Get the initial margin requirement for a futures contract.
71
+
72
+ This is used in backtesting to simulate the margin deduction when opening
73
+ a futures position and margin release when closing.
74
+
75
+ Args:
76
+ asset: The futures Asset object
77
+
78
+ Returns:
79
+ float: Initial margin requirement in dollars
80
+
81
+ Note:
82
+ These are TYPICAL values and may not match current broker requirements.
83
+ For live trading, brokers handle margin internally.
84
+ """
85
+ symbol = asset.symbol.upper()
86
+
87
+ # Try exact match first
88
+ if symbol in TYPICAL_FUTURES_MARGINS:
89
+ return TYPICAL_FUTURES_MARGINS[symbol]
90
+
91
+ # Try base symbol (remove month/year codes like "ESH4" -> "ES")
92
+ # Most futures symbols are 2-3 characters followed by month/year
93
+ base_symbol = ''.join(c for c in symbol if c.isalpha())
94
+ if base_symbol in TYPICAL_FUTURES_MARGINS:
95
+ return TYPICAL_FUTURES_MARGINS[base_symbol]
96
+
97
+ # Unknown contract - use conservative default
98
+ logger.warning(
99
+ f"Unknown futures contract '{symbol}'. Using default margin of "
100
+ f"${TYPICAL_FUTURES_MARGINS['DEFAULT']:.2f}. "
101
+ f"Consider adding this contract to TYPICAL_FUTURES_MARGINS."
102
+ )
103
+ return TYPICAL_FUTURES_MARGINS["DEFAULT"]
104
+
105
+
20
106
  class BacktestingBroker(Broker):
21
107
  # Metainfo
22
108
  IS_BACKTESTING_BROKER = True
@@ -215,17 +301,30 @@ class BacktestingBroker(Broker):
215
301
  trading_day = search.iloc[0]
216
302
  open_time = trading_day.market_open
217
303
 
304
+ # DEBUG: Log what's happening
305
+ print(f"[BROKER DEBUG] get_time_to_open: now={now}, next_trading_day={trading_day.name}, open_time={open_time}")
306
+
218
307
  # For Backtesting, sometimes the user can just pass in dates (i.e. 2023-08-01) and not datetimes
219
308
  # In this case the "now" variable is starting at midnight, so we need to adjust the open_time to be actual
220
- # market open time. In the case where the user passes in a time inside a valid trading day, use that time
309
+ # market open time. In the case where the user passes in a valid trading day, use that time
221
310
  # as the start of trading instead of market open.
311
+ # BUT: Only do this if the current day (now.date()) is actually a trading day
222
312
  if self.IS_BACKTESTING_BROKER and now > open_time:
223
- open_time = self.data_source.datetime_start
313
+ # Check if now.date() is in trading days before overriding
314
+ now_date = now.date() if hasattr(now, 'date') else now
315
+ trading_day_dates = self._trading_days.index.date
316
+ if now_date in trading_day_dates:
317
+ print(f"[BROKER DEBUG] Overriding open_time to datetime_start because now ({now}) is on a trading day but after market open")
318
+ open_time = self.data_source.datetime_start
319
+ else:
320
+ print(f"[BROKER DEBUG] NOT overriding open_time because now ({now}) is NOT a trading day")
224
321
 
225
322
  if now >= open_time:
323
+ print(f"[BROKER DEBUG] Market already open: now={now} >= open_time={open_time}, returning 0")
226
324
  return 0
227
325
 
228
326
  delta = open_time - now
327
+ print(f"[BROKER DEBUG] Market opens in {delta.total_seconds()} seconds")
229
328
  return delta.total_seconds()
230
329
 
231
330
  def get_time_to_close(self):
@@ -262,24 +361,30 @@ class BacktestingBroker(Broker):
262
361
  def _await_market_to_open(self, timedelta=None, strategy=None):
263
362
  # Process outstanding orders first before waiting for market to open
264
363
  # or else they don't get processed until the next day
364
+ print(f"[BROKER DEBUG] _await_market_to_open called, current datetime={self.datetime}, timedelta={timedelta}")
265
365
  self.process_pending_orders(strategy=strategy)
266
366
 
267
367
  time_to_open = self.get_time_to_open()
368
+ print(f"[BROKER DEBUG] get_time_to_open returned: {time_to_open}")
268
369
 
269
370
  # If None is returned, it means we've reached the end of available trading days
270
371
  if time_to_open is None:
271
372
  logger.info("Backtesting reached end of available trading days data")
373
+ print(f"[BROKER DEBUG] time_to_open is None, returning early")
272
374
  return
273
375
 
274
376
  # Allow the caller to specify a buffer (in minutes) before the actual open
275
377
  if timedelta:
276
378
  time_to_open -= 60 * timedelta
379
+ print(f"[BROKER DEBUG] Adjusted time_to_open for timedelta buffer: {time_to_open}")
277
380
 
278
381
  # Only advance time if there is something positive to advance;
279
382
  # prevents zero or negative time updates.
280
383
  if time_to_open <= 0:
384
+ print(f"[BROKER DEBUG] time_to_open <= 0 ({time_to_open}), returning without advancing time")
281
385
  return
282
386
 
387
+ print(f"[BROKER DEBUG] Advancing time by {time_to_open} seconds")
283
388
  self._update_datetime(time_to_open)
284
389
 
285
390
  def _await_market_to_close(self, timedelta=None, strategy=None):
@@ -499,8 +604,16 @@ class BacktestingBroker(Broker):
499
604
  def _cancel_inline(order: Order):
500
605
  if order.identifier in canceled_identifiers:
501
606
  return
502
- canceled_identifiers.add(order.identifier)
503
- self._process_trade_event(order, self.CANCELED_ORDER)
607
+
608
+ # BUGFIX: Only process CANCELED event if the order is actually active
609
+ # Don't try to cancel orders that are already filled or canceled
610
+ if order.is_active():
611
+ canceled_identifiers.add(order.identifier)
612
+ self._process_trade_event(order, self.CANCELED_ORDER)
613
+ else:
614
+ logger.debug(f"Order {order.identifier} not active (status={order.status}), skipping cancel event")
615
+ canceled_identifiers.add(order.identifier)
616
+
504
617
  for child in order.child_orders:
505
618
  _cancel_inline(child)
506
619
 
@@ -920,9 +1033,92 @@ class BacktestingBroker(Broker):
920
1033
  asset_type = getattr(order.asset, "asset_type", None)
921
1034
  quote_asset_type = getattr(order.quote, "asset_type", None) if hasattr(order, "quote") and order.quote else None
922
1035
 
1036
+ # For futures, use margin-based cash management (not full notional value)
1037
+ # Futures don't tie up full contract value - only margin requirement
1038
+ if (
1039
+ not is_multileg_parent
1040
+ and asset_type in (Asset.AssetType.FUTURE, Asset.AssetType.CONT_FUTURE)
1041
+ ):
1042
+ # Reconstruct position state BEFORE this order to determine if opening/closing
1043
+ futures_qty_before = 0
1044
+ futures_entry_price = None
1045
+
1046
+ # Look through filled_orders to find position before this order
1047
+ for filled_order in self._filled_orders.get_list():
1048
+ if (filled_order.asset == order.asset
1049
+ and filled_order.strategy == order.strategy
1050
+ and filled_order != order): # Don't count the current order
1051
+
1052
+ if filled_order.side in (Order.OrderSide.BUY, "buy", "buy_to_open"):
1053
+ futures_qty_before += filled_order.quantity
1054
+ # Track most recent BUY entry price (for long positions)
1055
+ if filled_order.avg_fill_price:
1056
+ futures_entry_price = float(filled_order.avg_fill_price)
1057
+ elif filled_order.side in (Order.OrderSide.SELL, Order.OrderSide.SELL_TO_CLOSE, "sell", "sell_to_close"):
1058
+ futures_qty_before -= filled_order.quantity
1059
+ # Track most recent SELL entry price (for short positions)
1060
+ # Note: This gets overwritten by SELL_TO_CLOSE, which is correct
1061
+ # We want the opening SELL price, not closing prices
1062
+ if (filled_order.side in (Order.OrderSide.SELL, "sell") # Opening short
1063
+ and filled_order.avg_fill_price):
1064
+ futures_entry_price = float(filled_order.avg_fill_price)
1065
+
1066
+ # Determine if this order is opening or closing a position
1067
+ is_opening = (futures_qty_before == 0)
1068
+ is_closing_long = (
1069
+ futures_qty_before > 0
1070
+ and order.side in (Order.OrderSide.SELL, Order.OrderSide.SELL_TO_CLOSE, "sell", "sell_to_close")
1071
+ )
1072
+ is_closing_short = (
1073
+ futures_qty_before < 0
1074
+ and order.side in (Order.OrderSide.BUY, Order.OrderSide.BUY_TO_OPEN, "buy", "buy_to_open")
1075
+ )
1076
+ is_closing = is_closing_long or is_closing_short
1077
+
1078
+ # Get margin requirement and multiplier
1079
+ margin_per_contract = get_futures_margin_requirement(order.asset)
1080
+ multiplier = getattr(order.asset, "multiplier", 1)
1081
+ total_margin = margin_per_contract * float(filled_quantity)
1082
+
1083
+ current_cash = strategy.cash
1084
+
1085
+ if is_opening:
1086
+ # ENTRY (long or short): Deduct initial margin from cash
1087
+ new_cash = current_cash - total_margin
1088
+ strategy._set_cash_position(new_cash)
1089
+
1090
+ elif is_closing:
1091
+ # EXIT (close long or cover short): Release margin and apply realized P&L
1092
+ if futures_entry_price:
1093
+ exit_price = float(price)
1094
+
1095
+ # For shorts, P&L is inverted: profit when price goes down
1096
+ if futures_qty_before < 0:
1097
+ # Closing short: P&L = (entry - exit) × qty × multiplier
1098
+ realized_pnl = (futures_entry_price - exit_price) * float(filled_quantity) * float(multiplier)
1099
+ else:
1100
+ # Closing long: P&L = (exit - entry) × qty × multiplier
1101
+ realized_pnl = (exit_price - futures_entry_price) * float(filled_quantity) * float(multiplier)
1102
+
1103
+ # Update cash: release margin + add realized P&L
1104
+ new_cash = current_cash + total_margin + realized_pnl
1105
+ strategy._set_cash_position(new_cash)
1106
+ else:
1107
+ # No entry price found - just release margin (shouldn't happen normally)
1108
+ logger.warning(
1109
+ f"No entry price found for futures exit: {order.asset.symbol}. "
1110
+ f"Only releasing margin, no P&L applied."
1111
+ )
1112
+ new_cash = current_cash + total_margin
1113
+ strategy._set_cash_position(new_cash)
1114
+ else:
1115
+ # Adding to existing position: deduct margin for additional contracts
1116
+ new_cash = current_cash - total_margin
1117
+ strategy._set_cash_position(new_cash)
1118
+
923
1119
  # For crypto base with forex quote (like BTC/USD where USD is forex), use cash
924
1120
  # For crypto base with crypto quote (like BTC/USDT where both are crypto), use positions
925
- if (
1121
+ elif (
926
1122
  not is_multileg_parent
927
1123
  and asset_type == Asset.AssetType.CRYPTO
928
1124
  and quote_asset_type == Asset.AssetType.FOREX
@@ -964,16 +1160,21 @@ class BacktestingBroker(Broker):
964
1160
  self._apply_trade_cost(strategy, trade_cost)
965
1161
 
966
1162
  def _process_crypto_quote(self, order, quantity, price):
967
- """Override to skip crypto quote processing for crypto+forex trades that are handled with direct cash updates."""
968
- # Check if this is a crypto+forex trade
1163
+ """Override to skip quote processing for assets that use direct cash updates or margin-based trading."""
1164
+ # Check asset types
969
1165
  asset_type = getattr(order.asset, "asset_type", None)
970
1166
  quote_asset_type = getattr(order.quote, "asset_type", None) if hasattr(order, "quote") and order.quote else None
971
1167
 
972
- # For crypto+forex trades, skip position-based quote processing since we handle cash directly
1168
+ # Skip position-based quote processing for:
1169
+ # 1. Crypto+forex trades (handled with direct cash updates)
1170
+ # 2. Futures contracts (use margin, only realize P&L on close, not full notional)
973
1171
  if asset_type == Asset.AssetType.CRYPTO and quote_asset_type == Asset.AssetType.FOREX:
974
1172
  return
975
1173
 
976
- # For crypto+crypto trades, use the original position-based processing
1174
+ if asset_type in (Asset.AssetType.FUTURE, Asset.AssetType.CONT_FUTURE):
1175
+ return
1176
+
1177
+ # For other asset types (crypto+crypto, stocks, etc.), use the original position-based processing
977
1178
  super()._process_crypto_quote(order, quantity, price)
978
1179
 
979
1180
  def calculate_trade_cost(self, order: Order, strategy, price: float):
@@ -1153,17 +1354,21 @@ class BacktestingBroker(Broker):
1153
1354
 
1154
1355
  # Get the OHLCV data for the asset if we're using the YAHOO, CCXT data source
1155
1356
  data_source_name = self.data_source.SOURCE.upper()
1156
- if data_source_name in ["CCXT", "YAHOO", "ALPACA", "DATABENTO"]:
1157
- # 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.
1158
1361
  timeshift = timedelta(minutes=-1)
1159
- if data_source_name == "DATABENTO":
1160
- # 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.
1161
1365
  timeshift = timedelta(minutes=-2)
1162
1366
  elif data_source_name == "YAHOO":
1163
- # 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.
1164
1369
  timeshift = timedelta(days=-1)
1165
1370
  elif data_source_name == "ALPACA":
1166
- # Alpaca minute bars are aligned to the current iteration already.
1371
+ # Alpaca minute bars line up with our clock already; no offset needed.
1167
1372
  timeshift = None
1168
1373
 
1169
1374
  ohlc = self.data_source.get_historical_prices(
@@ -1173,6 +1378,23 @@ class BacktestingBroker(Broker):
1173
1378
  timeshift=timeshift,
1174
1379
  )
1175
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
+
1176
1398
  # Handle both pandas and polars DataFrames
1177
1399
  if hasattr(ohlc.df, 'index'): # pandas
1178
1400
  dt = ohlc.df.index[-1]
@@ -1206,6 +1428,16 @@ class BacktestingBroker(Broker):
1206
1428
  )
1207
1429
  # Check if we got any ohlc data
1208
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
+ )
1209
1441
  self.cancel_order(order)
1210
1442
  continue
1211
1443
 
@@ -1302,41 +1534,89 @@ class BacktestingBroker(Broker):
1302
1534
  strategy=strategy,
1303
1535
  )
1304
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
+ )
1305
1552
  continue
1306
1553
 
1307
1554
  # After handling all pending orders, cash settle any residual expired contracts.
1308
1555
  self.process_expired_option_contracts(strategy)
1309
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
+
1310
1574
  def limit_order(self, limit_price, side, open_, high, low):
1311
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
+
1312
1584
  # Gap Up case: Limit wasn't triggered by previous candle but current candle opens higher, fill it now
1313
- if side == "sell" and limit_price <= open_:
1314
- return open_
1585
+ if side == "sell" and limit_val <= open_val:
1586
+ return open_val
1315
1587
 
1316
1588
  # Gap Down case: Limit wasn't triggered by previous candle but current candle opens lower, fill it now
1317
- if side == "buy" and limit_price >= open_:
1318
- return open_
1589
+ if side == "buy" and limit_val >= open_val:
1590
+ return open_val
1319
1591
 
1320
1592
  # Current candle triggered limit normally
1321
- if low <= limit_price <= high:
1322
- return limit_price
1593
+ if low_val <= limit_val <= high_val:
1594
+ return limit_val
1323
1595
 
1324
1596
  # Limit has not been met
1325
1597
  return None
1326
1598
 
1327
1599
  def stop_order(self, stop_price, side, open_, high, low):
1328
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
+
1329
1609
  # Gap Down case: Stop wasn't triggered by previous candle but current candle opens lower, fill it now
1330
- if side == "sell" and stop_price >= open_:
1331
- return open_
1610
+ if side == "sell" and stop_val >= open_val:
1611
+ return open_val
1332
1612
 
1333
1613
  # Gap Up case: Stop wasn't triggered by previous candle but current candle opens higher, fill it now
1334
- if side == "buy" and stop_price <= open_:
1335
- return open_
1614
+ if side == "buy" and stop_val <= open_val:
1615
+ return open_val
1336
1616
 
1337
1617
  # Current candle triggered stop normally
1338
- if low <= stop_price <= high:
1339
- return stop_price
1618
+ if low_val <= stop_val <= high_val:
1619
+ return stop_val
1340
1620
 
1341
1621
  # Stop has not been met
1342
1622
  return None
@@ -0,0 +1,7 @@
1
+ """Canonical DataBento backtesting aliasing the Polars implementation."""
2
+
3
+ from .databento_backtesting_polars import DataBentoDataBacktestingPolars as DataBentoDataBacktesting
4
+ from .databento_backtesting_pandas import DataBentoDataBacktestingPandas
5
+ from .databento_backtesting_polars import DataBentoDataBacktestingPolars
6
+
7
+ __all__ = ["DataBentoDataBacktesting", "DataBentoDataBacktestingPandas", "DataBentoDataBacktestingPolars"]