lumibot 4.1.2__tar.gz → 4.1.3__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 (269) hide show
  1. {lumibot-4.1.2/lumibot.egg-info → lumibot-4.1.3}/PKG-INFO +1 -1
  2. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/strategies/_strategy.py +29 -8
  3. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/databento_helper.py +32 -9
  4. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/databento_helper_polars.py +12 -5
  5. {lumibot-4.1.2 → lumibot-4.1.3/lumibot.egg-info}/PKG-INFO +1 -1
  6. {lumibot-4.1.2 → lumibot-4.1.3}/setup.py +1 -1
  7. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_databento_comprehensive_trading.py +62 -83
  8. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_databento_parity.py +27 -5
  9. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_futures_edge_cases.py +93 -60
  10. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_backtesting_data_source_env.py +44 -10
  11. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_databento_helper.py +6 -1
  12. {lumibot-4.1.2 → lumibot-4.1.3}/LICENSE +0 -0
  13. {lumibot-4.1.2 → lumibot-4.1.3}/MANIFEST.in +0 -0
  14. {lumibot-4.1.2 → lumibot-4.1.3}/README.md +0 -0
  15. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/__init__.py +0 -0
  16. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/backtesting/__init__.py +0 -0
  17. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/backtesting/alpaca_backtesting.py +0 -0
  18. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/backtesting/alpha_vantage_backtesting.py +0 -0
  19. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/backtesting/backtesting_broker.py +0 -0
  20. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/backtesting/ccxt_backtesting.py +0 -0
  21. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/backtesting/databento_backtesting.py +0 -0
  22. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/backtesting/interactive_brokers_rest_backtesting.py +0 -0
  23. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/backtesting/pandas_backtesting.py +0 -0
  24. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/backtesting/polygon_backtesting.py +0 -0
  25. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/backtesting/thetadata_backtesting.py +0 -0
  26. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/backtesting/yahoo_backtesting.py +0 -0
  27. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/brokers/__init__.py +0 -0
  28. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/brokers/alpaca.py +0 -0
  29. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/brokers/bitunix.py +0 -0
  30. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/brokers/broker.py +0 -0
  31. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/brokers/ccxt.py +0 -0
  32. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/brokers/example_broker.py +0 -0
  33. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/brokers/interactive_brokers.py +0 -0
  34. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/brokers/interactive_brokers_rest.py +0 -0
  35. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/brokers/projectx.py +0 -0
  36. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/brokers/schwab.py +0 -0
  37. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/brokers/tradier.py +0 -0
  38. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/brokers/tradovate.py +0 -0
  39. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/components/__init__.py +0 -0
  40. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/components/configs_helper.py +0 -0
  41. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/components/drift_rebalancer_logic.py +0 -0
  42. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/components/grok_helper.py +0 -0
  43. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/components/options_helper.py +0 -0
  44. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/components/perplexity_helper.py +0 -0
  45. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/components/quiver_helper.py +0 -0
  46. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/components/vix_helper.py +0 -0
  47. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/constants.py +0 -0
  48. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/credentials.py +0 -0
  49. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/__init__.py +0 -0
  50. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/alpaca_data.py +0 -0
  51. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/alpha_vantage_data.py +0 -0
  52. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/bitunix_data.py +0 -0
  53. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/ccxt_backtesting_data.py +0 -0
  54. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/ccxt_data.py +0 -0
  55. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/data_source.py +0 -0
  56. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/data_source_backtesting.py +0 -0
  57. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/databento_data.py +0 -0
  58. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/databento_data_polars_backtesting.py +0 -0
  59. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/databento_data_polars_live.py +0 -0
  60. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/example_broker_data.py +0 -0
  61. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/exceptions.py +0 -0
  62. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/interactive_brokers_data.py +0 -0
  63. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/interactive_brokers_rest_data.py +0 -0
  64. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/pandas_data.py +0 -0
  65. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/polars_mixin.py +0 -0
  66. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/polygon_data_polars.py +0 -0
  67. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/projectx_data.py +0 -0
  68. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/schwab_data.py +0 -0
  69. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/tradier_data.py +0 -0
  70. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/tradovate_data.py +0 -0
  71. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/yahoo_data.py +0 -0
  72. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/data_sources/yahoo_data_polars.py +0 -0
  73. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/entities/__init__.py +0 -0
  74. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/entities/asset.py +0 -0
  75. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/entities/bar.py +0 -0
  76. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/entities/bars.py +0 -0
  77. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/entities/chains.py +0 -0
  78. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/entities/data.py +0 -0
  79. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/entities/dataline.py +0 -0
  80. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/entities/order.py +0 -0
  81. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/entities/position.py +0 -0
  82. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/entities/quote.py +0 -0
  83. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/entities/trading_fee.py +0 -0
  84. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/__init__.py +0 -0
  85. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/bitunix_futures_example.py +0 -0
  86. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/ccxt_backtesting_example.py +0 -0
  87. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/classic_60_40.py +0 -0
  88. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/classic_60_40_config.py +0 -0
  89. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/crypto_50_50.py +0 -0
  90. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/crypto_50_50_config.py +0 -0
  91. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/crypto_important_functions.py +0 -0
  92. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/drift_rebalancer.py +0 -0
  93. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/forex_hold_to_expiry.py +0 -0
  94. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/futures_hold_to_expiry.py +0 -0
  95. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/lifecycle_logger.py +0 -0
  96. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/options_hold_to_expiry.py +0 -0
  97. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/schedule_function.py +0 -0
  98. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/simple_start_single_file.py +0 -0
  99. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/stock_bracket.py +0 -0
  100. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/stock_buy_and_hold.py +0 -0
  101. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/stock_diversified_leverage.py +0 -0
  102. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/stock_limit_and_trailing_stops.py +0 -0
  103. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/stock_momentum.py +0 -0
  104. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/stock_oco.py +0 -0
  105. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/strangle.py +0 -0
  106. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/example_strategies/test_broker_functions.py +0 -0
  107. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/resources/ThetaTerminal.jar +0 -0
  108. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/resources/conf.yaml +0 -0
  109. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/strategies/__init__.py +0 -0
  110. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/strategies/session_manager.py +0 -0
  111. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/strategies/strategy.py +0 -0
  112. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/strategies/strategy_executor.py +0 -0
  113. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/__init__.py +0 -0
  114. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/alpaca_helpers.py +0 -0
  115. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/bitunix_helpers.py +0 -0
  116. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/black_scholes.py +0 -0
  117. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/ccxt_data_store.py +0 -0
  118. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/databento_roll.py +0 -0
  119. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/debugers.py +0 -0
  120. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/decorators.py +0 -0
  121. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/futures_symbols.py +0 -0
  122. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/helpers.py +0 -0
  123. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/indicators.py +0 -0
  124. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/lumibot_logger.py +0 -0
  125. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/lumibot_time.py +0 -0
  126. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/pandas.py +0 -0
  127. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/polygon_helper.py +0 -0
  128. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/polygon_helper_async.py +0 -0
  129. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/polygon_helper_polars_optimized.py +0 -0
  130. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/projectx_helpers.py +0 -0
  131. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/schwab_helper.py +0 -0
  132. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/thetadata_helper.py +0 -0
  133. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/types.py +0 -0
  134. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/yahoo_helper.py +0 -0
  135. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/tools/yahoo_helper_polars_optimized.py +0 -0
  136. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/traders/__init__.py +0 -0
  137. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/traders/debug_log_trader.py +0 -0
  138. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/traders/trader.py +0 -0
  139. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/trading_builtins/__init__.py +0 -0
  140. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/trading_builtins/custom_stream.py +0 -0
  141. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot/trading_builtins/safe_list.py +0 -0
  142. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot.egg-info/SOURCES.txt +0 -0
  143. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot.egg-info/dependency_links.txt +0 -0
  144. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot.egg-info/requires.txt +0 -0
  145. {lumibot-4.1.2 → lumibot-4.1.3}/lumibot.egg-info/top_level.txt +0 -0
  146. {lumibot-4.1.2 → lumibot-4.1.3}/pyproject.toml +0 -0
  147. {lumibot-4.1.2 → lumibot-4.1.3}/setup.cfg +0 -0
  148. {lumibot-4.1.2 → lumibot-4.1.3}/tests/__init__.py +0 -0
  149. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/__init__.py +0 -0
  150. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/check_timing_offset.py +0 -0
  151. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/check_volume_spike.py +0 -0
  152. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/comprehensive_comparison.py +0 -0
  153. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/conftest.py +0 -0
  154. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/debug_comparison.py +0 -0
  155. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/diagnose_price_difference.py +0 -0
  156. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/direct_api_comparison.py +0 -0
  157. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/performance_tracker.py +0 -0
  158. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/profile_thetadata_vs_polygon.py +0 -0
  159. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/root_cause_analysis.py +0 -0
  160. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_accuracy_verification.py +0 -0
  161. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_backtesting_broker_processing.py +0 -0
  162. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_buy_hold_quiet_logs_full_run.py +0 -0
  163. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_crypto_cash_regressions.py +0 -0
  164. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_daily_data_timestamp_comparison.py +0 -0
  165. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_databento.py +0 -0
  166. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_debug_avg_fill_price.py +0 -0
  167. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_dividends.py +0 -0
  168. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_example_strategies.py +0 -0
  169. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_failing_backtest.py +0 -0
  170. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_futures_single_trade.py +0 -0
  171. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_futures_ultra_simple.py +0 -0
  172. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_index_data_verification.py +0 -0
  173. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_multileg_backtest.py +0 -0
  174. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_pandas_backtest.py +0 -0
  175. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_passing_trader_into_backtest.py +0 -0
  176. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_polygon.py +0 -0
  177. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_strategy_executor.py +0 -0
  178. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_thetadata.py +0 -0
  179. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_thetadata_comprehensive.py +0 -0
  180. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_thetadata_vs_polygon.py +0 -0
  181. {lumibot-4.1.2 → lumibot-4.1.3}/tests/backtest/test_yahoo.py +0 -0
  182. {lumibot-4.1.2 → lumibot-4.1.3}/tests/conftest.py +0 -0
  183. {lumibot-4.1.2 → lumibot-4.1.3}/tests/fixtures.py +0 -0
  184. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_alpaca.py +0 -0
  185. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_alpaca_auth_fix.py +0 -0
  186. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_alpaca_backtesting.py +0 -0
  187. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_alpaca_data.py +0 -0
  188. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_alpaca_helpers.py +0 -0
  189. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_alpaca_multileg_fix.py +0 -0
  190. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_alpaca_oauth.py +0 -0
  191. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_apscheduler_warnings.py +0 -0
  192. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_asset.py +0 -0
  193. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_asset_auto_expiry.py +0 -0
  194. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_auto_market_inference.py +0 -0
  195. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_backtesting_broker.py +0 -0
  196. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_backtesting_broker_await_close.py +0 -0
  197. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_backtesting_broker_time_advance.py +0 -0
  198. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_backtesting_crypto_cash_unit.py +0 -0
  199. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_backtesting_flow_control.py +0 -0
  200. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_backtesting_multileg_unit.py +0 -0
  201. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_backtesting_quiet_logs_complete.py +0 -0
  202. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_bars_aggregate_frequency_normalization.py +0 -0
  203. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_bars_aggregation_timeunits.py +0 -0
  204. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_bars_frequency_flex.py +0 -0
  205. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_botspot_handler.py +0 -0
  206. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_botspot_logger.py +0 -0
  207. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_broker_bitunix.py +0 -0
  208. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_broker_cleanup.py +0 -0
  209. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_broker_initialization.py +0 -0
  210. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_brokers_handle_crypto.py +0 -0
  211. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_cash.py +0 -0
  212. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_ccxt.py +0 -0
  213. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_ccxt_store.py +0 -0
  214. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_configs_helper.py +0 -0
  215. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_continuous_futures.py +0 -0
  216. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_continuous_futures_integration.py +0 -0
  217. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_continuous_futures_resolution.py +0 -0
  218. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_data_source.py +0 -0
  219. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_databento_asset_validation.py +0 -0
  220. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_databento_auto_expiry_integration.py +0 -0
  221. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_databento_backtesting.py +0 -0
  222. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_databento_data.py +0 -0
  223. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_databento_live.py +0 -0
  224. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_databento_timezone_fixes.py +0 -0
  225. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_drift_rebalancer.py +0 -0
  226. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_futures_integration.py +0 -0
  227. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_get_historical_prices.py +0 -0
  228. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_helpers.py +0 -0
  229. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_indicator_subplots.py +0 -0
  230. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_integration_tests.py +0 -0
  231. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_interactive_brokers.py +0 -0
  232. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_live_trading_resilience.py +0 -0
  233. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_logger_env_vars.py +0 -0
  234. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_logging.py +0 -0
  235. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_lumibot_logger.py +0 -0
  236. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_market_infinite_loop_bug.py +0 -0
  237. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_mes_symbols.py +0 -0
  238. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_momentum.py +0 -0
  239. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_options_helper.py +0 -0
  240. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_order.py +0 -0
  241. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_order_serialization.py +0 -0
  242. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_pandas_data.py +0 -0
  243. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_polygon_helper.py +0 -0
  244. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_position_serialization.py +0 -0
  245. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_projectx.py +0 -0
  246. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_projectx_bracket_helpers.py +0 -0
  247. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_projectx_bracket_lifecycle_unit.py +0 -0
  248. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_projectx_data.py +0 -0
  249. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_projectx_datetime_columns.py +0 -0
  250. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_projectx_datetime_index.py +0 -0
  251. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_projectx_helpers.py +0 -0
  252. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_projectx_lifecycle.py +0 -0
  253. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_projectx_lifecycle_unit.py +0 -0
  254. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_projectx_live_flow.py +0 -0
  255. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_projectx_timestep_alias.py +0 -0
  256. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_projectx_url_mappings.py +0 -0
  257. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_quiet_logs_buy_and_hold.py +0 -0
  258. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_quiet_logs_comprehensive.py +0 -0
  259. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_quiet_logs_functionality.py +0 -0
  260. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_quiet_logs_requirements.py +0 -0
  261. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_session_manager.py +0 -0
  262. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_strategy_methods.py +0 -0
  263. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_thetadata_helper.py +0 -0
  264. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_tradier.py +0 -0
  265. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_tradier_data.py +0 -0
  266. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_tradingfee.py +0 -0
  267. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_tradovate.py +0 -0
  268. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_unified_logger.py +0 -0
  269. {lumibot-4.1.2 → lumibot-4.1.3}/tests/test_vix_helper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumibot
