pfund 0.0.1.dev10__tar.gz → 0.0.1.dev12__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.
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/PKG-INFO +5 -1
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/CONTRIBUTING.md +3 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/adapter.py +4 -7
- pfund-0.0.1.dev12/pfund/analyzer.py +203 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/brokers/ib/broker_ib.py +4 -2
- pfund-0.0.1.dev12/pfund/config/binance/linear/config.yml +7 -0
- pfund-0.0.1.dev12/pfund/config/binance/linear/lot_sizes_linear.yml +2 -0
- pfund-0.0.1.dev12/pfund/config/binance/linear/pdt_matchings_linear.yml +2 -0
- pfund-0.0.1.dev12/pfund/config/binance/linear/tick_sizes_linear.yml +2 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/configuration.py +5 -7
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config_handler.py +4 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/const/commons.py +3 -1
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/const/paths.py +1 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/data_tools/data_tool_pandas.py +2 -6
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/engines/backtest_engine.py +50 -33
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/engines/trade_engine.py +7 -10
- pfund-0.0.1.dev12/pfund/exchanges/binance/__init__.py +3 -0
- pfund-0.0.1.dev12/pfund/exchanges/binance/exchange.py +37 -0
- pfund-0.0.1.dev12/pfund/exchanges/binance/linear/exchange.py +6 -0
- pfund-0.0.1.dev12/pfund/exchanges/binance/rest_api.py +15 -0
- pfund-0.0.1.dev12/pfund/exchanges/binance/ws_api.py +33 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/bybit/__init__.py +1 -2
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/bybit/exchange.py +10 -6
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/bybit/rest_api.py +3 -1
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/bybit/ws_api.py +4 -2
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/exchange_base.py +12 -9
- pfund-0.0.1.dev12/pfund/git_controller.py +60 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/managers/strategy_manager.py +1 -1
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/models/model_base.py +39 -1
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/strategies/strategy_base.py +39 -1
- pfund-0.0.1.dev12/pfund/templates/dashboards/pfund-overview.streamlit.py +0 -0
- pfund-0.0.1.dev12/pfund/templates/notebooks/pfund-analytics.ipynb +49 -0
- pfund-0.0.1.dev12/pfund/templates/notebooks/pfund-overview.ipynb +55 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/types/common_literals.py +3 -1
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/utils/utils.py +23 -3
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pyproject.toml +19 -8
- pfund-0.0.1.dev10/pfund/analyzer.py +0 -3
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/LICENSE +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/README.md +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/accounts/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/accounts/account_base.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/accounts/account_crypto.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/accounts/account_ib.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/balances/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/balances/balance_base.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/balances/balance_crypto.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/balances/balance_ib.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/brokers/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/brokers/broker_backtest.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/brokers/broker_base.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/brokers/broker_crypto.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/brokers/broker_live.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/brokers/ib/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/brokers/ib/ib_api.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/brokers/ib/ib_client.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/brokers/ib/ib_wrapper.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/cli/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/cli/commands/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/cli/commands/config.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/cli/commands/docker_compose.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/cli/main.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/bybit/config.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/bybit/lot_sizes_inverse.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/bybit/lot_sizes_linear.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/bybit/lot_sizes_option.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/bybit/lot_sizes_spot.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/bybit/pdt_matchings_inverse.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/bybit/pdt_matchings_linear.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/bybit/pdt_matchings_option.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/bybit/pdt_matchings_spot.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/bybit/tick_sizes_inverse.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/bybit/tick_sizes_linear.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/bybit/tick_sizes_option.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/bybit/tick_sizes_spot.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/ib/config.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/config/logging.yml +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/const/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/const/_zmq_routes.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/data_tools/data_tool_base.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/datas/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/datas/data_bar.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/datas/data_base.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/datas/data_quote.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/datas/data_tick.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/datas/data_time_based.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/datas/resolution.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/datas/timeframe.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/engines/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/engines/base_engine.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/engines/test_engine.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/engines/train_engine.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/errors.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/bybit/rest_api_samples/get_markets_result_inverse +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/bybit/rest_api_samples/get_markets_result_linear +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/bybit/rest_api_samples/get_markets_result_option +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/bybit/rest_api_samples/get_markets_result_spot +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/bybit/rest_api_samples/get_markets_return_inverse +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/bybit/rest_api_samples/get_markets_return_linear +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/bybit/rest_api_samples/get_markets_return_option +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/bybit/rest_api_samples/get_markets_return_spot +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/rest_api_base.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/exchanges/ws_api_base.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/account_summary_tags.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/client.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/comm.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/commission_report.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/common.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/connection.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/contract.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/decoder.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/enum_implem.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/errors.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/execution.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/ibapi.pyproj +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/message.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/news.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/object_implem.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/order.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/order_condition.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/order_state.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/orderdecoder.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/reader.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/scanner.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/server_versions.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/softdollartier.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/tag_value.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/ticktype.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/utils.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/externals/ibapi/wrapper.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/indicators/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/indicators/indicator_base.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/indicators/ta_indicator.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/indicators/talib_indicator.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/investment_profile.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/main.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/managers/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/managers/base_manager.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/managers/connection_manager.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/managers/data_manager.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/managers/order_manager.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/managers/portfolio_manager.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/managers/risk_manager.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/mixins/backtest.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/models/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/models/model_backtest.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/models/model_meta.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/models/pytorch_model.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/models/sklearn_model.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/orders/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/orders/order_base.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/orders/order_crypto.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/orders/order_ib.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/orders/order_statuses.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/orders/order_time_in_force.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/plogging/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/plogging/config.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/plogging/filters.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/plogging/formatter.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/plogging/handlers.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/portfolio.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/positions/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/positions/position_base.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/positions/position_crypto.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/positions/position_ib.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/products/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/products/product_base.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/products/product_crypto.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/products/product_ib.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/risk_monitor.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/strategies/__init__.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/strategies/allocation_strategy.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/strategies/diversification_strategy.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/strategies/hedging_strategy.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/strategies/optimization_strategy.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/strategies/portfolio_strategy.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/strategies/rebalancing_strategy.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/strategies/strategy_backtest.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/strategies/strategy_meta.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/types/backtest.py +0 -0
- /pfund-0.0.1.dev10/pfund/exchanges/bybit/types.py → /pfund-0.0.1.dev12/pfund/types/bybit.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/types/core.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/universe.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/utils/aliases.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/utils/envs.py +0 -0
- {pfund-0.0.1.dev10 → pfund-0.0.1.dev12}/pfund/zeromq.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pfund
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev12
|
|
4
4
|
Summary: A Complete Algo-Trading Framework for Machine Learning, enabling trading across TradFi, CeFi and DeFi. Supports Vectorized and Event-Driven Backtesting, Paper and Live Trading
|
|
5
5
|
Home-page: https://pfund.ai
|
|
6
6
|
License: Apache-2.0
|
|
@@ -13,13 +13,16 @@ Classifier: Programming Language :: Python :: 3
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Provides-Extra: analytics
|
|
16
17
|
Provides-Extra: data
|
|
17
18
|
Provides-Extra: ml
|
|
18
19
|
Requires-Dist: click (>=8.1.7,<9.0.0)
|
|
19
20
|
Requires-Dist: gitpython (>=3.1.43,<4.0.0)
|
|
20
21
|
Requires-Dist: mlflow (>=2.11.3,<3.0.0) ; extra == "ml"
|
|
21
22
|
Requires-Dist: orjson (>=3.9.14,<4.0.0) ; extra == "data"
|
|
23
|
+
Requires-Dist: papermill (>=2.5.0,<3.0.0) ; extra == "analytics"
|
|
22
24
|
Requires-Dist: pfeed[boost,data,df] (>=0.0.1.dev10,<0.0.2) ; extra == "data"
|
|
25
|
+
Requires-Dist: pfolio[bayesian,data,portfolio,temporary] (>=0.0.1.dev4,<0.0.2) ; extra == "analytics"
|
|
23
26
|
Requires-Dist: platformdirs (>=4.2.0,<5.0.0)
|
|
24
27
|
Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
|
|
25
28
|
Requires-Dist: python-telegram-bot (>=20.7,<21.0)
|
|
@@ -32,6 +35,7 @@ Requires-Dist: scikit-learn (>=1.4.0,<2.0.0) ; extra == "ml"
|
|
|
32
35
|
Requires-Dist: ta (>=0.11.0,<0.12.0) ; extra == "ml"
|
|
33
36
|
Requires-Dist: torch (>=2.1.2,<3.0.0) ; extra == "ml"
|
|
34
37
|
Requires-Dist: tqdm (>=4.66.2,<5.0.0)
|
|
38
|
+
Requires-Dist: voila (>=0.5.6,<0.6.0) ; extra == "analytics"
|
|
35
39
|
Requires-Dist: websocket-client (>=1.7.0,<2.0.0)
|
|
36
40
|
Project-URL: Documentation, https://pfund-docs.pfund.ai
|
|
37
41
|
Project-URL: Repository, https://github.com/PFund-Software-Ltd/pfund
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import yaml
|
|
2
2
|
import os
|
|
3
3
|
|
|
4
|
-
from pfund.const.paths import PROJ_CONFIG_PATH
|
|
5
|
-
|
|
6
4
|
|
|
7
5
|
class Adapter:
|
|
8
|
-
def __init__(self,
|
|
9
|
-
self.
|
|
6
|
+
def __init__(self, config_path, adapter_dict):
|
|
7
|
+
self.config_path = config_path
|
|
10
8
|
self._adapter_dict = adapter_dict
|
|
11
9
|
self._adapter = {}
|
|
12
10
|
self._ref_keys = []
|
|
@@ -25,9 +23,8 @@ class Adapter:
|
|
|
25
23
|
return pdt
|
|
26
24
|
|
|
27
25
|
def load_pdt_matchings(self):
|
|
28
|
-
file_path = f'{PROJ_CONFIG_PATH}/{self._trading_venue.lower()}'
|
|
29
26
|
config_name = 'pdt_matchings'
|
|
30
|
-
for file_name in os.listdir(
|
|
27
|
+
for file_name in os.listdir(self.config_path):
|
|
31
28
|
if not file_name.startswith(config_name):
|
|
32
29
|
continue
|
|
33
30
|
file_splits = file_name.split('_')
|
|
@@ -35,7 +32,7 @@ class Adapter:
|
|
|
35
32
|
category = file_splits[-1].split('.')[0]
|
|
36
33
|
else:
|
|
37
34
|
category = ''
|
|
38
|
-
with open(
|
|
35
|
+
with open(self.config_path + '/' + file_name, 'r') as f:
|
|
39
36
|
if pdt_macthings := yaml.safe_load(f):
|
|
40
37
|
for pdt, epdt in pdt_macthings.items():
|
|
41
38
|
self.update(pdt, epdt, ref_key=category)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from pfund.types.common_literals import tSUPPORTED_CODE_EDITORS, tSUPPORTED_TEMPLATE_TYPES
|
|
9
|
+
|
|
10
|
+
from pfund.utils import utils
|
|
11
|
+
from pfund.config_handler import ConfigHandler
|
|
12
|
+
from pfund.const.paths import PROJ_PATH
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Analyzer:
|
|
16
|
+
try:
|
|
17
|
+
Engine = utils.get_engine_class()
|
|
18
|
+
config = Engine.config
|
|
19
|
+
except:
|
|
20
|
+
config = ConfigHandler.load_config()
|
|
21
|
+
|
|
22
|
+
notebook_path = Path(config.notebook_path)
|
|
23
|
+
spreadsheet_path = Path(config.spreadsheet_path)
|
|
24
|
+
dashboard_path = Path(config.dashboard_path)
|
|
25
|
+
|
|
26
|
+
def __init__(self, data: dict | None=None):
|
|
27
|
+
self.data = data or {}
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def _is_file(template: str) -> bool:
|
|
31
|
+
if '\\' in template or '/' in template:
|
|
32
|
+
assert Path(template).resolve().is_file(), f"File {template} does not exist"
|
|
33
|
+
return True
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def _derive_template_type(template: str) -> tSUPPORTED_TEMPLATE_TYPES:
|
|
38
|
+
if '.ipynb' in template:
|
|
39
|
+
template_type = 'notebook'
|
|
40
|
+
elif '.grid' in template:
|
|
41
|
+
template_type = 'spreadsheet'
|
|
42
|
+
elif '.py' in template:
|
|
43
|
+
template_type = 'dashboard'
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError(f"Template {template} is not a valid template, only .ipynb, .grid, .py are supported.")
|
|
46
|
+
return template_type
|
|
47
|
+
|
|
48
|
+
def _find_template(self, template: str) -> str:
|
|
49
|
+
'''Check if the template exists in pfund's templates or user's templates
|
|
50
|
+
e.g. template = 'notebook.ipynb' or 'spreadsheet.grid' or 'dashboard.py'
|
|
51
|
+
'''
|
|
52
|
+
template_type = self._derive_template_type(template)
|
|
53
|
+
pfund_templates_dir = PROJ_PATH / 'templates' / (template_type+'s')
|
|
54
|
+
user_templates_dir = getattr(self, f'{template_type}_path')
|
|
55
|
+
for templates_dir in [pfund_templates_dir, user_templates_dir]:
|
|
56
|
+
for file_name in os.listdir(templates_dir):
|
|
57
|
+
if template == file_name:
|
|
58
|
+
template_file_path = templates_dir / template
|
|
59
|
+
return str(template_file_path)
|
|
60
|
+
else:
|
|
61
|
+
raise FileNotFoundError(f"Template {template} not found in pfund's templates or user's templates")
|
|
62
|
+
|
|
63
|
+
def _get_editor_cmd(self, editor: tSUPPORTED_CODE_EDITORS) -> str:
|
|
64
|
+
if editor == 'vscode':
|
|
65
|
+
cmd = 'code'
|
|
66
|
+
if utils.is_command_available(cmd):
|
|
67
|
+
return cmd
|
|
68
|
+
else:
|
|
69
|
+
print("VSCode command 'code' is not available, cannot open the output notebook")
|
|
70
|
+
elif editor == 'pycharm':
|
|
71
|
+
for cmd in ['charm', 'pycharm']:
|
|
72
|
+
if utils.is_command_available(cmd):
|
|
73
|
+
return cmd
|
|
74
|
+
else:
|
|
75
|
+
print("PyCharm commands 'charm'/'pycharm' are both not available, cannot open the output notebook")
|
|
76
|
+
else:
|
|
77
|
+
print(f"Editor '{editor}' is not supported, cannot open the output notebook")
|
|
78
|
+
|
|
79
|
+
def run_notebooks(
|
|
80
|
+
self,
|
|
81
|
+
notebooks: list[str] | str,
|
|
82
|
+
*voila_args,
|
|
83
|
+
data: dict | None=None,
|
|
84
|
+
display: bool=True,
|
|
85
|
+
port: int=8866,
|
|
86
|
+
show_results_only: bool=True,
|
|
87
|
+
open_outputs: bool=False,
|
|
88
|
+
outputs_path: str | None=None,
|
|
89
|
+
editor: tSUPPORTED_CODE_EDITORS='vscode'
|
|
90
|
+
) -> None:
|
|
91
|
+
'''
|
|
92
|
+
Args:
|
|
93
|
+
notebook:
|
|
94
|
+
- notebook_template's name
|
|
95
|
+
- notebook's full path in str or Path
|
|
96
|
+
voila_args: additional arguments to pass to voila
|
|
97
|
+
data: data to be analyzed, if None, use the data passed to the Analyzer instance during initialization
|
|
98
|
+
display: if True, display the notebook in voila
|
|
99
|
+
show_results_only: if True, display only the results (no source code) in voila
|
|
100
|
+
open_outputs: if True, open the output notebook in the editor
|
|
101
|
+
outputs_path: path to save the output notebooks, if None, do not save the output notebooks
|
|
102
|
+
'''
|
|
103
|
+
import subprocess
|
|
104
|
+
import papermill as pm
|
|
105
|
+
|
|
106
|
+
def _find_available_port(_port):
|
|
107
|
+
retry_num = 100
|
|
108
|
+
while retry_num:
|
|
109
|
+
if not utils.is_port_in_use(_port):
|
|
110
|
+
return _port
|
|
111
|
+
retry_num -= 1
|
|
112
|
+
_port += 1
|
|
113
|
+
else:
|
|
114
|
+
raise Exception(f"No available ports found starting from {_port - 100}, cannot display the notebook")
|
|
115
|
+
|
|
116
|
+
def _assert_voila_args_are_valid():
|
|
117
|
+
for arg in voila_args:
|
|
118
|
+
if not arg.startswith('--'):
|
|
119
|
+
raise ValueError(f"Voila argument '{arg}' should start with '--'")
|
|
120
|
+
if arg.startswith('--port='):
|
|
121
|
+
raise ValueError(f"Voila argument '{arg}' should not be passed in, use the 'port' argument instead")
|
|
122
|
+
if arg.startswith('--strip_sources='):
|
|
123
|
+
raise ValueError(f"Voila argument '{arg}' should not be passed in, use the 'show_results_only' argument instead")
|
|
124
|
+
|
|
125
|
+
voila_processes = []
|
|
126
|
+
nb_output_file_paths = []
|
|
127
|
+
if isinstance(notebooks, str):
|
|
128
|
+
notebooks = [notebooks]
|
|
129
|
+
data = data or self.data
|
|
130
|
+
if not data:
|
|
131
|
+
raise ValueError("No data passed in or stored in the Analyzer instance, please pass in the data to be analyzed.")
|
|
132
|
+
|
|
133
|
+
if open_outputs:
|
|
134
|
+
assert outputs_path is not None, f"{outputs_path=}, cannot open the output notebook without saving it."
|
|
135
|
+
editor_cmd = self._get_editor_cmd(editor)
|
|
136
|
+
|
|
137
|
+
_assert_voila_args_are_valid()
|
|
138
|
+
is_theme_provided = any(arg.startswith('--theme=') for arg in voila_args)
|
|
139
|
+
if not is_theme_provided:
|
|
140
|
+
default_theme = 'dark'
|
|
141
|
+
voila_args = [f'--theme={default_theme}', *voila_args]
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
for notebook in notebooks:
|
|
145
|
+
if self._is_file(notebook):
|
|
146
|
+
nb_input_file_path: str = notebook
|
|
147
|
+
notebook = Path(notebook).name # e.g. 'notebook.ipynb'
|
|
148
|
+
else:
|
|
149
|
+
nb_input_file_path: str = self._find_template(notebook)
|
|
150
|
+
nb_output_file_path = Path(outputs_path or '.').resolve() / f'{notebook.replace(".ipynb", "")}_output.ipynb'
|
|
151
|
+
nb_output_file_path = str(nb_output_file_path)
|
|
152
|
+
nb_output_file_paths.append(nb_output_file_path)
|
|
153
|
+
|
|
154
|
+
print(f"Executing notebook: {notebook}")
|
|
155
|
+
pm.execute_notebook(
|
|
156
|
+
nb_input_file_path,
|
|
157
|
+
nb_output_file_path,
|
|
158
|
+
parameters=data
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if open_outputs:
|
|
162
|
+
# e.g. code notebook_output.ipynb if using vscode
|
|
163
|
+
if editor_cmd:
|
|
164
|
+
subprocess.run([editor_cmd, nb_output_file_path])
|
|
165
|
+
|
|
166
|
+
if display:
|
|
167
|
+
port = _find_available_port(port)
|
|
168
|
+
is_last_notebook = (notebook == notebooks[-1])
|
|
169
|
+
subprocess_func = subprocess.run if is_last_notebook else subprocess.Popen
|
|
170
|
+
process = subprocess_func([
|
|
171
|
+
'voila',
|
|
172
|
+
f'--port={port}',
|
|
173
|
+
f'--strip_sources={show_results_only}',
|
|
174
|
+
*voila_args,
|
|
175
|
+
nb_output_file_path
|
|
176
|
+
])
|
|
177
|
+
voila_processes.append(process)
|
|
178
|
+
except KeyboardInterrupt:
|
|
179
|
+
print("KeyboardInterrupt: Stopping the execution of the notebooks")
|
|
180
|
+
except Exception:
|
|
181
|
+
raise
|
|
182
|
+
finally:
|
|
183
|
+
if outputs_path is None:
|
|
184
|
+
for nb_output_file_path in nb_output_file_paths:
|
|
185
|
+
print(f"{outputs_path=}, removing output notebook: {nb_output_file_path}")
|
|
186
|
+
os.remove(nb_output_file_path)
|
|
187
|
+
for process in voila_processes:
|
|
188
|
+
process.terminate()
|
|
189
|
+
process.wait()
|
|
190
|
+
|
|
191
|
+
# TODO:
|
|
192
|
+
def run_spreadsheets(
|
|
193
|
+
self,
|
|
194
|
+
spreadsheets: list[str] | str
|
|
195
|
+
):
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
# TODO:
|
|
199
|
+
def run_dashboards(
|
|
200
|
+
self,
|
|
201
|
+
dashboards: list[str] | str
|
|
202
|
+
):
|
|
203
|
+
pass
|
|
@@ -5,6 +5,7 @@ from collections import defaultdict
|
|
|
5
5
|
|
|
6
6
|
from pfund.adapter import Adapter
|
|
7
7
|
from pfund.config.configuration import Configuration
|
|
8
|
+
from pfund.const.paths import PROJ_CONFIG_PATH
|
|
8
9
|
from pfund.const.commons import SUPPORTED_PRODUCT_TYPES
|
|
9
10
|
from pfund.products import IBProduct
|
|
10
11
|
from pfund.accounts import IBAccount
|
|
@@ -19,8 +20,9 @@ from pfund.brokers.ib.ib_api import IBApi
|
|
|
19
20
|
class IBBroker(LiveBroker):
|
|
20
21
|
def __init__(self, env, **configs):
|
|
21
22
|
super().__init__(env, 'IB', **configs)
|
|
22
|
-
|
|
23
|
-
self.
|
|
23
|
+
config_path = f'{PROJ_CONFIG_PATH}/{self.bkr.lower()}'
|
|
24
|
+
self.configs = Configuration(config_path, 'config')
|
|
25
|
+
self.adapter = Adapter(config_path, self.configs.load_config_section('adapter'))
|
|
24
26
|
self.account = None
|
|
25
27
|
|
|
26
28
|
# API
|
|
@@ -2,23 +2,21 @@ import os
|
|
|
2
2
|
|
|
3
3
|
import yaml
|
|
4
4
|
|
|
5
|
-
from pfund.const.paths import PROJ_CONFIG_PATH
|
|
6
5
|
from pfund.utils.utils import short_path
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class Configuration:
|
|
10
|
-
def __init__(self,
|
|
11
|
-
self.
|
|
9
|
+
def __init__(self, config_path, config_name):
|
|
10
|
+
self.config_path = config_path
|
|
12
11
|
self.config_name = config_name
|
|
13
|
-
self.config_path = f'{PROJ_CONFIG_PATH}/{self.config_dir}'
|
|
14
12
|
self.configs = None
|
|
15
13
|
self.reload()
|
|
16
14
|
|
|
17
15
|
def reload(self):
|
|
18
16
|
self.configs = self.read_config(self.config_name)
|
|
19
17
|
|
|
20
|
-
def
|
|
21
|
-
return self.
|
|
18
|
+
def get_config_path(self):
|
|
19
|
+
return self.config_path
|
|
22
20
|
|
|
23
21
|
def read_config(self, config_name):
|
|
24
22
|
file_path = f'{self.config_path}/{config_name}.yml'
|
|
@@ -40,7 +38,7 @@ class Configuration:
|
|
|
40
38
|
raise Exception(f'could not find section {section} for config {self.config_name}')
|
|
41
39
|
|
|
42
40
|
def check_if_config_exists_and_not_empty(self, config_name):
|
|
43
|
-
file_path = f'{
|
|
41
|
+
file_path = f'{self.config_path}/{config_name}.yml'
|
|
44
42
|
if os.path.exists(file_path) and os.stat(file_path).st_size != 0:
|
|
45
43
|
return True
|
|
46
44
|
else:
|
|
@@ -98,6 +98,10 @@ class ConfigHandler:
|
|
|
98
98
|
def dashboard_path(self):
|
|
99
99
|
return f'{self.data_path}/templates/dashboards'
|
|
100
100
|
|
|
101
|
+
@property
|
|
102
|
+
def artifact_path(self):
|
|
103
|
+
return f'{self.data_path}/.artifacts'
|
|
104
|
+
|
|
101
105
|
def __post_init__(self):
|
|
102
106
|
self.logging_config = self.logging_config or {}
|
|
103
107
|
|
|
@@ -20,4 +20,6 @@ SUPPORTED_TIMEFRAMES = [
|
|
|
20
20
|
]
|
|
21
21
|
SUPPORTED_DATA_CHANNELS = ['orderbook', 'tradebook', 'kline']
|
|
22
22
|
SUPPORTED_BACKTEST_MODES = ['vectorized', 'event_driven']
|
|
23
|
-
SUPPORTED_DATA_TOOLS = ['pandas']
|
|
23
|
+
SUPPORTED_DATA_TOOLS = ['pandas']
|
|
24
|
+
SUPPORTED_CODE_EDITORS = ['vscode', 'pycharm']
|
|
25
|
+
SUPPORTED_TEMPLATE_TYPES = ['notebook', 'spreadsheet', 'dashboard']
|
|
@@ -151,12 +151,8 @@ class PandasDataTool(BaseDataTool):
|
|
|
151
151
|
index=pd.MultiIndex(levels=[[]]*len(index_names), codes=[[]]*len(index_names), names=index_names)
|
|
152
152
|
)
|
|
153
153
|
|
|
154
|
-
def output_df_to_parquet(self,
|
|
155
|
-
|
|
156
|
-
name = name + '.parquet'
|
|
157
|
-
if type(path) is str:
|
|
158
|
-
path = Path(path)
|
|
159
|
-
df.to_parquet(path / name)
|
|
154
|
+
def output_df_to_parquet(self, df: pd.DataFrame, file_path: str):
|
|
155
|
+
df.to_parquet(file_path)
|
|
160
156
|
|
|
161
157
|
def _create_multi_index(self, index_data: dict, index_names: list[str]) -> pd.MultiIndex:
|
|
162
158
|
return pd.MultiIndex.from_tuples([tuple(index_data[name] for name in index_names)], names=index_names)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
|
+
import inspect
|
|
4
5
|
import os
|
|
5
6
|
import time
|
|
6
7
|
import datetime
|
|
@@ -13,6 +14,7 @@ if TYPE_CHECKING:
|
|
|
13
14
|
from pfund.types.core import tStrategy, tModel, tFeature, tIndicator
|
|
14
15
|
|
|
15
16
|
import pfund as pf
|
|
17
|
+
from pfund.git_controller import GitController
|
|
16
18
|
from pfund.engines.base_engine import BaseEngine
|
|
17
19
|
from pfund.brokers.broker_backtest import BacktestBroker
|
|
18
20
|
from pfund.strategies.strategy_base import BaseStrategy
|
|
@@ -23,7 +25,7 @@ from pfund.mixins.backtest import BacktestMixin
|
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
class BacktestEngine(BaseEngine):
|
|
26
|
-
def __new__(cls, *, env: str='BACKTEST', data_tool: tSUPPORTED_DATA_TOOLS='pandas', mode: tSUPPORTED_BACKTEST_MODES='vectorized', append_to_strategy_df=False, use_prepared_signals=True, config: ConfigHandler | None=None, **settings):
|
|
28
|
+
def __new__(cls, *, env: str='BACKTEST', data_tool: tSUPPORTED_DATA_TOOLS='pandas', mode: tSUPPORTED_BACKTEST_MODES='vectorized', append_to_strategy_df=False, use_prepared_signals=True, config: ConfigHandler | None=None, auto_git_commit=False, **settings):
|
|
27
29
|
if not hasattr(cls, 'mode'):
|
|
28
30
|
cls.mode = mode.lower()
|
|
29
31
|
if not hasattr(cls, 'append_to_strategy_df'):
|
|
@@ -32,13 +34,18 @@ class BacktestEngine(BaseEngine):
|
|
|
32
34
|
# instead of recalculating the signals. This will make event-driven backtesting faster but less consistent with live trading
|
|
33
35
|
if not hasattr(cls, 'use_prepared_signals'):
|
|
34
36
|
cls.use_prepared_signals = use_prepared_signals
|
|
37
|
+
if not hasattr(cls, 'auto_git_commit'):
|
|
38
|
+
cls.auto_git_commit = auto_git_commit
|
|
35
39
|
return super().__new__(cls, env, data_tool=data_tool, config=config, **settings)
|
|
36
40
|
|
|
37
|
-
def __init__(self, *, env: str='BACKTEST', data_tool: tSUPPORTED_DATA_TOOLS='pandas', mode: tSUPPORTED_BACKTEST_MODES='vectorized', append_to_strategy_df=False, use_prepared_signals=True, config: ConfigHandler | None=None, **settings):
|
|
38
|
-
super().__init__(env, data_tool=data_tool)
|
|
41
|
+
def __init__(self, *, env: str='BACKTEST', data_tool: tSUPPORTED_DATA_TOOLS='pandas', mode: tSUPPORTED_BACKTEST_MODES='vectorized', append_to_strategy_df=False, use_prepared_signals=True, config: ConfigHandler | None=None, auto_git_commit=False, **settings):
|
|
39
42
|
# avoid re-initialization to implement singleton class correctly
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
if not hasattr(self, '_initialized'):
|
|
44
|
+
# Get the current frame and then the outer frame (where the engine instance is created)
|
|
45
|
+
caller_frame = inspect.currentframe().f_back
|
|
46
|
+
file_path = caller_frame.f_code.co_filename # Extract the file path from the frame
|
|
47
|
+
self._git = GitController(os.path.abspath(file_path))
|
|
48
|
+
super().__init__(env, data_tool=data_tool)
|
|
42
49
|
|
|
43
50
|
# HACK: since python doesn't support dynamic typing, true return type should be subclass of BacktestMixin and tStrategy
|
|
44
51
|
# write -> BacktestMixin | tStrategy for better intellisense in IDEs
|
|
@@ -88,11 +95,11 @@ class BacktestEngine(BaseEngine):
|
|
|
88
95
|
def _generate_backtest_id() -> str:
|
|
89
96
|
return uuid.uuid4().hex
|
|
90
97
|
|
|
91
|
-
def _create_backtest_name(self, strat: str):
|
|
98
|
+
def _create_backtest_name(self, strat: str, backtest_id: str, backtest_id_length: int=12):
|
|
92
99
|
local_tz = utils.get_local_timezone()
|
|
93
100
|
utcnow = datetime.datetime.now(tz=local_tz).strftime('%Y-%m-%d_%H:%M:%S_UTC%z')
|
|
94
|
-
|
|
95
|
-
return '.'.join([strat, utcnow,
|
|
101
|
+
trimmed_backtest_id = backtest_id[:backtest_id_length]
|
|
102
|
+
return '.'.join([strat, utcnow, trimmed_backtest_id])
|
|
96
103
|
|
|
97
104
|
@staticmethod
|
|
98
105
|
def _generate_backtest_hash(strategy: BaseStrategy):
|
|
@@ -131,60 +138,64 @@ class BacktestEngine(BaseEngine):
|
|
|
131
138
|
except:
|
|
132
139
|
self.logger.exception(f"Error writing to {file_path}:")
|
|
133
140
|
|
|
134
|
-
def _generate_backtest_iteration(self,
|
|
141
|
+
def _generate_backtest_iteration(self, backtest_hash: str) -> int:
|
|
135
142
|
'''Generate backtest iteration number for the same backtest_hash.
|
|
136
143
|
Read the existing backtest.json file to get the iteration number for the same strategy hash
|
|
137
144
|
If the backtest hash is not found, create a new entry with iteration number 1
|
|
138
145
|
else increment the iteration number by 1.
|
|
139
146
|
'''
|
|
140
|
-
backtest_hash = self._generate_backtest_hash(strategy)
|
|
141
147
|
file_name = 'backtest.json'
|
|
142
148
|
backtest_json = self.read_json(file_name)
|
|
143
149
|
backtest_json[backtest_hash] = backtest_json.get(backtest_hash, 0) + 1
|
|
144
150
|
self._write_json(file_name, backtest_json)
|
|
145
151
|
return backtest_json[backtest_hash]
|
|
146
152
|
|
|
147
|
-
def
|
|
148
|
-
|
|
149
|
-
strat
|
|
153
|
+
def _commit_strategy(self, strategy: BaseStrategy) -> str | None:
|
|
154
|
+
engine_name = self.__class__.__name__
|
|
155
|
+
strat = strategy.name
|
|
156
|
+
commit_hash: str | None = self._git.commit(strategy._file_path, f'[PFund] {engine_name}: auto-commit strategy "{strat}"')
|
|
157
|
+
if commit_hash:
|
|
158
|
+
self.logger.debug(f"Strategy {strat} committed. {commit_hash=}")
|
|
159
|
+
else:
|
|
160
|
+
commit_hash = self._git.get_last_n_commit(n=1)[0]
|
|
161
|
+
self.logger.debug(f"Strategy {strat} has no changes to commit, return the last {commit_hash=}")
|
|
162
|
+
return commit_hash
|
|
163
|
+
|
|
164
|
+
def _output_backtest_results(self, strat: str, df, start_time: float, end_time: float, commit_hash: str | None):
|
|
150
165
|
strategy = self.get_strategy(strat)
|
|
166
|
+
backtest_id = self._generate_backtest_id()
|
|
167
|
+
backtest_hash = self._generate_backtest_hash(strategy)
|
|
168
|
+
backtest_name = self._create_backtest_name(strat, backtest_id)
|
|
169
|
+
backtest_iter = self._generate_backtest_iteration(backtest_hash)
|
|
151
170
|
local_tz = utils.get_local_timezone()
|
|
152
171
|
duration = end_time - start_time
|
|
172
|
+
df_file_path = os.path.join(self.config.backtest_path, f'{backtest_name}.parquet')
|
|
153
173
|
backtest_history = {
|
|
154
|
-
'settings': self.settings,
|
|
155
174
|
'metadata': {
|
|
156
175
|
'pfund_version': pf.__version__,
|
|
157
|
-
'backtest_name': backtest_name,
|
|
158
176
|
'backtest_id': backtest_id,
|
|
159
|
-
'
|
|
177
|
+
'backtest_hash': backtest_hash,
|
|
178
|
+
'backtest_name': backtest_name,
|
|
179
|
+
'backtest_iteration': backtest_iter,
|
|
180
|
+
'commit_hash': commit_hash,
|
|
160
181
|
'duration': f'{duration:.2f}s' if duration > 1 else f'{duration*1000:.2f}ms',
|
|
161
182
|
'start_time': datetime.datetime.fromtimestamp(start_time, tz=local_tz).strftime('%Y-%m-%dT%H:%M:%S%z'),
|
|
162
183
|
'end_time': datetime.datetime.fromtimestamp(end_time, tz=local_tz).strftime('%Y-%m-%dT%H:%M:%S%z'),
|
|
184
|
+
'settings': self.settings,
|
|
163
185
|
},
|
|
164
186
|
'strategy': strategy.to_dict(),
|
|
165
|
-
'
|
|
166
|
-
'df_file_path': os.path.join(self.config.backtest_path, f'{backtest_name_trimmed}.parquet'),
|
|
167
|
-
}
|
|
187
|
+
'result': df_file_path
|
|
168
188
|
}
|
|
169
|
-
self.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
splits = backtest_name.split('.')
|
|
173
|
-
backtest_id_len = 12
|
|
174
|
-
splits[-1] = splits[-1][:backtest_id_len]
|
|
175
|
-
return '.'.join(splits)
|
|
176
|
-
|
|
177
|
-
def output_backtest_results(self, strat: str, df, start_time: float, end_time: float):
|
|
178
|
-
backtest_name = self._create_backtest_name(strat)
|
|
179
|
-
backtest_name_trimmed = self.trim_backtest_name(backtest_name)
|
|
180
|
-
self.data_tool.output_df_to_parquet(backtest_name_trimmed, df, self.config.backtest_path)
|
|
181
|
-
self._write_backtest_history(backtest_name, backtest_name_trimmed, start_time, end_time)
|
|
189
|
+
self.data_tool.output_df_to_parquet(df, df_file_path)
|
|
190
|
+
self._write_json(f'{backtest_name}.json', backtest_history)
|
|
191
|
+
return backtest_history
|
|
182
192
|
|
|
183
193
|
def run(self):
|
|
184
194
|
for broker in self.brokers.values():
|
|
185
195
|
broker.start()
|
|
186
196
|
self.strategy_manager.start()
|
|
187
197
|
|
|
198
|
+
backtests = {}
|
|
188
199
|
if self.mode == 'vectorized':
|
|
189
200
|
for strat, strategy in self.strategy_manager.strategies.items():
|
|
190
201
|
# _dummy strategy is only created for model training, do nothing
|
|
@@ -192,11 +203,16 @@ class BacktestEngine(BaseEngine):
|
|
|
192
203
|
continue
|
|
193
204
|
if not hasattr(strategy, 'backtest'):
|
|
194
205
|
raise Exception(f'Strategy {strat} does not have backtest() method, cannot run vectorized backtesting')
|
|
206
|
+
if self.auto_git_commit and self._git.is_git_repo():
|
|
207
|
+
commit_hash = self._commit_strategy(strategy)
|
|
208
|
+
else:
|
|
209
|
+
commit_hash = None
|
|
195
210
|
start_time = time.time()
|
|
196
211
|
strategy.backtest()
|
|
197
212
|
end_time = time.time()
|
|
198
213
|
df = strategy.get_df()
|
|
199
|
-
self.
|
|
214
|
+
backtest_history: dict = self._output_backtest_results(strat, df, start_time, end_time, commit_hash)
|
|
215
|
+
backtests[strat] = backtest_history
|
|
200
216
|
elif self.mode == 'event_driven':
|
|
201
217
|
for strat, strategy in self.strategy_manager.strategies.items():
|
|
202
218
|
if strat == '_dummy':
|
|
@@ -248,6 +264,7 @@ class BacktestEngine(BaseEngine):
|
|
|
248
264
|
else:
|
|
249
265
|
raise NotImplementedError(f'Backtesting mode {self.mode} is not supported')
|
|
250
266
|
self.strategy_manager.stop(reason='finished backtesting')
|
|
267
|
+
return backtests
|
|
251
268
|
|
|
252
269
|
def end(self):
|
|
253
270
|
self.strategy_manager.stop(reason='finished backtesting')
|
|
@@ -22,7 +22,7 @@ import schedule
|
|
|
22
22
|
|
|
23
23
|
from pfund.engines.base_engine import BaseEngine
|
|
24
24
|
from pfund.brokers.broker_base import BaseBroker
|
|
25
|
-
from pfund.utils.utils import flatten_dict
|
|
25
|
+
from pfund.utils.utils import flatten_dict, is_port_in_use
|
|
26
26
|
from pfund.zeromq import ZeroMQ
|
|
27
27
|
from pfund.config_handler import ConfigHandler
|
|
28
28
|
|
|
@@ -57,15 +57,12 @@ class TradeEngine(BaseEngine):
|
|
|
57
57
|
def _assign_zmq_ports(self) -> dict:
|
|
58
58
|
_assigned_ports = []
|
|
59
59
|
def _is_port_available(_port):
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
else:
|
|
67
|
-
_assigned_ports.append(_port)
|
|
68
|
-
return True
|
|
60
|
+
_is_port_assigned = (_port in _assigned_ports)
|
|
61
|
+
if is_port_in_use(_port) or _is_port_assigned:
|
|
62
|
+
return False
|
|
63
|
+
else:
|
|
64
|
+
_assigned_ports.append(_port)
|
|
65
|
+
return True
|
|
69
66
|
def _get_port(start_port=None):
|
|
70
67
|
_port = start_port or self._zmq_port
|
|
71
68
|
if _is_port_available(_port):
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pfund.exchanges.exchange_base import BaseExchange
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Exchange(BaseExchange):
|
|
7
|
+
SUPPORTED_CATEGORIES = ['linear', 'inverse', 'spot', 'option']
|
|
8
|
+
PTYPE_TO_CATEGORY = {
|
|
9
|
+
'PERP': 'linear',
|
|
10
|
+
'FUT': 'linear',
|
|
11
|
+
'IPERP': 'inverse',
|
|
12
|
+
'IFUT': 'inverse',
|
|
13
|
+
'SPOT': 'spot',
|
|
14
|
+
'OPT': 'option',
|
|
15
|
+
}
|
|
16
|
+
def __new__(cls, env: str, ptype: str):
|
|
17
|
+
from pfund.exchanges.binance.linear.exchange import ExchangeLinear
|
|
18
|
+
|
|
19
|
+
ptype = ptype.upper()
|
|
20
|
+
category = cls.PTYPE_TO_CATEGORY[ptype]
|
|
21
|
+
|
|
22
|
+
if category == 'linear':
|
|
23
|
+
instance = super().__new__(ExchangeLinear)
|
|
24
|
+
instance.category = category
|
|
25
|
+
return instance
|
|
26
|
+
# EXTEND: Add other categories
|
|
27
|
+
else:
|
|
28
|
+
raise ValueError(f"Invalid {category=}")
|
|
29
|
+
|
|
30
|
+
def __init__(self, env: str, ptype: str):
|
|
31
|
+
exch = Path(__file__).parent.name
|
|
32
|
+
super().__init__(env, exch)
|
|
33
|
+
|
|
34
|
+
# FIXME: temporarily override the method, remove it later
|
|
35
|
+
def _setup_configs(self):
|
|
36
|
+
pass
|
|
37
|
+
|