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