3
- Version: 4.1.2
3
+ Version: 4.1.3
4
4
  Summary: Backtesting and Trading Library, Made by Lumiwealth
5
5
  Home-page: https://github.com/Lumiwealth/lumibot
6
6
  Author: Robert Grzesik
@@ -1264,9 +1264,24 @@ class _Strategy:
1264
1264
  if show_indicators is None:
1265
1265
  show_indicators = SHOW_INDICATORS
1266
1266
 
1267
- # Auto-select datasource from environment variable if None
1268
- if datasource_class is None:
1269
- from lumibot.credentials import BACKTESTING_DATA_SOURCE
1267
+ from lumibot.credentials import BACKTESTING_DATA_SOURCE as _DEFAULT_BACKTESTING_DATA_SOURCE
1268
+
1269
+ # Determine whether an environment override exists. When BACKTESTING_DATA_SOURCE
1270
+ # is set (and not blank/\"none\"), it should take precedence even if a
1271
+ # datasource_class argument was provided.
1272
+ env_override_raw = os.environ.get("BACKTESTING_DATA_SOURCE")
1273
+ env_override_name = None
1274
+
1275
+ if env_override_raw is not None:
1276
+ trimmed = env_override_raw.strip()
1277
+ if trimmed and trimmed.lower() != "none":
1278
+ env_override_name = trimmed.lower()
1279
+ elif datasource_class is None:
1280
+ # No override provided and no class in code – fall back to the default
1281
+ # configured in credentials (ThetaData unless the project overrides it).
1282
+ env_override_name = _DEFAULT_BACKTESTING_DATA_SOURCE.lower()
1283
+
1284
+ if env_override_name is not None:
1270
1285
  from lumibot.backtesting import (
1271
1286
  PolygonDataBacktesting,
1272
1287
  ThetaDataBacktesting,
@@ -1285,18 +1300,24 @@ class _Strategy:
1285
1300
  "databento": DataBentoDataBacktesting,
1286
1301
  }
