lumibot 4.0.21__tar.gz → 4.0.23__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.
- {lumibot-4.0.21/lumibot.egg-info → lumibot-4.0.23}/PKG-INFO +1 -1
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/backtesting/__init__.py +3 -3
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/__init__.py +2 -1
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/databento_data.py +5 -5
- lumibot-4.0.23/lumibot/data_sources/databento_data_polars_backtesting.py +490 -0
- lumibot-4.0.21/lumibot/data_sources/databento_data_polars.py → lumibot-4.0.23/lumibot/data_sources/databento_data_polars_live.py +2 -2
- {lumibot-4.0.21 → lumibot-4.0.23/lumibot.egg-info}/PKG-INFO +1 -1
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot.egg-info/SOURCES.txt +2 -1
- {lumibot-4.0.21 → lumibot-4.0.23}/setup.py +1 -1
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_databento.py +56 -2
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_databento_live.py +10 -10
- {lumibot-4.0.21 → lumibot-4.0.23}/LICENSE +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/MANIFEST.in +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/README.md +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/__init__.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/backtesting/alpaca_backtesting.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/backtesting/alpha_vantage_backtesting.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/backtesting/backtesting_broker.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/backtesting/ccxt_backtesting.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/backtesting/databento_backtesting.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/backtesting/databento_backtesting_polars.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/backtesting/interactive_brokers_rest_backtesting.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/backtesting/pandas_backtesting.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/backtesting/polygon_backtesting.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/backtesting/thetadata_backtesting.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/backtesting/yahoo_backtesting.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/brokers/__init__.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/brokers/alpaca.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/brokers/bitunix.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/brokers/broker.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/brokers/ccxt.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/brokers/example_broker.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/brokers/interactive_brokers.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/brokers/interactive_brokers_rest.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/brokers/projectx.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/brokers/schwab.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/brokers/tradier.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/brokers/tradovate.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/components/__init__.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/components/configs_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/components/drift_rebalancer_logic.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/components/grok_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/components/options_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/components/perplexity_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/components/quiver_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/components/vix_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/constants.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/credentials.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/alpaca_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/alpha_vantage_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/bitunix_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/ccxt_backtesting_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/ccxt_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/data_source.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/data_source_backtesting.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/example_broker_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/exceptions.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/interactive_brokers_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/interactive_brokers_rest_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/pandas_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/polars_mixin.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/polygon_data_polars.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/projectx_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/schwab_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/tradier_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/tradovate_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/yahoo_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/data_sources/yahoo_data_polars.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/entities/__init__.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/entities/asset.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/entities/bar.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/entities/bars.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/entities/chains.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/entities/data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/entities/dataline.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/entities/order.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/entities/position.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/entities/quote.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/entities/trading_fee.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/__init__.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/bitunix_futures_example.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/ccxt_backtesting_example.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/classic_60_40.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/classic_60_40_config.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/crypto_50_50.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/crypto_50_50_config.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/crypto_important_functions.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/drift_rebalancer.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/forex_hold_to_expiry.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/futures_hold_to_expiry.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/lifecycle_logger.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/options_hold_to_expiry.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/schedule_function.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/simple_start_single_file.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/stock_bracket.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/stock_buy_and_hold.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/stock_diversified_leverage.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/stock_limit_and_trailing_stops.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/stock_momentum.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/stock_oco.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/strangle.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/example_strategies/test_broker_functions.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/resources/conf.yaml +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/strategies/__init__.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/strategies/_strategy.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/strategies/session_manager.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/strategies/strategy.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/strategies/strategy_executor.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/__init__.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/alpaca_helpers.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/bitunix_helpers.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/black_scholes.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/ccxt_data_store.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/databento_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/databento_helper_polars.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/debugers.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/decorators.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/futures_symbols.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/helpers.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/indicators.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/lumibot_logger.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/lumibot_time.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/pandas.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/polygon_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/polygon_helper_async.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/polygon_helper_polars_optimized.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/projectx_helpers.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/schwab_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/thetadata_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/types.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/yahoo_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/tools/yahoo_helper_polars_optimized.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/traders/__init__.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/traders/debug_log_trader.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/traders/trader.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/trading_builtins/__init__.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/trading_builtins/custom_stream.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot/trading_builtins/safe_list.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot.egg-info/dependency_links.txt +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot.egg-info/requires.txt +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/lumibot.egg-info/top_level.txt +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/pyproject.toml +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/setup.cfg +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/__init__.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/__init__.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/conftest.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/performance_tracker.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_backtesting_broker_processing.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_buy_hold_quiet_logs_full_run.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_crypto_cash_regressions.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_dividends.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_example_strategies.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_failing_backtest.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_multileg_backtest.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_pandas_backtest.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_passing_trader_into_backtest.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_polygon.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_strategy_executor.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_thetadata.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/backtest/test_yahoo.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/conftest.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/fixtures.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_alpaca.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_alpaca_auth_fix.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_alpaca_backtesting.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_alpaca_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_alpaca_helpers.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_alpaca_multileg_fix.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_alpaca_oauth.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_apscheduler_warnings.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_asset.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_asset_auto_expiry.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_auto_market_inference.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_backtesting_broker.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_backtesting_broker_await_close.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_backtesting_broker_time_advance.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_backtesting_crypto_cash_unit.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_backtesting_flow_control.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_backtesting_multileg_unit.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_backtesting_quiet_logs_complete.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_bars_aggregate_frequency_normalization.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_bars_aggregation_timeunits.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_bars_frequency_flex.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_botspot_handler.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_botspot_logger.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_broker_bitunix.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_broker_cleanup.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_broker_initialization.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_brokers_handle_crypto.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_cash.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_ccxt.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_ccxt_store.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_configs_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_continuous_futures.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_continuous_futures_integration.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_continuous_futures_resolution.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_data_source.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_databento_asset_validation.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_databento_auto_expiry_integration.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_databento_backtesting.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_databento_backtesting_polars.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_databento_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_databento_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_databento_timezone_fixes.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_drift_rebalancer.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_futures_integration.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_get_historical_prices.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_helpers.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_indicator_subplots.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_integration_tests.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_interactive_brokers.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_live_trading_resilience.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_logger_env_vars.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_logging.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_lumibot_logger.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_market_infinite_loop_bug.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_mes_symbols.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_momentum.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_options_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_order.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_order_serialization.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_pandas_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_polygon_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_position_serialization.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_projectx.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_projectx_bracket_helpers.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_projectx_bracket_lifecycle_unit.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_projectx_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_projectx_datetime_columns.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_projectx_datetime_index.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_projectx_helpers.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_projectx_lifecycle.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_projectx_lifecycle_unit.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_projectx_live_flow.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_projectx_timestep_alias.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_projectx_url_mappings.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_quiet_logs_buy_and_hold.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_quiet_logs_comprehensive.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_quiet_logs_functionality.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_quiet_logs_requirements.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_session_manager.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_strategy_methods.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_thetadata_helper.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_tradier.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_tradier_data.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_tradingfee.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_tradovate.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_unified_logger.py +0 -0
- {lumibot-4.0.21 → lumibot-4.0.23}/tests/test_vix_helper.py +0 -0
|
@@ -8,8 +8,8 @@ from .polygon_backtesting import PolygonDataBacktesting
|
|
|
8
8
|
from .thetadata_backtesting import ThetaDataBacktesting
|
|
9
9
|
from .yahoo_backtesting import YahooDataBacktesting
|
|
10
10
|
|
|
11
|
-
# Import DataBento backtesting - use polars
|
|
11
|
+
# Import DataBento backtesting - use pandas version (polars version is slow)
|
|
12
12
|
try:
|
|
13
|
-
from .
|
|
13
|
+
from .databento_backtesting import DataBentoDataBacktesting
|
|
14
14
|
except ImportError:
|
|
15
|
-
from .
|
|
15
|
+
from .databento_backtesting_polars import DataBentoDataBacktestingPolars as DataBentoDataBacktesting
|
|
@@ -20,5 +20,6 @@ from .interactive_brokers_rest_data import InteractiveBrokersRESTData
|
|
|
20
20
|
from .schwab_data import SchwabData
|
|
21
21
|
from .tradovate_data import TradovateData
|
|
22
22
|
|
|
23
|
-
from .
|
|
23
|
+
from .databento_data_polars_live import DataBentoDataPolarsLive as DataBentoData
|
|
24
|
+
from .databento_data_polars_backtesting import DataBentoDataPolarsBacktesting as DataBentoDataBacktesting
|
|
24
25
|
from .projectx_data import ProjectXData
|
|
@@ -8,9 +8,9 @@ from lumibot.tools import databento_helper
|
|
|
8
8
|
from lumibot.tools.lumibot_logger import get_logger
|
|
9
9
|
|
|
10
10
|
try:
|
|
11
|
-
from .
|
|
11
|
+
from .databento_data_polars_live import DataBentoDataPolarsLive
|
|
12
12
|
except Exception: # pragma: no cover - optional dependency path
|
|
13
|
-
|
|
13
|
+
DataBentoDataPolarsLive = None
|
|
14
14
|
|
|
15
15
|
logger = get_logger(__name__)
|
|
16
16
|
|
|
@@ -330,13 +330,13 @@ class DataBentoData(DataSource):
|
|
|
330
330
|
price = self.get_last_price(asset, quote=quote)
|
|
331
331
|
return Quote(asset=asset, price=price)
|
|
332
332
|
|
|
333
|
-
def _ensure_live_delegate(self) -> Optional['
|
|
334
|
-
if
|
|
333
|
+
def _ensure_live_delegate(self) -> Optional['DataBentoDataPolarsLive']:
|
|
334
|
+
if DataBentoDataPolarsLive is None or self.is_backtesting_mode:
|
|
335
335
|
return None
|
|
336
336
|
|
|
337
337
|
if self._live_delegate is None:
|
|
338
338
|
try:
|
|
339
|
-
self._live_delegate =
|
|
339
|
+
self._live_delegate = DataBentoDataPolarsLive(
|
|
340
340
|
api_key=self._api_key,
|
|
341
341
|
has_paid_subscription=True,
|
|
342
342
|
enable_cache=False,
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"""Ultra-optimized DataBento backtesting using pure polars with zero pandas conversions.
|
|
2
|
+
|
|
3
|
+
This implementation:
|
|
4
|
+
1. Uses polars columnar storage directly
|
|
5
|
+
2. Lazy evaluation for maximum performance
|
|
6
|
+
3. Efficient caching with parquet files
|
|
7
|
+
4. Vectorized operations only
|
|
8
|
+
5. Inherits from DataSourceBacktesting (proper architecture)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import traceback
|
|
13
|
+
from datetime import datetime, timedelta
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
from typing import Dict, Optional, Union
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import polars as pl
|
|
19
|
+
|
|
20
|
+
from lumibot.data_sources import DataSourceBacktesting
|
|
21
|
+
from lumibot.data_sources.polars_mixin import PolarsMixin
|
|
22
|
+
from lumibot.entities import Asset, Bars
|
|
23
|
+
from lumibot.tools import databento_helper_polars
|
|
24
|
+
from lumibot.tools.lumibot_logger import get_logger
|
|
25
|
+
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
START_BUFFER = timedelta(days=5)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DataBentoDataPolarsBacktesting(PolarsMixin, DataSourceBacktesting):
|
|
31
|
+
"""Ultra-optimized DataBento backtesting data source with pure polars."""
|
|
32
|
+
|
|
33
|
+
SOURCE = "DATABENTO"
|
|
34
|
+
MIN_TIMESTEP = "minute"
|
|
35
|
+
TIMESTEP_MAPPING = [
|
|
36
|
+
{"timestep": "minute", "representations": ["1m", "minute", "1 minute"]},
|
|
37
|
+
{"timestep": "hour", "representations": ["1h", "hour", "1 hour"]},
|
|
38
|
+
{"timestep": "day", "representations": ["1d", "day", "1 day"]},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
datetime_start,
|
|
44
|
+
datetime_end,
|
|
45
|
+
api_key=None,
|
|
46
|
+
max_memory=None,
|
|
47
|
+
timeout=30,
|
|
48
|
+
max_retries=3,
|
|
49
|
+
**kwargs,
|
|
50
|
+
):
|
|
51
|
+
super().__init__(
|
|
52
|
+
datetime_start=datetime_start,
|
|
53
|
+
datetime_end=datetime_end,
|
|
54
|
+
api_key=api_key,
|
|
55
|
+
**kwargs
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self.name = "databento"
|
|
59
|
+
self._api_key = api_key or os.environ.get("DATABENTO_API_KEY")
|
|
60
|
+
self._timeout = timeout
|
|
61
|
+
self._max_retries = max_retries
|
|
62
|
+
self.MAX_STORAGE_BYTES = max_memory
|
|
63
|
+
|
|
64
|
+
# Initialize polars storage from mixin
|
|
65
|
+
self._init_polars_storage()
|
|
66
|
+
|
|
67
|
+
# DataBento-specific caches
|
|
68
|
+
self._eager_cache: Dict[Asset, pl.DataFrame] = {}
|
|
69
|
+
|
|
70
|
+
# Prefetch tracking - CRITICAL for performance
|
|
71
|
+
self._prefetch_cache: Dict[tuple, bool] = {}
|
|
72
|
+
self._prefetched_assets = set() # Track which assets have been fully loaded
|
|
73
|
+
|
|
74
|
+
logger.info(f"DataBento backtesting initialized for period: {datetime_start} to {datetime_end}")
|
|
75
|
+
|
|
76
|
+
def _enforce_storage_limit(self, data_store: Dict[Asset, pl.LazyFrame]):
|
|
77
|
+
"""Enforce storage limit by removing least recently used data."""
|
|
78
|
+
# Use mixin's enforce method
|
|
79
|
+
self._enforce_storage_limit_polars(self.MAX_STORAGE_BYTES)
|
|
80
|
+
|
|
81
|
+
# Clean up DataBento-specific caches
|
|
82
|
+
if self.MAX_STORAGE_BYTES and len(self._eager_cache) > 0:
|
|
83
|
+
# Remove from eager cache too
|
|
84
|
+
assets_to_remove = [a for a in self._eager_cache.keys() if a not in data_store]
|
|
85
|
+
for asset in assets_to_remove:
|
|
86
|
+
del self._eager_cache[asset]
|
|
87
|
+
|
|
88
|
+
def _store_data(self, asset: Asset, data: pl.DataFrame) -> pl.LazyFrame:
|
|
89
|
+
"""Store data efficiently using lazy frames.
|
|
90
|
+
|
|
91
|
+
Returns lazy frame for efficient subsequent operations.
|
|
92
|
+
"""
|
|
93
|
+
# Use mixin's store method first
|
|
94
|
+
lazy_data = self._store_data_polars(asset, data)
|
|
95
|
+
|
|
96
|
+
if lazy_data is None:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
# Update the stored data
|
|
100
|
+
self._data_store[asset] = lazy_data
|
|
101
|
+
|
|
102
|
+
# Enforce storage limit
|
|
103
|
+
if self.MAX_STORAGE_BYTES:
|
|
104
|
+
self._enforce_storage_limit(self._data_store)
|
|
105
|
+
|
|
106
|
+
return lazy_data
|
|
107
|
+
|
|
108
|
+
def get_start_datetime_and_ts_unit(self, length, timestep, start_dt=None, start_buffer=timedelta(days=5)):
|
|
109
|
+
"""
|
|
110
|
+
Get the start datetime for the data.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
length : int
|
|
115
|
+
The number of data points to get.
|
|
116
|
+
timestep : str
|
|
117
|
+
The timestep to use. For example, "minute" or "hour" or "day".
|
|
118
|
+
start_dt : datetime
|
|
119
|
+
The start datetime to use. If None, the current self.datetime_start will be used.
|
|
120
|
+
start_buffer : timedelta
|
|
121
|
+
The buffer to add to the start datetime.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
datetime
|
|
126
|
+
The start datetime.
|
|
127
|
+
str
|
|
128
|
+
The timestep unit.
|
|
129
|
+
"""
|
|
130
|
+
# Convert timestep string to timedelta and get start datetime
|
|
131
|
+
td, ts_unit = self.convert_timestep_str_to_timedelta(timestep)
|
|
132
|
+
if ts_unit == "day":
|
|
133
|
+
weeks_requested = length // 5 # Full trading week is 5 days
|
|
134
|
+
extra_padding_days = weeks_requested * 3 # to account for 3day weekends
|
|
135
|
+
td = timedelta(days=length + extra_padding_days)
|
|
136
|
+
else:
|
|
137
|
+
td *= length
|
|
138
|
+
if start_dt is not None:
|
|
139
|
+
start_datetime = start_dt - td
|
|
140
|
+
else:
|
|
141
|
+
start_datetime = self.datetime_start - td
|
|
142
|
+
start_datetime = start_datetime - start_buffer
|
|
143
|
+
return start_datetime, ts_unit
|
|
144
|
+
|
|
145
|
+
def is_data_cached(self, asset: Asset, start_dt, end_dt, timestep: str) -> bool:
|
|
146
|
+
"""
|
|
147
|
+
Check if data is already cached for the given parameters.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
asset : Asset
|
|
152
|
+
The asset to check
|
|
153
|
+
start_dt : datetime
|
|
154
|
+
Start datetime
|
|
155
|
+
end_dt : datetime
|
|
156
|
+
End datetime
|
|
157
|
+
timestep : str
|
|
158
|
+
Time granularity
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
bool
|
|
163
|
+
True if data is cached, False otherwise
|
|
164
|
+
"""
|
|
165
|
+
search_asset = asset
|
|
166
|
+
if isinstance(asset, tuple):
|
|
167
|
+
search_asset = asset
|
|
168
|
+
|
|
169
|
+
# Check if in data store
|
|
170
|
+
if search_asset not in self._data_store:
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
# Check if in filtered cache for daily data
|
|
174
|
+
if timestep == "day":
|
|
175
|
+
cache_key = (search_asset, start_dt.date(), timestep)
|
|
176
|
+
if cache_key in self._filtered_data_cache:
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
# Check prefetch cache
|
|
180
|
+
cache_key = (search_asset, start_dt.date(), end_dt.date(), timestep)
|
|
181
|
+
return cache_key in self._prefetch_cache
|
|
182
|
+
|
|
183
|
+
def _update_data(self, asset: Asset, quote: Asset, length: int, timestep: str, start_dt=None):
|
|
184
|
+
"""
|
|
185
|
+
Get asset data and update the self._data_store dictionary.
|
|
186
|
+
|
|
187
|
+
Parameters
|
|
188
|
+
----------
|
|
189
|
+
asset : Asset
|
|
190
|
+
The asset to get data for.
|
|
191
|
+
quote : Asset
|
|
192
|
+
The quote asset to use. For example, if asset is "SPY" and quote is "USD", the data will be for "SPY/USD".
|
|
193
|
+
length : int
|
|
194
|
+
The number of data points to get.
|
|
195
|
+
timestep : str
|
|
196
|
+
The timestep to use. For example, "minute" or "hour" or "day".
|
|
197
|
+
start_dt : datetime
|
|
198
|
+
The start datetime to use. If None, the current self.start_datetime will be used.
|
|
199
|
+
"""
|
|
200
|
+
search_asset = asset
|
|
201
|
+
asset_separated = asset
|
|
202
|
+
quote_asset = quote if quote is not None else Asset("USD", "forex")
|
|
203
|
+
|
|
204
|
+
if isinstance(search_asset, tuple):
|
|
205
|
+
asset_separated, quote_asset = search_asset
|
|
206
|
+
else:
|
|
207
|
+
search_asset = (search_asset, quote_asset)
|
|
208
|
+
|
|
209
|
+
# CRITICAL: If asset was prefetched, don't fetch again!
|
|
210
|
+
if search_asset in self._prefetched_assets:
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
# Check if we already have data in the store
|
|
214
|
+
if search_asset in self._data_store:
|
|
215
|
+
# Data already loaded, mark as prefetched and return
|
|
216
|
+
self._prefetched_assets.add(search_asset)
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
# Get the start datetime and timestep unit
|
|
220
|
+
start_datetime, ts_unit = self.get_start_datetime_and_ts_unit(
|
|
221
|
+
length, timestep, start_dt, start_buffer=START_BUFFER
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Fetch data for ENTIRE backtest period (like pandas does)
|
|
225
|
+
start_datetime = self.datetime_start - START_BUFFER
|
|
226
|
+
end_datetime = self.datetime_end + timedelta(days=1)
|
|
227
|
+
|
|
228
|
+
logger.info(f"Prefetching {asset_separated.symbol} data from {start_datetime.date()} to {end_datetime.date()}")
|
|
229
|
+
|
|
230
|
+
# Check if we have data for this asset
|
|
231
|
+
if search_asset in self._data_store:
|
|
232
|
+
# For daily timestep, use optimized caching strategy
|
|
233
|
+
if ts_unit == "day":
|
|
234
|
+
# Check if we need to clear cache for new date
|
|
235
|
+
current_date = self._datetime.date()
|
|
236
|
+
|
|
237
|
+
# Try to get from filtered cache first
|
|
238
|
+
cache_key = (search_asset, current_date, ts_unit)
|
|
239
|
+
if cache_key in self._filtered_data_cache:
|
|
240
|
+
result = self._filtered_data_cache[cache_key]
|
|
241
|
+
if len(result) >= length:
|
|
242
|
+
# Cache hit!
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
# Download data from DataBento using polars helper
|
|
246
|
+
try:
|
|
247
|
+
df = databento_helper_polars.get_price_data_from_databento_polars(
|
|
248
|
+
api_key=self._api_key,
|
|
249
|
+
asset=asset_separated,
|
|
250
|
+
start=start_datetime,
|
|
251
|
+
end=end_datetime,
|
|
252
|
+
timestep=timestep,
|
|
253
|
+
venue=None,
|
|
254
|
+
force_cache_update=False
|
|
255
|
+
)
|
|
256
|
+
except Exception as e:
|
|
257
|
+
# Handle all exceptions
|
|
258
|
+
logger.error(f"Error getting data from DataBento: {e}")
|
|
259
|
+
logger.error(traceback.format_exc())
|
|
260
|
+
# Mark as prefetched even on error to avoid retry loops
|
|
261
|
+
self._prefetched_assets.add(search_asset)
|
|
262
|
+
raise Exception("Error getting data from DataBento") from e
|
|
263
|
+
|
|
264
|
+
if (df is None) or len(df) == 0:
|
|
265
|
+
logger.warning(
|
|
266
|
+
f"DataBento returned no data: asset={getattr(asset_separated, 'symbol', asset_separated)} "
|
|
267
|
+
f"quote={getattr(quote_asset, 'symbol', quote_asset)} "
|
|
268
|
+
f"timestep={timestep} start={start_datetime.strftime('%Y-%m-%d %H:%M:%S')} "
|
|
269
|
+
f"end={end_datetime.strftime('%Y-%m-%d %H:%M:%S')} len=0"
|
|
270
|
+
)
|
|
271
|
+
# Mark as prefetched to avoid retry
|
|
272
|
+
self._prefetched_assets.add(search_asset)
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
# Store data
|
|
276
|
+
self._store_data(search_asset, df)
|
|
277
|
+
logger.info(f"Cached {len(df)} rows for {asset_separated.symbol}")
|
|
278
|
+
|
|
279
|
+
# Mark as prefetched
|
|
280
|
+
self._prefetched_assets.add(search_asset)
|
|
281
|
+
|
|
282
|
+
def _pull_source_symbol_bars(
|
|
283
|
+
self,
|
|
284
|
+
asset: Asset,
|
|
285
|
+
length: int,
|
|
286
|
+
timestep: str = "day",
|
|
287
|
+
timeshift: int = None,
|
|
288
|
+
quote: Asset = None,
|
|
289
|
+
exchange: str = None,
|
|
290
|
+
include_after_hours: bool = True,
|
|
291
|
+
) -> Optional[pl.DataFrame]:
|
|
292
|
+
"""Pull bars with maximum efficiency using pre-filtered cache."""
|
|
293
|
+
|
|
294
|
+
# Build search key
|
|
295
|
+
search_asset = asset if not isinstance(asset, tuple) else asset
|
|
296
|
+
if quote:
|
|
297
|
+
search_asset = (asset, quote)
|
|
298
|
+
|
|
299
|
+
# For daily timestep, use optimized caching strategy
|
|
300
|
+
if timestep == "day":
|
|
301
|
+
current_date = self._datetime.date()
|
|
302
|
+
cache_key = (search_asset, current_date, timestep)
|
|
303
|
+
|
|
304
|
+
# Try cache first
|
|
305
|
+
if cache_key in self._filtered_data_cache:
|
|
306
|
+
result = self._filtered_data_cache[cache_key]
|
|
307
|
+
if len(result) >= length:
|
|
308
|
+
return result.tail(length)
|
|
309
|
+
|
|
310
|
+
# Get the current datetime and calculate the start datetime
|
|
311
|
+
current_dt = self.get_datetime()
|
|
312
|
+
# Get data from DataBento
|
|
313
|
+
self._update_data(asset, quote, length, timestep, current_dt)
|
|
314
|
+
|
|
315
|
+
# Get lazy data
|
|
316
|
+
search_asset = asset if not isinstance(asset, tuple) else asset
|
|
317
|
+
if quote:
|
|
318
|
+
search_asset = (asset, quote)
|
|
319
|
+
|
|
320
|
+
lazy_data = self._get_data_lazy(search_asset)
|
|
321
|
+
|
|
322
|
+
if lazy_data is None:
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
# Use lazy evaluation and collect only when needed
|
|
326
|
+
# Check if we have cached filtered data first
|
|
327
|
+
if timestep == "day":
|
|
328
|
+
current_date = self._datetime.date()
|
|
329
|
+
cache_key = (search_asset, current_date, timestep)
|
|
330
|
+
if cache_key in self._filtered_data_cache:
|
|
331
|
+
data = self._filtered_data_cache[cache_key]
|
|
332
|
+
else:
|
|
333
|
+
# Collect with filtering for efficiency
|
|
334
|
+
data = lazy_data.collect()
|
|
335
|
+
else:
|
|
336
|
+
# For minute data, collect on demand
|
|
337
|
+
data = lazy_data.collect()
|
|
338
|
+
|
|
339
|
+
# OPTIMIZATION: Direct filtering on eager DataFrame
|
|
340
|
+
current_dt = self.to_default_timezone(self._datetime)
|
|
341
|
+
|
|
342
|
+
# Determine end filter
|
|
343
|
+
if timestep == "day":
|
|
344
|
+
dt = self._datetime.replace(hour=23, minute=59, second=59, microsecond=999999)
|
|
345
|
+
end_filter = dt - timedelta(days=1)
|
|
346
|
+
else:
|
|
347
|
+
end_filter = current_dt
|
|
348
|
+
|
|
349
|
+
if timeshift:
|
|
350
|
+
if isinstance(timeshift, int):
|
|
351
|
+
timeshift = timedelta(days=timeshift)
|
|
352
|
+
end_filter = end_filter - timeshift
|
|
353
|
+
|
|
354
|
+
logger.debug(f"Filtering {asset.symbol} data: current_dt={current_dt}, end_filter={end_filter}, timestep={timestep}, timeshift={timeshift}")
|
|
355
|
+
|
|
356
|
+
# Convert to lazy frame for filtering
|
|
357
|
+
lazy_data = data.lazy() if not hasattr(data, 'collect') else data
|
|
358
|
+
|
|
359
|
+
# Use mixin's filter method
|
|
360
|
+
result = self._filter_data_polars(search_asset, lazy_data, end_filter, length, timestep)
|
|
361
|
+
|
|
362
|
+
if result is None:
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
if len(result) < length:
|
|
366
|
+
logger.debug(
|
|
367
|
+
f"Requested {length} bars but only {len(result)} available "
|
|
368
|
+
f"for {asset.symbol} before {end_filter}"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
logger.debug(f"Returning {len(result)} bars for {asset.symbol}")
|
|
372
|
+
|
|
373
|
+
return result
|
|
374
|
+
|
|
375
|
+
def _parse_source_symbol_bars(
|
|
376
|
+
self,
|
|
377
|
+
response: pl.DataFrame,
|
|
378
|
+
asset: Asset,
|
|
379
|
+
quote: Optional[Asset] = None,
|
|
380
|
+
length: Optional[int] = None,
|
|
381
|
+
return_polars: bool = False,
|
|
382
|
+
) -> Bars:
|
|
383
|
+
"""Parse bars from polars DataFrame."""
|
|
384
|
+
if quote is not None:
|
|
385
|
+
logger.warning(f"quote is not implemented for DataBentoData, but {quote} was passed as the quote")
|
|
386
|
+
|
|
387
|
+
# Use mixin's parse method
|
|
388
|
+
return self._parse_source_symbol_bars_polars(
|
|
389
|
+
response, asset, self.SOURCE, quote, length, return_polars=return_polars
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def get_last_price(
|
|
393
|
+
self,
|
|
394
|
+
asset: Asset,
|
|
395
|
+
timestep: str = "minute",
|
|
396
|
+
quote: Optional[Asset] = None,
|
|
397
|
+
exchange: Optional[str] = None,
|
|
398
|
+
**kwargs
|
|
399
|
+
) -> Union[float, Decimal, None]:
|
|
400
|
+
"""Get last price with aggressive caching."""
|
|
401
|
+
|
|
402
|
+
if timestep is None:
|
|
403
|
+
timestep = self.get_timestep()
|
|
404
|
+
|
|
405
|
+
# Use mixin's cache check
|
|
406
|
+
current_datetime = self._datetime
|
|
407
|
+
cached_price = self._get_cached_last_price_polars(asset, current_datetime, timestep)
|
|
408
|
+
if cached_price is not None:
|
|
409
|
+
return cached_price
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
dt = self.get_datetime()
|
|
413
|
+
self._update_data(asset, quote, 1, timestep, dt)
|
|
414
|
+
except Exception as e:
|
|
415
|
+
logger.error(f"Error get_last_price from DataBento: {e}")
|
|
416
|
+
logger.error(f"Error get_last_price from DataBento: {asset=} {quote=} {timestep=} {dt=} {e}")
|
|
417
|
+
self._cache_last_price_polars(asset, None, current_datetime, timestep)
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
# Get price efficiently
|
|
421
|
+
# For daily data, don't apply additional timeshift since _pull_source_symbol_bars
|
|
422
|
+
# already handles getting the previous day's data
|
|
423
|
+
# Only request 1 bar for efficiency (matching pandas implementation)
|
|
424
|
+
timeshift = None if timestep == "day" else timedelta(days=-1)
|
|
425
|
+
length = 1
|
|
426
|
+
|
|
427
|
+
bars_data = self._pull_source_symbol_bars(
|
|
428
|
+
asset, length, timestep=timestep, timeshift=timeshift, quote=quote
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
if bars_data is None or len(bars_data) == 0:
|
|
432
|
+
logger.warning(f"No bars data for {asset.symbol} at {current_datetime}")
|
|
433
|
+
self._cache_last_price_polars(asset, None, current_datetime, timestep)
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
# Direct column access - since we only request 1 bar, take the first (and only) element
|
|
437
|
+
open_price = bars_data["open"][0]
|
|
438
|
+
|
|
439
|
+
# Convert if needed
|
|
440
|
+
if isinstance(open_price, (np.int64, np.integer)):
|
|
441
|
+
open_price = Decimal(int(open_price))
|
|
442
|
+
elif isinstance(open_price, (np.float64, np.floating)):
|
|
443
|
+
open_price = float(open_price)
|
|
444
|
+
|
|
445
|
+
# Use mixin's cache method
|
|
446
|
+
self._cache_last_price_polars(asset, open_price, current_datetime, timestep)
|
|
447
|
+
return open_price
|
|
448
|
+
|
|
449
|
+
def get_historical_prices(
|
|
450
|
+
self,
|
|
451
|
+
asset: Asset,
|
|
452
|
+
length: int,
|
|
453
|
+
timestep: str = None,
|
|
454
|
+
timeshift: Optional[timedelta] = None,
|
|
455
|
+
quote: Optional[Asset] = None,
|
|
456
|
+
exchange: Optional[str] = None,
|
|
457
|
+
include_after_hours: bool = False,
|
|
458
|
+
return_polars: bool = False,
|
|
459
|
+
) -> Optional[Bars]:
|
|
460
|
+
"""Get historical prices using polars."""
|
|
461
|
+
logger.debug(f"get_historical_prices called for {asset.symbol}")
|
|
462
|
+
if timestep is None:
|
|
463
|
+
timestep = self.get_timestep()
|
|
464
|
+
|
|
465
|
+
# Get bars data
|
|
466
|
+
bars_data = self._pull_source_symbol_bars(
|
|
467
|
+
asset,
|
|
468
|
+
length,
|
|
469
|
+
timestep=timestep,
|
|
470
|
+
timeshift=timeshift,
|
|
471
|
+
quote=quote,
|
|
472
|
+
include_after_hours=include_after_hours
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if bars_data is None:
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
# Create and return Bars object
|
|
479
|
+
return self._parse_source_symbol_bars(
|
|
480
|
+
bars_data, asset, quote=quote, length=length, return_polars=return_polars
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
def get_chains(self, asset: Asset, quote: Asset = None, exchange: str = None):
|
|
484
|
+
"""Get option chains - not implemented for DataBento."""
|
|
485
|
+
logger.warning("get_chains is not implemented for DataBentoData")
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
def get_quote(self, asset: Asset) -> None:
|
|
489
|
+
"""Get quote - not implemented for DataBento backtesting."""
|
|
490
|
+
return None
|
|
@@ -28,10 +28,10 @@ from lumibot.tools.lumibot_logger import get_logger
|
|
|
28
28
|
logger = get_logger(__name__)
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
class
|
|
31
|
+
class DataBentoDataPolarsLive(PolarsMixin, DataSource):
|
|
32
32
|
"""
|
|
33
33
|
DataBento data source optimized with Polars and proper Live API usage.
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
Uses Live API for real-time trade streaming to achieve <1 minute lag.
|
|
36
36
|
Falls back to Historical API for older data.
|
|
37
37
|
"""
|
|
@@ -53,7 +53,8 @@ lumibot/data_sources/ccxt_data.py
|
|
|
53
53
|
lumibot/data_sources/data_source.py
|
|
54
54
|
lumibot/data_sources/data_source_backtesting.py
|
|
55
55
|
lumibot/data_sources/databento_data.py
|
|
56
|
-
lumibot/data_sources/
|
|
56
|
+
lumibot/data_sources/databento_data_polars_backtesting.py
|
|
57
|
+
lumibot/data_sources/databento_data_polars_live.py
|
|
57
58
|
lumibot/data_sources/example_broker_data.py
|
|
58
59
|
lumibot/data_sources/exceptions.py
|
|
59
60
|
lumibot/data_sources/interactive_brokers_data.py
|
|
@@ -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.0.
|
|
8
|
+
version="4.0.23",
|
|
9
9
|
author="Robert Grzesik",
|
|
10
10
|
author_email="rob@lumiwealth.com",
|
|
11
11
|
description="Backtesting and Trading Library, Made by Lumiwealth",
|
|
@@ -3,6 +3,7 @@ import pytest
|
|
|
3
3
|
import pytz
|
|
4
4
|
|
|
5
5
|
from lumibot.backtesting import BacktestingBroker, DataBentoDataBacktesting
|
|
6
|
+
from lumibot.data_sources import DataBentoDataBacktesting as DataBentoDataBacktestingPolars
|
|
6
7
|
from lumibot.entities import Asset
|
|
7
8
|
from lumibot.strategies import Strategy
|
|
8
9
|
from lumibot.traders import Trader
|
|
@@ -97,6 +98,58 @@ class TestDatabentoBacktestFull:
|
|
|
97
98
|
for price in strat_obj.prices:
|
|
98
99
|
assert price is not None and price > 0, f"Expected valid price, got {price}"
|
|
99
100
|
|
|
101
|
+
@pytest.mark.apitest
|
|
102
|
+
@pytest.mark.skipif(
|
|
103
|
+
not DATABENTO_API_KEY,
|
|
104
|
+
reason="This test requires a Databento API key"
|
|
105
|
+
)
|
|
106
|
+
@pytest.mark.skipif(
|
|
107
|
+
DATABENTO_API_KEY == '<your key here>',
|
|
108
|
+
reason="This test requires a Databento API key"
|
|
109
|
+
)
|
|
110
|
+
def test_databento_continuous_futures_minute_data_polars(self):
|
|
111
|
+
"""
|
|
112
|
+
Test Databento with Polars implementation - minute-level data.
|
|
113
|
+
Should be significantly faster than pandas version.
|
|
114
|
+
"""
|
|
115
|
+
# Use timezone-aware datetimes for futures trading
|
|
116
|
+
tzinfo = pytz.timezone("America/New_York")
|
|
117
|
+
backtesting_start = tzinfo.localize(datetime.datetime(2025, 1, 2, 9, 30))
|
|
118
|
+
backtesting_end = tzinfo.localize(datetime.datetime(2025, 1, 3, 16, 0))
|
|
119
|
+
|
|
120
|
+
data_source = DataBentoDataBacktestingPolars(
|
|
121
|
+
datetime_start=backtesting_start,
|
|
122
|
+
datetime_end=backtesting_end,
|
|
123
|
+
api_key=DATABENTO_API_KEY,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
broker = BacktestingBroker(data_source=data_source)
|
|
127
|
+
|
|
128
|
+
strat_obj = SimpleContinuousFutures(
|
|
129
|
+
broker=broker,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
trader = Trader(logfile="", backtest=True)
|
|
133
|
+
trader.add_strategy(strat_obj)
|
|
134
|
+
results = trader.run_all(
|
|
135
|
+
show_plot=False,
|
|
136
|
+
show_tearsheet=False,
|
|
137
|
+
show_indicators=False,
|
|
138
|
+
save_tearsheet=False
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Verify results
|
|
142
|
+
assert results is not None
|
|
143
|
+
assert len(strat_obj.prices) > 0, "Expected to collect some prices"
|
|
144
|
+
assert len(strat_obj.times) > 0, "Expected to collect some timestamps"
|
|
145
|
+
|
|
146
|
+
# Verify minute-level cadence
|
|
147
|
+
assert len(strat_obj.prices) > 100, f"Expected many minute-level data points, got {len(strat_obj.prices)}"
|
|
148
|
+
|
|
149
|
+
# Verify all prices are valid numbers
|
|
150
|
+
for price in strat_obj.prices:
|
|
151
|
+
assert price is not None and price > 0, f"Expected valid price, got {price}"
|
|
152
|
+
|
|
100
153
|
@pytest.mark.apitest
|
|
101
154
|
@pytest.mark.skipif(
|
|
102
155
|
not DATABENTO_API_KEY,
|
|
@@ -111,8 +164,9 @@ class TestDatabentoBacktestFull:
|
|
|
111
164
|
Test Databento with continuous futures using daily data over a longer period.
|
|
112
165
|
This is similar to the profiling test but as a permanent test.
|
|
113
166
|
"""
|
|
114
|
-
|
|
115
|
-
|
|
167
|
+
tzinfo = pytz.timezone("America/New_York")
|
|
168
|
+
backtesting_start = tzinfo.localize(datetime.datetime(2025, 1, 2))
|
|
169
|
+
backtesting_end = tzinfo.localize(datetime.datetime(2025, 3, 31))
|
|
116
170
|
|
|
117
171
|
# Simple daily strategy
|
|
118
172
|
class DailyContinuousFutures(Strategy):
|