1287
1302
 
1288
- datasource_name = BACKTESTING_DATA_SOURCE.lower()
1289
- if datasource_name not in datasource_map:
1303
+ if env_override_name not in datasource_map:
1304
+ label = env_override_raw or _DEFAULT_BACKTESTING_DATA_SOURCE
1290
1305
  raise ValueError(
1291
- f"Unknown BACKTESTING_DATA_SOURCE: '{BACKTESTING_DATA_SOURCE}'. "
1306
+ f"Unknown BACKTESTING_DATA_SOURCE: '{label}'. "
1292
1307
  f"Valid options: {list(datasource_map.keys())}"
1293
1308
  )
1294
1309
 
1295
- datasource_class = datasource_map[datasource_name]
1310
+ datasource_class = datasource_map[env_override_name]
1311
+ label = env_override_raw or _DEFAULT_BACKTESTING_DATA_SOURCE
1296
1312
  get_logger(__name__).info(colored(
1297
- f"Auto-selected backtesting data source from BACKTESTING_DATA_SOURCE env var: {BACKTESTING_DATA_SOURCE}",
1313
+ f"Using BACKTESTING_DATA_SOURCE setting for backtest data: {label}",
1298
1314
  "green"
1299
1315
  ))
1316
+ elif datasource_class is None:
1317
+ raise ValueError(
1318
+ "No backtesting data source provided. Set BACKTESTING_DATA_SOURCE in the environment "
1319
+ "or pass datasource_class when calling backtest()."
1320
+ )
1300
1321
 
1301
1322
  # Make sure polygon_api_key is set if using PolygonDataBacktesting
1302
1323
  polygon_api_key = polygon_api_key if polygon_api_key is not None else POLYGON_API_KEY
@@ -593,13 +593,29 @@ def _filter_front_month_rows_pandas(
593
593
  if df.empty or "symbol" not in df.columns or schedule is None:
594
594
  return df
595
595
 
596
+ index_tz = getattr(df.index, "tz", None)
597
+
598
+ def _align(ts: datetime | pd.Timestamp | None) -> pd.Timestamp | None:
599
+ if ts is None:
600
+ return None
601
+ ts_pd = pd.Timestamp(ts)
602
+ if index_tz is None:
603
+ return ts_pd.tz_localize(None) if ts_pd.tz is not None else ts_pd
604
+ if ts_pd.tz is None:
605
+ ts_pd = ts_pd.tz_localize(index_tz)
606
+ else:
607
+ ts_pd = ts_pd.tz_convert(index_tz)
608
+ return ts_pd
609
+
596
610
  mask = pd.Series(False, index=df.index)
597
611
  for symbol, start_dt, end_dt in schedule:
598
612
  cond = df["symbol"] == symbol
599
- if start_dt is not None:
600
- cond &= df.index >= start_dt
601
- if end_dt is not None:
602
- cond &= df.index < end_dt
613
+ start_aligned = _align(start_dt)
614
+ end_aligned = _align(end_dt)
615
+ if start_aligned is not None:
616
+ cond &= df.index >= start_aligned
617
+ if end_aligned is not None:
618
+ cond &= df.index < end_aligned
603
619
  mask |= cond
604
620
 
605
621
  filtered = df.loc[mask]
@@ -783,15 +799,22 @@ def get_price_data_from_databento(
783
799
  start_naive = start.replace(tzinfo=None) if start.tzinfo is not None else start
784
800
  end_naive = end.replace(tzinfo=None) if end.tzinfo is not None else end
785
801
 
786
- if asset.asset_type == Asset.AssetType.CONT_FUTURE:
802
+ roll_asset = asset
803
+ if asset.asset_type == Asset.AssetType.FUTURE and not asset.expiration:
804
+ roll_asset = Asset(asset.symbol, Asset.AssetType.CONT_FUTURE)
805
+
806
+ if roll_asset.asset_type == Asset.AssetType.CONT_FUTURE:
787
807
  schedule_start = start
788
- symbols = databento_roll.resolve_symbols_for_range(asset, schedule_start, end)
789
- front_symbol = databento_roll.resolve_symbol_for_datetime(asset, reference_date or start)
808
+ symbols = databento_roll.resolve_symbols_for_range(roll_asset, schedule_start, end)
809
+ front_symbol = databento_roll.resolve_symbol_for_datetime(roll_asset, reference_date or start)
790
810
  if front_symbol not in symbols:
791
811
  symbols.insert(0, front_symbol)
792
812
  else:
793
813
  schedule_start = start
794
- front_symbol = _format_futures_symbol_for_databento(asset)
814
+ front_symbol = _format_futures_symbol_for_databento(
815
+ asset,
816
+ reference_date=reference_date or start,
817
+ )
795
818
  symbols = [front_symbol]
796
819
 
797
820
  # Ensure multiplier is populated using the first contract.
@@ -904,7 +927,7 @@ def get_price_data_from_databento(
904
927
  return definition
905
928
 
906
929
  schedule = databento_roll.build_roll_schedule(
907
- asset,
930
+ roll_asset,
908
931
  schedule_start,
909
932
  end,
910
933
  definition_provider=get_definition,
@@ -929,10 +929,14 @@ def get_price_data_from_databento_polars(
929
929
  start_naive = start.replace(tzinfo=None) if start.tzinfo is not None else start
930
930
  end_naive = end.replace(tzinfo=None) if end.tzinfo is not None else end
931
931
 
932
- if asset.asset_type == Asset.AssetType.CONT_FUTURE:
932
+ roll_asset = asset
933
+ if asset.asset_type == Asset.AssetType.FUTURE and not asset.expiration:
934
+ roll_asset = Asset(asset.symbol, Asset.AssetType.CONT_FUTURE)
935
+
936
+ if roll_asset.asset_type == Asset.AssetType.CONT_FUTURE:
933
937
  schedule_start = start
934
- symbols_to_fetch = databento_roll.resolve_symbols_for_range(asset, schedule_start, end)
935
- front_symbol = databento_roll.resolve_symbol_for_datetime(asset, reference_date or start)
938
+ symbols_to_fetch = databento_roll.resolve_symbols_for_range(roll_asset, schedule_start, end)
939
+ front_symbol = databento_roll.resolve_symbol_for_datetime(roll_asset, reference_date or start)
936
940
  if front_symbol not in symbols_to_fetch:
937
941
  symbols_to_fetch.insert(0, front_symbol)
938
942
  logger.info(
@@ -941,7 +945,10 @@ def get_price_data_from_databento_polars(
941
945
  )
942
946
  else:
943
947
  schedule_start = start
944
- front_symbol = _format_futures_symbol_for_databento(asset)
948
+ front_symbol = _format_futures_symbol_for_databento(
949
+ asset,
950
+ reference_date=reference_date or start,
951
+ )
945
952
  symbols_to_fetch = [front_symbol]
946
953
 
947
954
  # Fetch and cache futures multiplier from DataBento if needed (after symbol resolution)
@@ -1092,7 +1099,7 @@ def get_price_data_from_databento_polars(
1092
1099
  return definition
1093
1100
 
1094
1101
  schedule = databento_roll.build_roll_schedule(
1095
- asset,
1102
+ roll_asset,
1096
1103
  schedule_start,
1097
1104
  end,
1098
1105
  definition_provider=get_definition,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lumibot
3
- Version: 4.1.2
3
+ Version: 4.1.3
4
4
  Summary: Backtesting and Trading Library, Made by Lumiwealth
5
5
  Home-page: https://github.com/Lumiwealth/lumibot
6
6
  Author: Robert Grzesik
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="lumibot",
8
- version="4.1.2",
8
+ version="4.1.3",
9
9
  author="Robert Grzesik",
10
10
  author_email="rob@lumiwealth.com",
11
11
  description="Backtesting and Trading Library, Made by Lumiwealth",
@@ -215,6 +215,14 @@ class TestDatabentoComprehensiveTrading:
215
215
 
216
216
  print(f"\n Instruments traded: {list(trades_by_instrument.keys())}")
217
217
 
218
+ snapshots_by_symbol = {}
219
+ for snap in strat.snapshots:
220
+ symbol = snap.get("current_asset")
221
+ if symbol:
222
+ snapshots_by_symbol.setdefault(symbol, []).append(snap)
223
+
224
+ fee_amount = float(fee.flat_fee)
225
+
218
226
  # Analyze each instrument's trades
219
227
  for symbol, trades in trades_by_instrument.items():
220
228
  print(f"\n" + "-"*80)
@@ -249,16 +257,45 @@ class TestDatabentoComprehensiveTrading:
249
257
  assert actual_asset.multiplier == expected_multiplier, \
250
258
  f"{symbol} asset.multiplier should be {expected_multiplier}, got {actual_asset.multiplier}"
251
259
 
252
- # For now, just verify cash is reasonable (not testing exact margin since
253
- # we may have P&L from previous trades affecting cash)
254
- print(f"\nCASH STATE:")
255
- print(f" Cash after entry: ${entry['cash_after']:,.2f}")
256
- print(f" (Note: Cash includes P&L from previous trades)")
257
-
258
- # Verify portfolio value is reasonable (shouldn't be massively negative)
259
- portfolio_after = entry['portfolio_after']
260
- assert portfolio_after > 50000, \
261
- f"{symbol} portfolio value seems wrong after entry: ${portfolio_after:,.2f}"
260
+ symbol_snapshots = snapshots_by_symbol.get(symbol, [])
261
+ entry_snapshot = next((s for s in symbol_snapshots if s.get("phase") == "BUY"), None)
262
+ sell_snapshot = next((s for s in symbol_snapshots if s.get("phase") == "SELL"), None)
263
+ hold_snapshots = [s for s in symbol_snapshots if s.get("phase") == "HOLD"]
264
+
265
+ assert entry_snapshot is not None, f"No entry snapshot recorded for {symbol}"
266
+ assert sell_snapshot is not None, f"No sell snapshot recorded for {symbol}"
267
+
268
+ cash_before_entry = float(entry_snapshot["cash"])
269
+ entry_cash_after = float(entry["cash_after"])
270
+ margin_deposit = cash_before_entry - entry_cash_after - fee_amount
271
+ expected_margin_total = expected_margin * float(entry["quantity"])
272
+
273
+ print(f"\nCASH / MARGIN STATE:")
274
+ print(f" Cash before entry: ${cash_before_entry:,.2f}")
275
+ print(f" Cash after entry: ${entry_cash_after:,.2f}")
276
+ print(f" Margin captured: ${margin_deposit:,.2f} (expected ${expected_margin_total:,.2f})")
277
+ assert pytest.approx(margin_deposit, abs=0.01) == expected_margin_total, (
278
+ f"{symbol} margin mismatch: expected ${expected_margin_total:,.2f}, "
279
+ f"got ${margin_deposit:,.2f}"
280
+ )
281
+
282
+ # Verify mark-to-market during hold period is exact
283
+ for snap in hold_snapshots:
284
+ price = snap.get("price")
285
+ if price is None:
286
+ continue
287
+ unrealized = (price - entry["price"]) * float(entry["quantity"]) * expected_multiplier
288
+ expected_portfolio = entry_cash_after + margin_deposit + unrealized
289
+ assert pytest.approx(expected_portfolio, abs=0.01) == float(snap["portfolio"]), (
290
+ f"{symbol} mark-to-market mismatch at {snap['datetime']}: "
291
+ f"expected ${expected_portfolio:,.2f}, got ${snap['portfolio']:,.2f}"
292
+ )
293
+
294
+ # Snapshot immediately before exit should have identical cash to post-entry state
295
+ assert pytest.approx(float(sell_snapshot["cash"]), abs=0.01) == entry_cash_after, (
296
+ f"{symbol} cash prior to exit changed unexpectedly: "
297
+ f"{sell_snapshot['cash']} vs {entry_cash_after}"
298
+ )
262
299
 
263
300
  if len(exits) > 0 and len(entries) > 0:
264
301
  entry = entries[0]
@@ -283,79 +320,21 @@ class TestDatabentoComprehensiveTrading:
283
320
  print(f" Price change: ${price_change:.2f}")
284
321
  print(f" Expected P&L: ${expected_pnl:.2f} (change × qty × {expected_multiplier})")
285
322
 
286
- # Verify final portfolio reflects P&L
287
- # Note: We can't verify exact final cash without knowing all previous trades,
288
- # but we can verify the P&L calculation makes sense
289
- assert abs(expected_pnl) < 100000, \
290
- f"{symbol} P&L seems unrealistic: {expected_pnl}"
291
-
292
- # CRITICAL: Verify portfolio value changed by approximately expected P&L
293
- # (can't be exact due to fees and previous trades, but should be in ballpark)
294
- entry_portfolio = entry['portfolio_after']
295
- exit_portfolio = exit_trade['portfolio_after']
296
- portfolio_change = exit_portfolio - entry_portfolio
297
-
298
- # Portfolio change should be close to expected P&L (within margin for fees/rounding)
299
- pnl_diff = abs(portfolio_change - expected_pnl)
300
- print(f" Portfolio change: ${portfolio_change:.2f}")
301
- print(f" Difference from expected: ${pnl_diff:.2f}")
302
-
303
- # Allow generous tolerance for fees, rounding, and concurrent trades
304
- # For small P&L, allow larger percentage; for large P&L, allow smaller percentage
305
- tolerance = max(abs(expected_pnl) * 0.5, 500)
306
- # For this comprehensive test with multiple concurrent trades, just verify it's reasonable
307
- # (exact match is tested in simpler single-trade tests)
308
- if pnl_diff < tolerance:
309
- print(f" ✓ Portfolio change matches expected P&L within tolerance")
310
- else:
311
- print(f" ⚠ Portfolio change differs (may be due to concurrent trades)")
312
-
313
- # CRITICAL: Verify unrealized P&L during HOLD periods
314
- # This catches bugs in portfolio value calculation (multiplier applied to unrealized P&L)
315
- print(f"\n" + "-"*80)
316
- print("VERIFYING UNREALIZED P&L DURING HOLD PERIODS")
317
- print("-"*80)
318
-
319
- for symbol in trades_by_instrument.keys():
320
- # Find snapshots where we're holding this position
321
- holding_snapshots = [s for s in strat.snapshots if s['position_qty'] > 0 and s.get('current_asset') == symbol]
322
-
323
- if len(holding_snapshots) >= 2:
324
- # Check a couple of snapshots during the hold
325
- snap = holding_snapshots[len(holding_snapshots)//2] # middle of hold period
326
-
327
- # Get the entry trade for this position
328
- entries = [t for t in trades_by_instrument[symbol] if "buy" in str(t["side"]).lower()]
329
- if entries:
330
- entry = entries[0]
331
- entry_price = entry['price']
332
- quantity = entry['quantity']
333
- current_price = snap['price']
334
- expected_mult = CONTRACT_SPECS.get(symbol, {}).get("multiplier", 1)
335
- expected_margin = CONTRACT_SPECS.get(symbol, {}).get("margin", 1000)
336
-
337
- # Calculate expected portfolio value
338
- cash = snap['cash']
339
- margin_tied_up = quantity * expected_margin
340
- unrealized_pnl = (current_price - entry_price) * quantity * expected_mult
341
- expected_portfolio = cash + margin_tied_up + unrealized_pnl
342
- actual_portfolio = snap['portfolio']
343
-
344
- print(f"\n{symbol} during HOLD (snapshot {strat.snapshots.index(snap)}):")
345
- print(f" Entry: ${entry_price:.2f} × {quantity} contracts")
346
- print(f" Current: ${current_price:.2f}")
347
- print(f" Cash: ${cash:,.2f}")
348
- print(f" Margin: ${margin_tied_up:,.2f}")
349
- print(f" Unrealized P&L: ${unrealized_pnl:,.2f} = (${current_price:.2f} - ${entry_price:.2f}) × {quantity} × {expected_mult}")
350
- print(f" Expected portfolio: ${expected_portfolio:,.2f}")
351
- print(f" Actual portfolio: ${actual_portfolio:,.2f}")
352
- print(f" Difference: ${abs(actual_portfolio - expected_portfolio):,.2f}")
353
-
354
- # This tolerance should catch multiplier bugs (5x error would be huge)
355
- tolerance = max(abs(expected_portfolio) * 0.02, 100) # 2% or $100
356
- assert abs(actual_portfolio - expected_portfolio) < tolerance, \
357
- f"{symbol} portfolio value incorrect during hold: expected ${expected_portfolio:,.2f}, got ${actual_portfolio:,.2f}"
358
- print(f" ✓ Portfolio value matches expected (within ${tolerance:.2f})")
323
+ cash_before_entry = float(snapshots_by_symbol[symbol][0]["cash"])
324
+ expected_cash_after_exit = (
325
+ cash_before_entry
326
+ - fee_amount # entry fee
327
+ - fee_amount # exit fee
328
+ + expected_pnl
329
+ )
330
+ print(f"\nCASH RECONCILIATION:")
331
+ print(f" Expected cash after exit: ${expected_cash_after_exit:,.2f}")
332
+ actual_cash_after_exit = float(exit_trade["cash_after"])
333
+ print(f" Actual cash after exit: ${actual_cash_after_exit:,.2f}")
334
+ assert pytest.approx(expected_cash_after_exit, abs=0.01) == actual_cash_after_exit, (
335
+ f"{symbol} cash after exit mismatch: expected ${expected_cash_after_exit:,.2f}, "
336
+ f"got ${actual_cash_after_exit:,.2f}"
337
+ )
359
338
 
360
339
  print(f"\n" + "="*80)
361
340
  print("✓ ALL INSTRUMENTS VERIFIED")
@@ -1,9 +1,10 @@
1
1
  """Parity checks between DataBento pandas and polars backends."""
2
2
 
3
3
  import shutil
4
- from pathlib import Path
5
4
  from datetime import datetime
5
+ from pathlib import Path
6
6
 
7
+ import numpy as np
7
8
  import pandas as pd
8
9
  import pytest
9
10
  import pytz
@@ -56,10 +57,31 @@ def test_databento_price_parity():
56
57
  )
57
58
 
58
59
  # Prime caches
59
- pandas_bars = pandas_ds.get_historical_prices(asset, 500, timestep="minute").df
60
- polars_bars = polars_ds.get_historical_prices(asset, 500, timestep="minute").df
60
+ pandas_bars = pandas_ds.get_historical_prices(asset, 500, timestep="minute").df.sort_index()
61
+ polars_bars = polars_ds.get_historical_prices(asset, 500, timestep="minute").df.sort_index()
62
+
63
+ candidate_columns = ["open", "high", "low", "close", "volume", "vwap"]
64
+ common_columns = [col for col in candidate_columns if col in pandas_bars.columns and col in polars_bars.columns]
65
+ assert common_columns, "No shared OHLCV columns between pandas and polars DataFrames"
61
66
 
62
- pd.testing.assert_frame_equal(polars_bars, pandas_bars)
67
+ aligned_pandas = pandas_bars[common_columns].copy()
68
+ aligned_polars = polars_bars[common_columns].copy()
69
+
70
+ for col in common_columns:
71
+ dtype_left = aligned_pandas[col].dtype
72
+ dtype_right = aligned_polars[col].dtype
73
+ if dtype_left != dtype_right:
74
+ target_dtype = np.promote_types(dtype_left, dtype_right)
75
+ aligned_pandas[col] = aligned_pandas[col].astype(target_dtype)
76
+ aligned_polars[col] = aligned_polars[col].astype(target_dtype)
77
+
78
+ pd.testing.assert_frame_equal(
79
+ aligned_pandas,
80
+ aligned_polars,
81
+ check_exact=True,
82
+ check_index_type=True,
83
+ check_column_type=True,
84
+ )
63
85
 
64
86
  checkpoints = [
65
87
  (0, 0),
@@ -76,6 +98,6 @@ def test_databento_price_parity():
76
98
  polars_ds._datetime = current_dt
77
99
  pandas_price = pandas_ds.get_last_price(asset)
78
100
  polars_price = polars_ds.get_last_price(asset)
79
- assert pandas_price == pytest.approx(polars_price), (
101
+ assert pandas_price == polars_price, (
80
102
  f"Mismatch at {current_dt}: pandas={pandas_price}, polars={polars_price}"
81
103
  )
@@ -31,6 +31,8 @@ DATABENTO_API_KEY = DATABENTO_CONFIG.get("API_KEY")
31
31
  # Contract specs
32
32
  MES_MULTIPLIER = 5
33
33
  ES_MULTIPLIER = 50
34
+ MES_MARGIN = 1300
35
+ ES_MARGIN = 13000
34
36
 
35
37
 
36
38
  def _clear_polars_cache():
@@ -211,6 +213,8 @@ class TestFuturesEdgeCases:
211
213
 
212
214
  broker = BacktestingBroker(data_source=data_source)
213
215
  fee = TradingFee(flat_fee=0.50)
216
+ fee_amount = float(fee.flat_fee)
217
+ fee_amount = float(fee.flat_fee)
214
218
 
215
219
  strat = ShortSellingStrategy(
216
220
  broker=broker,
@@ -252,7 +256,7 @@ class TestFuturesEdgeCases:
252
256
  assert trade['multiplier'] == MES_MULTIPLIER, \
253
257
  f"MES multiplier should be {MES_MULTIPLIER}, got {trade['multiplier']}"
254
258
 
255
- # If we have both entry and exit, verify P&L
259
+ # If we have both entry and exit, verify P&L and cash bookkeeping exactly
256
260
  if len(strat.trades) >= 2:
257
261
  entry = strat.trades[0] # Sell to open
258
262
  exit_trade = strat.trades[1] # Buy to cover
@@ -261,10 +265,19 @@ class TestFuturesEdgeCases:
261
265
  print("P&L VERIFICATION (SHORT TRADE)")
262
266
  print("-"*80)
263
267
 
264
- # For short: P&L = (Entry - Exit) × Qty × Multiplier (inverted!)
268
+ entry_snapshot = strat.snapshots[0]
269
+ holding_snapshots = [s for s in strat.snapshots if s['position_qty'] < 0]
270
+ assert holding_snapshots, "No holding snapshots recorded for short position"
271
+ sell_snapshot = holding_snapshots[-1] # still short immediately before closing
272
+ final_snapshot = next((s for s in strat.snapshots if s['position_qty'] == 0 and s['iteration'] > sell_snapshot['iteration']), strat.snapshots[-1])
273
+
274
+ margin_deposit = float(entry_snapshot['cash']) - float(entry['cash_after']) - fee_amount
275
+ print(f" Margin captured: ${margin_deposit:.2f}")
276
+ assert pytest.approx(margin_deposit, abs=0.01) == 1300.0, f"Expected $1,300 margin, got ${margin_deposit:.2f}"
277
+
265
278
  entry_price = entry['price']
266
279
  exit_price = exit_trade['price']
267
- price_change = entry_price - exit_price # Inverted for short
280
+ price_change = entry_price - exit_price # inverted for short
268
281
  expected_pnl = price_change * MES_MULTIPLIER
269
282
 
270
283
  print(f" Entry (sell): ${entry_price:.2f}")
@@ -272,23 +285,34 @@ class TestFuturesEdgeCases:
272
285
  print(f" Price change (entry - exit): ${price_change:.2f}")
273
286
  print(f" Expected P&L: ${expected_pnl:.2f} (inverted for short)")
274
287
 
275
- # Verify final cash
276
- starting_cash = 100000
277
- total_fees = 1.00 # 2 × $0.50
278
- expected_final_cash = starting_cash + expected_pnl - total_fees
279
-
280
- # Get final snapshot cash
281
- final_cash = strat.snapshots[-1]['cash']
282
- cash_diff = abs(expected_final_cash - final_cash)
283
-
284
- print(f"\n Starting cash: ${starting_cash:,.2f}")
285
- print(f" Expected final cash: ${expected_final_cash:,.2f}")
286
- print(f" Actual final cash: ${final_cash:,.2f}")
287
- print(f" Difference: ${cash_diff:.2f}")
288
-
289
- # Allow tolerance for timing/fill differences
290
- assert cash_diff < 150, f"Cash difference too large: ${cash_diff:.2f}"
291
- print(f"\n✓ PASS: Short selling P&L is correct")
288
+ # Mark-to-market validation during the hold window
289
+ for snap in holding_snapshots[:-1]:
290
+ current_price = snap['price']
291
+ if current_price is None:
292
+ continue
293
+ unrealized = (entry_price - current_price) * MES_MULTIPLIER
294
+ expected_portfolio = float(entry['cash_after']) + margin_deposit + unrealized
295
+ assert pytest.approx(expected_portfolio, abs=0.01) == float(snap['portfolio']), (
296
+ f"Short unrealized P&L mismatch at {snap['datetime']}: "
297
+ f"expected ${expected_portfolio:,.2f}, got ${snap['portfolio']:,.2f}"
298
+ )
299
+
300
+ assert pytest.approx(float(sell_snapshot['cash']), abs=0.01) == float(entry['cash_after']), \
301
+ "Cash should not drift while holding the short position"
302
+
303
+ # Final cash should equal initial cash minus fees plus realized P&L
304
+ starting_cash = float(entry_snapshot['cash'])
305
+ expected_final_cash = starting_cash - 2 * fee_amount + expected_pnl
306
+ print(f"\n Expected final cash: ${expected_final_cash:,.2f}")
307
+ actual_final_cash = float(final_snapshot['cash'])
308
+ print(f" Actual final cash: ${actual_final_cash:,.2f}")
309
+ assert pytest.approx(expected_final_cash, abs=0.01) == actual_final_cash, \
310
+ f"Final cash mismatch: expected ${expected_final_cash:,.2f}, got ${actual_final_cash:,.2f}"
311
+
312
+ assert pytest.approx(expected_final_cash, abs=0.01) == float(exit_trade['cash_after']), \
313
+ "Cash reported in exit trade callback should match final ledger"
314
+
315
+ print(f"\n✓ PASS: Short selling P&L and cash bookkeeping are exact")
292
316
 
293
317
  print("\n" + "="*80)
294
318
  print("✓ SHORT SELLING TEST PASSED")
@@ -383,54 +407,63 @@ class TestFuturesEdgeCases:
383
407
  assert trade['multiplier'] == ES_MULTIPLIER, \
384
408
  f"ES multiplier should be {ES_MULTIPLIER}, got {trade['multiplier']}"
385
409
 
386
- # Calculate expected P&L for each instrument
410
+ fee_amount = float(fee.flat_fee)
411
+ mes_entry = mes_trades[0]
412
+ mes_exit = mes_trades[1]
413
+ es_entry = es_trades[0]
414
+ es_exit = es_trades[1]
415
+
416
+ mes_entry_snapshot = next(s for s in strat.snapshots if s['datetime'] == mes_entry['datetime'])
417
+ es_entry_snapshot = next(s for s in strat.snapshots if s['datetime'] == es_entry['datetime'])
418
+
419
+ mes_margin = float(mes_entry_snapshot['cash']) - float(mes_entry['cash_after']) - fee_amount
420
+ es_margin = float(es_entry_snapshot['cash']) - float(es_entry['cash_after']) - fee_amount
421
+
422
+ assert pytest.approx(mes_margin, abs=0.01) == MES_MARGIN, \
423
+ f"MES margin should be ${MES_MARGIN}, got ${mes_margin:.2f}"
424
+ assert pytest.approx(es_margin, abs=0.01) == ES_MARGIN, \
425
+ f"ES margin should be ${ES_MARGIN}, got ${es_margin:.2f}"
426
+
387
427
  print("\n" + "-"*80)
388
- print("P&L VERIFICATION")
428
+ print("MARK-TO-MARKET VERIFICATION")
389
429
  print("-"*80)
390
430
 
391
- # MES P&L
392
- mes_entry = mes_trades[0]
393
- mes_exit = mes_trades[1]
394
- mes_pnl = (mes_exit['price'] - mes_entry['price']) * MES_MULTIPLIER
431
+ for snap in strat.snapshots:
432
+ expected_portfolio = float(snap['cash'])
395
433
 
396
- print(f"\nMES:")
397
- print(f" Entry: ${mes_entry['price']:.2f}")
398
- print(f" Exit: ${mes_exit['price']:.2f}")
399
- print(f" P&L: ${mes_pnl:.2f}")
434
+ if snap['mes_qty'] != 0 and snap.get('mes_price') is not None:
435
+ expected_portfolio += abs(snap['mes_qty']) * MES_MARGIN
436
+ expected_portfolio += (snap['mes_price'] - mes_entry['price']) * snap['mes_qty'] * MES_MULTIPLIER
400
437
 
401
- # ES P&L
402
- es_entry = es_trades[0]
403
- es_exit = es_trades[1]
404
- es_pnl = (es_exit['price'] - es_entry['price']) * ES_MULTIPLIER
438
+ if snap['es_qty'] != 0 and snap.get('es_price') is not None:
439
+ expected_portfolio += abs(snap['es_qty']) * ES_MARGIN
440
+ expected_portfolio += (snap['es_price'] - es_entry['price']) * snap['es_qty'] * ES_MULTIPLIER
405
441
 
406
- print(f"\nES:")
407
- print(f" Entry: ${es_entry['price']:.2f}")
408
- print(f" Exit: ${es_exit['price']:.2f}")
409
- print(f" P&L: ${es_pnl:.2f}")
442
+ assert pytest.approx(expected_portfolio, abs=0.01) == float(snap['portfolio']), (
443
+ f"Portfolio mismatch at {snap['datetime']}: "
444
+ f"expected ${expected_portfolio:,.2f}, got ${snap['portfolio']:,.2f}"
445
+ )
410
446
 
411
- # Total P&L
447
+ mes_pnl = (mes_exit['price'] - mes_entry['price']) * MES_MULTIPLIER
448
+ es_pnl = (es_exit['price'] - es_entry['price']) * ES_MULTIPLIER
412
449
  total_pnl = mes_pnl + es_pnl
413
- total_fees = 4.00 # 4 trades × $0.50 each (assuming $0.50 per side)
414
-
415
- print(f"\nTotal:")
416
- print(f" Combined P&L: ${total_pnl:.2f}")
417
- print(f" Total fees: ${total_fees:.2f}")
418
- print(f" Net P&L: ${total_pnl - total_fees:.2f}")
419
-
420
- # Verify final cash
421
- starting_cash = 100000
422
- expected_final_cash = starting_cash + total_pnl - total_fees
423
- final_cash = strat.snapshots[-1]['cash']
424
- cash_diff = abs(expected_final_cash - final_cash)
425
-
426
- print(f"\n Starting cash: ${starting_cash:,.2f}")
427
- print(f" Expected final cash: ${expected_final_cash:,.2f}")
428
- print(f" Actual final cash: ${final_cash:,.2f}")
429
- print(f" Difference: ${cash_diff:.2f}")
430
-
431
- # Allow tolerance
432
- assert cash_diff < 200, f"Cash difference too large: ${cash_diff:.2f}"
433
- print(f"\n✓ PASS: Multiple simultaneous positions tracked correctly")
450
+ total_fees = 4 * fee_amount
451
+
452
+ starting_cash = float(strat.snapshots[0]['cash'])
453
+ expected_final_cash = starting_cash - total_fees + total_pnl
454
+ final_cash = float(strat.snapshots[-1]['cash'])
455
+
456
+ print(f"\nTotal realised P&L: ${total_pnl:.2f}")
457
+ print(f"Total fees: ${total_fees:.2f}")
458
+ print(f"Expected final cash: ${expected_final_cash:,.2f}")
459
+ print(f"Actual final cash: ${final_cash:,.2f}")
460
+
461
+ assert pytest.approx(expected_final_cash, abs=0.01) == final_cash, \
462
+ f"Final cash mismatch: expected ${expected_final_cash:,.2f}, got ${final_cash:,.2f}"
463
+ assert pytest.approx(expected_final_cash, abs=0.01) == float(es_exit['cash_after']), \
464
+ "Exit trade cash should match ledger final cash"
465
+
466
+ print(f"\n✓ PASS: Multiple simultaneous positions tracked with exact accounting")
434
467
 
435
468
  # Verify we had both positions at the same time (iteration 3-4)
436
469
  snapshot_with_both = None