Qubx 4.0.0.dev1__tar.gz → 4.1.0__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.
- {qubx-4.0.0.dev1 → qubx-4.1.0}/PKG-INFO +1 -1
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/_version.py +2 -2
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/runner.py +2 -1
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/simulator.py +18 -3
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/utils.py +35 -9
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/connector.py +278 -20
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/binance/connector.py +157 -2
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +8 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/factory.py +7 -1
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/plugin.py +6 -1
- qubx-4.1.0/src/qubx/connectors/ccxt/replace.py +111 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/plugin.py +10 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/builtin.py +22 -19
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/diffs.py +6 -3
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/manager.py +86 -34
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/reconciler.py +2 -2
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/reducer.py +9 -2
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/state.py +41 -9
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/basics.py +60 -22
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/context.py +26 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/fit_context.py +3 -1
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/interfaces.py +15 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/loggers.py +1 -1
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/lookups.py +223 -205
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/instrument_service.py +1 -1
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/market.py +3 -1
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/processing.py +16 -7
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/subscription.py +27 -18
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/trading.py +15 -4
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/subscription_watchdog.py +31 -13
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/multi.py +70 -41
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/questdb.py +47 -10
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/trackers/rebalancers.py +1 -1
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/_jupyter_runner.pyt +2 -2
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/configs.py +11 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/runner.py +78 -14
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/init_code.py +2 -2
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/time.py +3 -3
- {qubx-4.0.0.dev1 → qubx-4.1.0}/.gitignore +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/LICENSE +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/README.md +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/pyproject.toml +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/scripts/build.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/_nb_magic.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/connector.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/data.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/iteratedstream.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/management.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/ome.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/optimization.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/backtester/transfers.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/commands.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/deploy.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/misc.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/release.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/resolver.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/s3.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/theme.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/tui.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/cli/user_config.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/config.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/_two_stream.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/bitfinex/connector.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/bybit/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/bybit/bybit.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/bybit/connector.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/okx/connector.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/rate_limits.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/registry.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/tardis/plugin.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/decorator.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/executor.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/interfaces.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/server.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/control/types.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/config.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/account_manager/state_machine.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/boot.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/connector.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/detectors/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/detectors/delisting.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/detectors/stale.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/errors.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/events.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/exceptions.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/fit_executor.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/helpers.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/initializer.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/instrument_service.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/metrics.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/series.pxd +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/series.pyi +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/series.pyx +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/state_snapshot.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/status.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/utils.pyi +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/core/utils.pyx +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/cache.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/containers.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/guards.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/registry.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storage.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/_missing_extra.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/ccxt.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/csv.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/dukascopy.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/handy.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/iceberg.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/stub.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/utils.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/storages/yahoo.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/data/transformers.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/base.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/composite.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/csv.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/indicator.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/emitters/questdb.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/composite.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/exporters/slack.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/gathering/simplest.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/health/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/health/base.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/health/dummy.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/health/status.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/loggers/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/loggers/csv.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/loggers/factory.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/loggers/postgres.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/notifications/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/notifications/composite.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/notifications/slack.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/notifications/throttler.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/pandaz/stats.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/pandaz/ta.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/pandaz/utils.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/plugins/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/plugins/loader.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/backend.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/config.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/engine.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/ip_resolver.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/manager.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/pools.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/redis_backend.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/rate_limiting/resilient.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/_build.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restarts/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/balance.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/factory.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/position.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/signal.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/state.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/restorers/utils.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/state/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/state/dummy.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/state/redis.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/state/runtime_info.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/state/safe.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/ta/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/base.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/project/template.yml +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.gitignore.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.python-version +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.vscode/launch.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.vscode/settings.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/README.md.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/configs/.gitkeep +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/justfile.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/research/.gitkeep +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/template.yml +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/trackers/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/trackers/advanced.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/trackers/composite.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/trackers/sizers.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/clock.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/collections.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/hft/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/hft/numba_utils.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/hft/orderbook.pyi +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/hft/orderbook.pyx +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/misc.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/nonce.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/orderbook.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/questdb.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/rate_limiter.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/results.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/ringbuffer.pxd +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/ringbuffer.pyi +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/ringbuffer.pyx +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/app.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/handlers.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/styles.tcss +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/log_filter.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/s3.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/slack.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/threading.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/throttler.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/src/qubx/utils/websocket_manager.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
- {qubx-4.0.0.dev1 → qubx-4.1.0}/tests/strategies/obi_trader/src/obi_trader/models/obi_trader.py +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '4.
|
|
22
|
-
__version_tuple__ = version_tuple = (4,
|
|
21
|
+
__version__ = version = '4.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (4, 1, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -612,9 +612,10 @@ class SimulationRunner:
|
|
|
612
612
|
connectors=self._connectors,
|
|
613
613
|
base_currencies=self.setup.base_currencies,
|
|
614
614
|
time=time_provider,
|
|
615
|
-
cfg=AccountManagerConfig(),
|
|
615
|
+
cfg=AccountManagerConfig(maint_margin_rate=self.setup.maint_margin_rate),
|
|
616
616
|
account_id=self.account_id,
|
|
617
617
|
tcc=_exchange_to_tcc,
|
|
618
|
+
account_markets=self.setup.account_markets,
|
|
618
619
|
)
|
|
619
620
|
|
|
620
621
|
# - seed initial capital per exchange into the account state
|
|
@@ -5,8 +5,9 @@ from joblib import delayed
|
|
|
5
5
|
|
|
6
6
|
from qubx import QubxLogConfig, file_formatter, logger
|
|
7
7
|
from qubx.backtester.utils import SetupTypes
|
|
8
|
-
from qubx.core.basics import Instrument
|
|
8
|
+
from qubx.core.basics import DEFAULT_MAINTENANCE_MARGIN, Instrument
|
|
9
9
|
from qubx.core.exceptions import SimulationError
|
|
10
|
+
from qubx.core.lookups import lookup
|
|
10
11
|
from qubx.core.metrics import TradingSessionResult
|
|
11
12
|
from qubx.data.storage import IStorage
|
|
12
13
|
from qubx.emitters.inmemory import InMemoryMetricEmitter
|
|
@@ -45,6 +46,7 @@ def simulate(
|
|
|
45
46
|
accurate_stop_orders_execution: bool = False,
|
|
46
47
|
signal_timeframe: str = "1Min",
|
|
47
48
|
enable_funding: bool = False,
|
|
49
|
+
maint_margin_rate: float = DEFAULT_MAINTENANCE_MARGIN,
|
|
48
50
|
debug: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "WARNING",
|
|
49
51
|
show_latency_report: bool = False,
|
|
50
52
|
portfolio_log_freq: str = "5Min",
|
|
@@ -55,6 +57,7 @@ def simulate(
|
|
|
55
57
|
prefetch_config: PrefetchConfig | None = None,
|
|
56
58
|
trading_sessions_time: str | dict[str, str | tuple[int, int] | tuple[str, str]] = "DEFAULT",
|
|
57
59
|
log_file: str | None = None,
|
|
60
|
+
accounts: dict[str, list[str]] | None = None,
|
|
58
61
|
) -> list[TradingSessionResult]:
|
|
59
62
|
"""
|
|
60
63
|
Backtest utility for trading strategies or signals using historical data.
|
|
@@ -76,6 +79,7 @@ def simulate(
|
|
|
76
79
|
- accurate_stop_orders_execution (bool): If True, enables more accurate stop order execution simulation.
|
|
77
80
|
- signal_timeframe (str): Timeframe for signals, default is "1Min".
|
|
78
81
|
- enable_funding (bool): If True, enables funding rate simulation, default is False.
|
|
82
|
+
- maint_margin_rate (float): Maintenance margin as a fraction of notional for simulated positions (the account's `maint_margin_rate`).
|
|
79
83
|
- debug (Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None): Logging level for debugging.
|
|
80
84
|
- show_latency_report: If True, shows simulator's latency report.
|
|
81
85
|
- portfolio_log_freq (str): Frequency for portfolio logging, default is "5Min".
|
|
@@ -86,6 +90,10 @@ def simulate(
|
|
|
86
90
|
- prefetch_config (dict[str, Any] | None): Configuration for prefetching auxiliary data, default is None.
|
|
87
91
|
- trading_sessions_time (str | dict[str, str | tuple[int, int] | tuple[str, str]]): trading session times (may be "DEFAULT", "STOCKS", "CME" or tuple like ("9:30:00", "15:59:59") or specified for every exchange {"NYSE": "STOKS", ....})
|
|
88
92
|
- log_file (str | None): Optional path to write simulation logs to a file (in addition to stdout). Defaults to None.
|
|
93
|
+
- accounts (dict[str, list[str]] | None): account -> the markets it trades, its own first, e.g.
|
|
94
|
+
{"BINANCE.UM": ["BINANCE.UM", "BINANCE"]} for a portfolio-margin account trading perps and spot: the spot
|
|
95
|
+
market keeps its own simulated exchange (fees, execution) but books into the BINANCE.UM account — one
|
|
96
|
+
capital pool, capital merged into it. Default: one account per exchange.
|
|
89
97
|
Returns:
|
|
90
98
|
- list[TradingSessionResult]: A list of TradingSessionResult objects containing the results of each simulation setup.
|
|
91
99
|
"""
|
|
@@ -125,6 +133,7 @@ def simulate(
|
|
|
125
133
|
accurate_stop_orders_execution=accurate_stop_orders_execution,
|
|
126
134
|
run_separate_instruments=run_separate_instruments,
|
|
127
135
|
enable_funding=enable_funding,
|
|
136
|
+
maint_margin_rate=maint_margin_rate,
|
|
128
137
|
)
|
|
129
138
|
if not simulation_setups:
|
|
130
139
|
logger.error(
|
|
@@ -132,6 +141,9 @@ def simulate(
|
|
|
132
141
|
:= "Can't recognize setup - it should be a strategy, a set of signals or list of signals/strategies + tracker !"
|
|
133
142
|
)
|
|
134
143
|
raise SimulationError(_msg)
|
|
144
|
+
if accounts:
|
|
145
|
+
for setup in simulation_setups:
|
|
146
|
+
setup.set_accounts(accounts)
|
|
135
147
|
|
|
136
148
|
# - inform about separate instruments mode
|
|
137
149
|
if run_separate_instruments and len(simulation_setups) > 1:
|
|
@@ -259,10 +271,13 @@ def _run_setup(
|
|
|
259
271
|
# Adjust the start date for the simulation to the onboard date of the instrument with the minimum onboard date.
|
|
260
272
|
# TODO: this can be removed once we add some artificial data stream to move the simulation
|
|
261
273
|
if setup.setup_type in [SetupTypes.SIGNAL, SetupTypes.SIGNAL_AND_TRACKER]:
|
|
274
|
+
# - a relisted symbol has data from its first listing on
|
|
262
275
|
listed_dates = [
|
|
263
|
-
to_utc_naive(to_timestamp(
|
|
276
|
+
to_utc_naive(to_timestamp(listing.listed_at))
|
|
264
277
|
for instrument in setup.instruments
|
|
265
|
-
|
|
278
|
+
for listing in lookup.find_listings(instrument.exchange, instrument.symbol, instrument.market_type)
|
|
279
|
+
or [instrument]
|
|
280
|
+
if listing.listed_at is not None
|
|
266
281
|
]
|
|
267
282
|
if listed_dates:
|
|
268
283
|
start = max(start, min(listed_dates))
|
|
@@ -8,6 +8,7 @@ import pandas as pd
|
|
|
8
8
|
|
|
9
9
|
from qubx import logger
|
|
10
10
|
from qubx.core.basics import (
|
|
11
|
+
DEFAULT_MAINTENANCE_MARGIN,
|
|
11
12
|
STABLE_CURRENCIES,
|
|
12
13
|
Balance,
|
|
13
14
|
CtrlChannel,
|
|
@@ -114,7 +115,10 @@ class SimulationSetup:
|
|
|
114
115
|
signal_timeframe: str = "1Min"
|
|
115
116
|
accurate_stop_orders_execution: bool = False
|
|
116
117
|
enable_funding: bool = False
|
|
118
|
+
maint_margin_rate: float = DEFAULT_MAINTENANCE_MARGIN
|
|
117
119
|
base_currencies: dict[str, str] = field(init=False, default_factory=dict)
|
|
120
|
+
# - account -> the markets it trades, its own first, for accounts trading more; see set_accounts
|
|
121
|
+
account_markets: dict[str, tuple[str, ...]] = field(init=False, default_factory=dict)
|
|
118
122
|
|
|
119
123
|
def __post_init__(self) -> None:
|
|
120
124
|
# Per-exchange base currency: explicit mapping wins, else the venue's shared settle
|
|
@@ -146,6 +150,26 @@ class SimulationSetup:
|
|
|
146
150
|
per_exchange = float(self.capital) / n if n > 0 else float(self.capital)
|
|
147
151
|
self.capital = {exchange: per_exchange for exchange in self.exchanges}
|
|
148
152
|
|
|
153
|
+
def set_accounts(self, accounts: dict[str, list[str]] | dict[str, tuple[str, ...]]) -> None:
|
|
154
|
+
"""Trade each account's further markets on that account: one capital pool, their capital
|
|
155
|
+
merged into the account's. Each market keeps its own simulated exchange (fees, execution).
|
|
156
|
+
Markets not traded in this setup are ignored."""
|
|
157
|
+
assert isinstance(self.capital, dict)
|
|
158
|
+
for account, markets in accounts.items():
|
|
159
|
+
further = [m for m in markets if m != account and m in self.exchanges]
|
|
160
|
+
if not further:
|
|
161
|
+
continue
|
|
162
|
+
if account not in self.exchanges:
|
|
163
|
+
raise SimulationConfigError(f"account {account}: not traded in this setup")
|
|
164
|
+
for market in further:
|
|
165
|
+
if self.base_currencies[market] != self.base_currencies[account]:
|
|
166
|
+
raise SimulationConfigError(
|
|
167
|
+
f"account {account}: market {market} has another base currency "
|
|
168
|
+
f"({self.base_currencies[market]} vs {self.base_currencies[account]})"
|
|
169
|
+
)
|
|
170
|
+
self.capital[account] = self.capital.get(account, 0.0) + self.capital.pop(market, 0.0)
|
|
171
|
+
self.account_markets[account] = (account, *further)
|
|
172
|
+
|
|
149
173
|
def __str__(self) -> str:
|
|
150
174
|
return f"{self.name} {self.setup_type} capital {self.capital} {self.base_currency} for [{','.join(map(lambda x: x.symbol, self.instruments))}] @ {self.exchanges}[{self.commissions}]"
|
|
151
175
|
|
|
@@ -405,6 +429,7 @@ def recognize_simulation_configuration(
|
|
|
405
429
|
accurate_stop_orders_execution: bool,
|
|
406
430
|
run_separate_instruments: bool = False,
|
|
407
431
|
enable_funding: bool = False,
|
|
432
|
+
maint_margin_rate: float = DEFAULT_MAINTENANCE_MARGIN,
|
|
408
433
|
) -> list[SimulationSetup]:
|
|
409
434
|
"""
|
|
410
435
|
Recognize and create setups based on the provided simulation configuration.
|
|
@@ -426,6 +451,7 @@ def recognize_simulation_configuration(
|
|
|
426
451
|
- accurate_stop_orders_execution (bool): If True, enables more accurate stop order execution simulation.
|
|
427
452
|
- run_separate_instruments (bool): If True, creates separate setups for each instrument.
|
|
428
453
|
- enable_funding (bool): If True, enables funding rate simulation, default is False.
|
|
454
|
+
- maint_margin_rate (float): Maintenance margin as a fraction of notional for simulated positions.
|
|
429
455
|
|
|
430
456
|
Returns:
|
|
431
457
|
- list[SimulationSetup]: A list of SimulationSetup objects, each representing a
|
|
@@ -447,7 +473,7 @@ def recognize_simulation_configuration(
|
|
|
447
473
|
recognize_simulation_configuration(
|
|
448
474
|
_n + n, v, instruments, exchanges, capital, basic_currency, commissions,
|
|
449
475
|
signal_timeframe, accurate_stop_orders_execution, run_separate_instruments,
|
|
450
|
-
enable_funding
|
|
476
|
+
enable_funding, maint_margin_rate
|
|
451
477
|
)
|
|
452
478
|
)
|
|
453
479
|
|
|
@@ -468,14 +494,14 @@ def recognize_simulation_configuration(
|
|
|
468
494
|
if run_separate_instruments:
|
|
469
495
|
# Create separate setups for each instrument
|
|
470
496
|
for instrument in setup_instruments:
|
|
471
|
-
_s1 =
|
|
497
|
+
_s1 = _s[instrument.symbol] if isinstance(_s, pd.DataFrame) else _s
|
|
472
498
|
r.append(
|
|
473
499
|
SimulationSetup(
|
|
474
500
|
_t, f"{name}/{instrument.symbol}", _s1, c1, # type: ignore
|
|
475
501
|
[instrument],
|
|
476
502
|
exchanges, capital, basic_currency, commissions,
|
|
477
503
|
signal_timeframe, accurate_stop_orders_execution,
|
|
478
|
-
enable_funding
|
|
504
|
+
enable_funding, maint_margin_rate
|
|
479
505
|
)
|
|
480
506
|
)
|
|
481
507
|
else:
|
|
@@ -485,7 +511,7 @@ def recognize_simulation_configuration(
|
|
|
485
511
|
setup_instruments,
|
|
486
512
|
exchanges, capital, basic_currency, commissions,
|
|
487
513
|
signal_timeframe, accurate_stop_orders_execution,
|
|
488
|
-
enable_funding
|
|
514
|
+
enable_funding, maint_margin_rate
|
|
489
515
|
)
|
|
490
516
|
)
|
|
491
517
|
else:
|
|
@@ -495,7 +521,7 @@ def recognize_simulation_configuration(
|
|
|
495
521
|
# name + "/" + str(j), s, instruments, exchange, capital, basic_currency, commissions
|
|
496
522
|
name, s, instruments, exchanges, capital, basic_currency, commissions, # type: ignore
|
|
497
523
|
signal_timeframe, accurate_stop_orders_execution, run_separate_instruments,
|
|
498
|
-
enable_funding
|
|
524
|
+
enable_funding, maint_margin_rate
|
|
499
525
|
)
|
|
500
526
|
)
|
|
501
527
|
|
|
@@ -509,7 +535,7 @@ def recognize_simulation_configuration(
|
|
|
509
535
|
f"{name}/{instrument.symbol}", configs, None, [instrument],
|
|
510
536
|
exchanges, capital, basic_currency, commissions,
|
|
511
537
|
signal_timeframe, accurate_stop_orders_execution,
|
|
512
|
-
enable_funding
|
|
538
|
+
enable_funding, maint_margin_rate
|
|
513
539
|
)
|
|
514
540
|
)
|
|
515
541
|
else:
|
|
@@ -519,7 +545,7 @@ def recognize_simulation_configuration(
|
|
|
519
545
|
name, configs, None, instruments,
|
|
520
546
|
exchanges, capital, basic_currency, commissions,
|
|
521
547
|
signal_timeframe, accurate_stop_orders_execution,
|
|
522
|
-
enable_funding
|
|
548
|
+
enable_funding, maint_margin_rate
|
|
523
549
|
)
|
|
524
550
|
)
|
|
525
551
|
|
|
@@ -538,7 +564,7 @@ def recognize_simulation_configuration(
|
|
|
538
564
|
f"{name}/{instrument.symbol}", _c1, None, [instrument],
|
|
539
565
|
exchanges, capital, basic_currency, commissions,
|
|
540
566
|
signal_timeframe, accurate_stop_orders_execution,
|
|
541
|
-
enable_funding
|
|
567
|
+
enable_funding, maint_margin_rate
|
|
542
568
|
)
|
|
543
569
|
)
|
|
544
570
|
else:
|
|
@@ -548,7 +574,7 @@ def recognize_simulation_configuration(
|
|
|
548
574
|
name, c1, None, setup_instruments,
|
|
549
575
|
exchanges, capital, basic_currency, commissions,
|
|
550
576
|
signal_timeframe, accurate_stop_orders_execution,
|
|
551
|
-
enable_funding
|
|
577
|
+
enable_funding, maint_margin_rate
|
|
552
578
|
)
|
|
553
579
|
)
|
|
554
580
|
|
|
@@ -91,6 +91,7 @@ from qubx.utils.time import to_timedelta
|
|
|
91
91
|
|
|
92
92
|
from .exceptions import CcxtSymbolNotRecognized
|
|
93
93
|
from .exchange_manager import ExchangeManager
|
|
94
|
+
from .replace import Replacement
|
|
94
95
|
from .utils import (
|
|
95
96
|
FRAMEWORK_ONLY_OPTIONS,
|
|
96
97
|
ccxt_convert_balance,
|
|
@@ -169,6 +170,25 @@ class _LeverageInfo:
|
|
|
169
170
|
margin_mode: str | None = None
|
|
170
171
|
|
|
171
172
|
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class _ReplaceRequest:
|
|
175
|
+
"""An amend to run as cancel + recreate, read off the Order synchronously in update_order."""
|
|
176
|
+
|
|
177
|
+
instrument: Instrument
|
|
178
|
+
client_order_id: str | None
|
|
179
|
+
venue_order_id: str | None
|
|
180
|
+
side: str
|
|
181
|
+
order_type: str
|
|
182
|
+
time_in_force: str
|
|
183
|
+
post_only: bool
|
|
184
|
+
reduce_only: bool
|
|
185
|
+
price: float | None
|
|
186
|
+
total: float
|
|
187
|
+
local_filled: float
|
|
188
|
+
requested_price: float | None
|
|
189
|
+
requested_total: float | None
|
|
190
|
+
|
|
191
|
+
|
|
172
192
|
_VENUE_VERDICT_ERRORS: tuple[type[Exception], ...] = (
|
|
173
193
|
ccxt.InsufficientFunds,
|
|
174
194
|
ccxt.OrderNotFillable,
|
|
@@ -223,6 +243,9 @@ class CcxtConnector(ChannelEmitter):
|
|
|
223
243
|
# make_client_id actually produces, so producer and classifier can never drift.
|
|
224
244
|
cid_framework_prefix: str = FRAMEWORK_CID_PREFIX
|
|
225
245
|
|
|
246
|
+
# Markets an account of this venue can trade besides its own (ExchangePlugin.extra_markets).
|
|
247
|
+
EXTRA_MARKETS: tuple[str, ...] = ()
|
|
248
|
+
|
|
226
249
|
# Whether the venue's ACCOUNT_UPDATE `a.B[].wb` is the ACCOUNT wallet balance.
|
|
227
250
|
# True on plain UM/CM, where the futures wallet is the account. False on venues
|
|
228
251
|
# whose stream reports a SUB-wallet (Binance PM) — there an absolute push would
|
|
@@ -269,6 +292,8 @@ class CcxtConnector(ChannelEmitter):
|
|
|
269
292
|
self._leverage_future: Any = None
|
|
270
293
|
# - venue leverage state per ccxt symbol, refreshed by _leverage_poll_loop
|
|
271
294
|
self._leverage_cache: dict[str, _LeverageInfo] = {}
|
|
295
|
+
# - client id -> its amend-by-cancel+recreate record (see replace.py); touched on the loop only
|
|
296
|
+
self._replacements: dict[str, Replacement] = {}
|
|
272
297
|
|
|
273
298
|
# Re-subscribe the account WS stream + resync against venue truth after the
|
|
274
299
|
# ExchangeManager swaps in a fresh exchange (the running _subscribe_executions
|
|
@@ -396,6 +421,9 @@ class CcxtConnector(ChannelEmitter):
|
|
|
396
421
|
if k in FRAMEWORK_ONLY_OPTIONS:
|
|
397
422
|
continue
|
|
398
423
|
payload["params"].setdefault(k, v)
|
|
424
|
+
if instrument.is_spot():
|
|
425
|
+
# - spot has no reduce-only (Binance PM margin rejects it); executors may still set it
|
|
426
|
+
payload["params"].pop("reduceOnly", None)
|
|
399
427
|
|
|
400
428
|
self._spawn(self._submit_async(instrument, request.client_id, payload))
|
|
401
429
|
|
|
@@ -483,6 +511,13 @@ class CcxtConnector(ChannelEmitter):
|
|
|
483
511
|
outcome (the cancel may still have landed), so the order is left inflight for
|
|
484
512
|
AM to reconcile rather than terminal-rejected.
|
|
485
513
|
"""
|
|
514
|
+
if client_order_id is not None and (r := self._replacements.get(client_order_id)) is not None:
|
|
515
|
+
if r.in_flight:
|
|
516
|
+
# - the replace resolves it: no recreate, or a cancel of the recreated order
|
|
517
|
+
r.cancel_requested = True
|
|
518
|
+
return
|
|
519
|
+
if venue_order_id in r.superseded and r.current_vid is not None:
|
|
520
|
+
venue_order_id = r.current_vid
|
|
486
521
|
try:
|
|
487
522
|
# Outside _cancel_with_retry: one slot per operation, and its broad handlers would eat the timeout.
|
|
488
523
|
await self._acquire_endpoint_budget("cancel_order")
|
|
@@ -713,6 +748,35 @@ class CcxtConnector(ChannelEmitter):
|
|
|
713
748
|
f"update_order for {order.client_order_id}: effective amend amount "
|
|
714
749
|
f"{wire_amount} <= 0 (total {total}, filled {order.filled_quantity})"
|
|
715
750
|
)
|
|
751
|
+
if not self._amends_in_place(order.instrument):
|
|
752
|
+
if not self._replaces_with_same_client_id:
|
|
753
|
+
# - no in-place amend and no verified emulation: refuse without touching the order
|
|
754
|
+
self._emit_update_rejected(
|
|
755
|
+
order.client_order_id,
|
|
756
|
+
order.venue_order_id,
|
|
757
|
+
ccxt.NotSupported(f"{self.exchange_name} cannot amend {order.instrument.symbol} orders"),
|
|
758
|
+
)
|
|
759
|
+
return
|
|
760
|
+
self._spawn(
|
|
761
|
+
self._replace_async(
|
|
762
|
+
_ReplaceRequest(
|
|
763
|
+
instrument=order.instrument,
|
|
764
|
+
client_order_id=order.client_order_id,
|
|
765
|
+
venue_order_id=order.venue_order_id,
|
|
766
|
+
side=order.side,
|
|
767
|
+
order_type=str(order.type),
|
|
768
|
+
time_in_force=order.time_in_force,
|
|
769
|
+
post_only=order.post_only or str(order.time_in_force).upper() == "GTX",
|
|
770
|
+
reduce_only=order.reduce_only,
|
|
771
|
+
price=wire_price,
|
|
772
|
+
total=total,
|
|
773
|
+
local_filled=order.filled_quantity,
|
|
774
|
+
requested_price=price,
|
|
775
|
+
requested_total=quantity,
|
|
776
|
+
)
|
|
777
|
+
)
|
|
778
|
+
)
|
|
779
|
+
return
|
|
716
780
|
self._spawn(
|
|
717
781
|
self._update_async(
|
|
718
782
|
order.client_order_id,
|
|
@@ -750,12 +814,7 @@ class CcxtConnector(ChannelEmitter):
|
|
|
750
814
|
vid = venue_order_id
|
|
751
815
|
try:
|
|
752
816
|
await self._acquire_endpoint_budget("edit_order")
|
|
753
|
-
|
|
754
|
-
r = await self._edit_order_direct(
|
|
755
|
-
client_order_id, vid, symbol, side, order_type, wire_price, wire_amount
|
|
756
|
-
)
|
|
757
|
-
else:
|
|
758
|
-
r = await self._update_via_cancel_recreate(client_order_id, venue_order_id, wire_price, wire_amount)
|
|
817
|
+
r = await self._edit_order_direct(client_order_id, vid, symbol, side, order_type, wire_price, wire_amount)
|
|
759
818
|
except RateLimitGateTimeout as e:
|
|
760
819
|
self._emit_update_rejected(client_order_id, venue_order_id, e)
|
|
761
820
|
return
|
|
@@ -829,18 +888,202 @@ class CcxtConnector(ChannelEmitter):
|
|
|
829
888
|
params={},
|
|
830
889
|
)
|
|
831
890
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
891
|
+
# - the venue lets a cancelled order's client id be reused by a new order (unique among OPEN
|
|
892
|
+
# orders only), so an amend can be emulated as cancel + recreate; opt-in per venue, verified live
|
|
893
|
+
_replaces_with_same_client_id: bool = False
|
|
894
|
+
|
|
895
|
+
def _amends_in_place(self, instrument: Instrument) -> bool:
|
|
896
|
+
"""True when the venue modifies an order of ``instrument`` in place; else ``update_order``
|
|
897
|
+
is emulated as cancel + recreate under the same client id (``_replace_async``)."""
|
|
898
|
+
return bool(self._em.exchange.has.get("editOrder", False))
|
|
899
|
+
|
|
900
|
+
async def _replace_async(self, req: "_ReplaceRequest") -> None:
|
|
901
|
+
"""Amend as cancel + recreate, keeping the client id (see replace.py for the event rules).
|
|
902
|
+
|
|
903
|
+
The order stays live unless the cancel lands: any refusal before it is an update reject.
|
|
904
|
+
After it, the order is either recreated (OrderUpdatedEvent carrying the new venue id) or
|
|
905
|
+
it has ended (OrderCanceledEvent: nothing left to place, recreate refused, or the strategy
|
|
906
|
+
cancelled meanwhile). The recreate is sized from the cancel response's executed quantity,
|
|
907
|
+
the venue's final figure, not the local one, which may trail the fill stream.
|
|
908
|
+
"""
|
|
909
|
+
cid, vid, instrument = req.client_order_id, req.venue_order_id, req.instrument
|
|
910
|
+
symbol = instrument_to_ccxt_symbol(instrument)
|
|
911
|
+
if vid is None:
|
|
912
|
+
self._emit_update_rejected(cid, vid, ccxt.NotSupported("order not acknowledged yet; amend after the ack"))
|
|
913
|
+
return
|
|
914
|
+
record = self._replacements.get(cid)
|
|
915
|
+
if record is not None and record.in_flight:
|
|
916
|
+
self._emit_update_rejected(cid, vid, ccxt.NotSupported("a replace of this order is already in flight"))
|
|
917
|
+
return
|
|
918
|
+
if record is not None and vid in record.superseded and record.current_vid is not None:
|
|
919
|
+
vid = record.current_vid
|
|
920
|
+
# - validate the replacement before touching the live order: a refusal now leaves it as is
|
|
921
|
+
try:
|
|
922
|
+
self._replacement_payload(req, req.total - req.local_filled)
|
|
923
|
+
except (InvalidOrderParameters, ccxt.BadRequest) as e:
|
|
924
|
+
self._emit_update_rejected(cid, vid, e)
|
|
925
|
+
return
|
|
926
|
+
if record is None:
|
|
927
|
+
record = self._replacements[cid] = Replacement(instrument=instrument, cancelling_vid=None)
|
|
928
|
+
record.begin(vid)
|
|
929
|
+
|
|
930
|
+
try:
|
|
931
|
+
await self._acquire_endpoint_budget("cancel_order")
|
|
932
|
+
ok, response = await self._cancel_with_retry(cid, vid, symbol)
|
|
933
|
+
except RateLimitGateTimeout as e:
|
|
934
|
+
self._end_replace(cid, record, vid, symbol)
|
|
935
|
+
self._emit_update_rejected(cid, vid, e)
|
|
936
|
+
return
|
|
937
|
+
except ccxt.NetworkError as e:
|
|
938
|
+
# - UNKNOWN whether the cancel landed: no recreate (two live orders would be worse than
|
|
939
|
+
# none); stop filtering so the venue's own report of the old order gets through
|
|
940
|
+
self._end_replace(cid, record, vid, symbol)
|
|
941
|
+
logger.warning(f"[{self.exchange_name}] replace of {cid}: cancel outcome unknown ({e}); leaving inflight")
|
|
942
|
+
return
|
|
943
|
+
if ok is False:
|
|
944
|
+
self._end_replace(cid, record, vid, symbol)
|
|
945
|
+
self._emit_update_rejected(cid, vid, ccxt.OperationRejected("venue refused the cancel of the replace"))
|
|
946
|
+
return
|
|
947
|
+
if ok is None:
|
|
948
|
+
# - already gone (filled, or ended elsewhere): its terminal report is the truth, fetch it
|
|
949
|
+
self._end_replace(cid, record)
|
|
950
|
+
self._emit_update_rejected(cid, vid, ccxt.OrderNotFound("order no longer open; not replaced"))
|
|
951
|
+
self._spawn(self._order_status_async(cid, vid, symbol, instrument))
|
|
952
|
+
return
|
|
953
|
+
|
|
954
|
+
filled = float((response or {}).get("filled") or 0.0)
|
|
955
|
+
cost = float((response or {}).get("cost") or 0.0)
|
|
956
|
+
record.cancelled(vid, filled, cost)
|
|
957
|
+
remaining = req.total - record.filled_before
|
|
958
|
+
if record.cancel_requested:
|
|
959
|
+
self._finish_replace_as_cancel(cid, vid, record, "cancel requested during the replace")
|
|
960
|
+
return
|
|
961
|
+
try:
|
|
962
|
+
payload = self._replacement_payload(req, remaining)
|
|
963
|
+
except (InvalidOrderParameters, ccxt.BadRequest) as e:
|
|
964
|
+
self._finish_replace_as_cancel(cid, vid, record, f"remaining {remaining} not placeable: {e}")
|
|
965
|
+
return
|
|
966
|
+
|
|
967
|
+
try:
|
|
968
|
+
await self._acquire_endpoint_budget("create_order")
|
|
969
|
+
created = await self._em.exchange.create_order(**payload)
|
|
970
|
+
except (RateLimitGateTimeout, *_VENUE_VERDICT_ERRORS) as e:
|
|
971
|
+
# - verdicts first: rate-limit / maintenance are NetworkError subclasses (see _submit_async)
|
|
972
|
+
self._report_order_limit_hit(e)
|
|
973
|
+
self._finish_replace_as_cancel(cid, vid, record, f"recreate refused: {e}")
|
|
974
|
+
return
|
|
975
|
+
except ccxt.NetworkError as e:
|
|
976
|
+
logger.warning(f"[{self.exchange_name}] replace of {cid}: recreate outcome unknown ({e}); looking it up")
|
|
977
|
+
await self._resolve_unknown_recreate(req, vid, record)
|
|
978
|
+
return
|
|
979
|
+
except Exception as e: # noqa: BLE001
|
|
980
|
+
logger.error(f"[{self.exchange_name}] replace of {cid}: unexpected recreate error {e!r}; looking it up")
|
|
981
|
+
await self._resolve_unknown_recreate(req, vid, record)
|
|
982
|
+
return
|
|
983
|
+
if created is None or created.get("id") is None:
|
|
984
|
+
await self._resolve_unknown_recreate(req, vid, record)
|
|
985
|
+
return
|
|
986
|
+
self._recreated(req, record, str(created["id"]))
|
|
987
|
+
|
|
988
|
+
def _replacement_payload(self, req: "_ReplaceRequest", amount: float) -> dict[str, Any]:
|
|
989
|
+
if amount <= 0:
|
|
990
|
+
raise InvalidOrderParameters(f"nothing left to place ({amount})")
|
|
991
|
+
quote = self._data_provider.get_quote(req.instrument)
|
|
992
|
+
price = req.price
|
|
993
|
+
if req.post_only and quote is not None and price is not None:
|
|
994
|
+
crosses = price >= quote.ask if req.side == "BUY" else price <= quote.bid
|
|
995
|
+
if crosses:
|
|
996
|
+
# - a post-only replacement at a crossing price would be refused AFTER the cancel
|
|
997
|
+
raise InvalidOrderParameters(f"post-only price {price} crosses the book ({quote.bid}/{quote.ask})")
|
|
998
|
+
payload = prepare_ccxt_order_payload(
|
|
999
|
+
instrument=req.instrument,
|
|
1000
|
+
order_side=req.side,
|
|
1001
|
+
order_type=req.order_type,
|
|
1002
|
+
amount=amount,
|
|
1003
|
+
price=price,
|
|
1004
|
+
client_id=req.client_order_id,
|
|
1005
|
+
time_in_force=req.time_in_force,
|
|
1006
|
+
quote=quote,
|
|
1007
|
+
reduce_only=req.reduce_only,
|
|
1008
|
+
post_only=req.post_only,
|
|
1009
|
+
)
|
|
1010
|
+
if req.instrument.is_spot():
|
|
1011
|
+
payload["params"].pop("reduceOnly", None)
|
|
1012
|
+
return payload
|
|
1013
|
+
|
|
1014
|
+
def _recreated(self, req: "_ReplaceRequest", record: Replacement, new_vid: str) -> None:
|
|
1015
|
+
record.recreated(new_vid)
|
|
1016
|
+
logger.debug(f"[{self.exchange_name}] replace of {req.client_order_id}: now venue order {new_vid}")
|
|
1017
|
+
self.send(
|
|
1018
|
+
OrderUpdatedEvent(
|
|
1019
|
+
instrument=None,
|
|
1020
|
+
client_order_id=req.client_order_id,
|
|
1021
|
+
venue_order_id=new_vid,
|
|
1022
|
+
new_price=req.requested_price,
|
|
1023
|
+
new_quantity=req.requested_total,
|
|
1024
|
+
)
|
|
1025
|
+
)
|
|
1026
|
+
if record.cancel_requested:
|
|
1027
|
+
self._spawn(self._cancel_async(req.client_order_id, new_vid, instrument_to_ccxt_symbol(req.instrument)))
|
|
1028
|
+
|
|
1029
|
+
async def _resolve_unknown_recreate(self, req: "_ReplaceRequest", old_vid: str, record: Replacement) -> None:
|
|
1030
|
+
"""The recreate may or may not have landed: look the client id up (the venue returns its
|
|
1031
|
+
newest order) — a new venue id means it did, the cancelled one means it did not."""
|
|
1032
|
+
cid = req.client_order_id
|
|
1033
|
+
assert cid is not None
|
|
1034
|
+
symbol = instrument_to_ccxt_symbol(req.instrument)
|
|
1035
|
+
for attempt in range(3):
|
|
1036
|
+
await asyncio.sleep(1.0 + attempt)
|
|
1037
|
+
try:
|
|
1038
|
+
raw = await self._em.exchange.fetch_order_with_client_order_id(cid, symbol)
|
|
1039
|
+
except ccxt.OrderNotFound:
|
|
1040
|
+
break
|
|
1041
|
+
except Exception as e: # noqa: BLE001
|
|
1042
|
+
logger.warning(f"[{self.exchange_name}] replace of {cid}: lookup failed ({e}); retrying")
|
|
1043
|
+
continue
|
|
1044
|
+
found = None if raw is None else raw.get("id")
|
|
1045
|
+
if found is not None and str(found) != old_vid and str(found) not in record.superseded:
|
|
1046
|
+
self._recreated(req, record, str(found))
|
|
1047
|
+
self._handle_ws_order(raw)
|
|
1048
|
+
return
|
|
1049
|
+
break
|
|
1050
|
+
else:
|
|
1051
|
+
# - could not tell: stop filtering and leave the order to the reconciler
|
|
1052
|
+
logger.error(
|
|
1053
|
+
f"[{self.exchange_name}] replace of {cid}: recreate outcome still unknown; reconciler takes over"
|
|
1054
|
+
)
|
|
1055
|
+
# - a recreated order's venue id is unknown here: a pending cancel goes by client id
|
|
1056
|
+
self._end_replace(cid, record, None, symbol)
|
|
1057
|
+
return
|
|
1058
|
+
self._finish_replace_as_cancel(cid, old_vid, record, "recreate did not land")
|
|
1059
|
+
|
|
1060
|
+
def _finish_replace_as_cancel(self, cid: str | None, vid: str, record: Replacement, why: str) -> None:
|
|
1061
|
+
logger.warning(f"[{self.exchange_name}] replace of {cid} ended the order: {why}")
|
|
1062
|
+
self._end_replace(cid, record)
|
|
1063
|
+
self.send(OrderCanceledEvent(instrument=None, client_order_id=cid, venue_order_id=vid))
|
|
1064
|
+
|
|
1065
|
+
def _end_replace(
|
|
1066
|
+
self, cid: str | None, record: Replacement, live_vid: str | None = None, symbol: str | None = None
|
|
1067
|
+
) -> None:
|
|
1068
|
+
"""Stop the replace. With ``symbol`` given the order may still be live at the venue (``live_vid``, or
|
|
1069
|
+
unknown): a cancel the strategy asked for meanwhile, held back by the replace, is sent now."""
|
|
1070
|
+
# - kept only while a live recreated order needs its reports rewritten
|
|
1071
|
+
record.abandoned()
|
|
1072
|
+
if cid is not None and (record.current_vid is None or not record.superseded):
|
|
1073
|
+
self._replacements.pop(cid, None)
|
|
1074
|
+
if symbol is not None and record.cancel_requested:
|
|
1075
|
+
record.cancel_requested = False
|
|
1076
|
+
self._spawn(self._cancel_async(cid, live_vid, symbol))
|
|
1077
|
+
|
|
1078
|
+
def _replaced_view(self, order: Order) -> Order | None:
|
|
1079
|
+
"""Route one venue report of an order through its replace record, if it has one."""
|
|
1080
|
+
record = self._replacements.get(order.client_order_id) if order.client_order_id else None
|
|
1081
|
+
if record is None:
|
|
1082
|
+
return order
|
|
1083
|
+
viewed = record.view(order)
|
|
1084
|
+
if viewed is not None and record.is_done(viewed):
|
|
1085
|
+
self._replacements.pop(order.client_order_id, None)
|
|
1086
|
+
return viewed
|
|
844
1087
|
|
|
845
1088
|
def _emit_update_rejected(self, client_order_id: str | None, venue_order_id: str | None, error: Exception) -> None:
|
|
846
1089
|
logger.warning(f"[{self.exchange_name}] Update for {client_order_id or venue_order_id} rejected: {error}")
|
|
@@ -1536,7 +1779,9 @@ class CcxtConnector(ChannelEmitter):
|
|
|
1536
1779
|
logger.warning(f"[{self.exchange_name}] WS order for unknown symbol {raw.get('symbol')}; skipped")
|
|
1537
1780
|
return
|
|
1538
1781
|
order = ccxt_convert_order_info(instrument, raw, framework_prefix=self.cid_framework_prefix)
|
|
1539
|
-
self.
|
|
1782
|
+
if (viewed := self._replaced_view(order)) is None:
|
|
1783
|
+
return
|
|
1784
|
+
self._emit_order_events(instrument, viewed, raw)
|
|
1540
1785
|
|
|
1541
1786
|
def _emit_order_events(self, instrument: Instrument, order: Order, raw: dict[str, Any]) -> None:
|
|
1542
1787
|
"""Map a converted order's status to the typed lifecycle event(s).
|
|
@@ -1964,6 +2209,15 @@ class CcxtConnector(ChannelEmitter):
|
|
|
1964
2209
|
logger.debug(f"[{self.exchange_name}] snapshot: no trigger open-orders surface ({e}); regular only")
|
|
1965
2210
|
return []
|
|
1966
2211
|
|
|
2212
|
+
async def _fetch_extra_open_orders(self) -> list[dict]:
|
|
2213
|
+
"""Open orders the venue-wide ``fetch_open_orders`` does not return: trigger orders here;
|
|
2214
|
+
a venue with a further order surface (Binance PM margin) adds it."""
|
|
2215
|
+
return await self._fetch_trigger_open_orders()
|
|
2216
|
+
|
|
2217
|
+
async def _balance_positions(self, raw_balance: dict[str, Any]) -> list[Position]:
|
|
2218
|
+
"""Positions the venue reports as balances rather than positions (spot holdings); none here."""
|
|
2219
|
+
return []
|
|
2220
|
+
|
|
1967
2221
|
def _merge_open_orders(self, raw_orders: list[dict], raw_trigger_orders: list[dict]) -> list[Order]:
|
|
1968
2222
|
"""Merge regular + trigger raw open orders into framework Orders (dedup by venue id).
|
|
1969
2223
|
|
|
@@ -1983,6 +2237,9 @@ class CcxtConnector(ChannelEmitter):
|
|
|
1983
2237
|
)
|
|
1984
2238
|
continue
|
|
1985
2239
|
order = ccxt_convert_order_info(instrument, raw, framework_prefix=self.cid_framework_prefix)
|
|
2240
|
+
if (viewed := self._replaced_view(order)) is None:
|
|
2241
|
+
continue
|
|
2242
|
+
order = viewed
|
|
1986
2243
|
key = order.venue_order_id or order.client_order_id
|
|
1987
2244
|
if key is not None and key in seen:
|
|
1988
2245
|
continue
|
|
@@ -2028,7 +2285,7 @@ class CcxtConnector(ChannelEmitter):
|
|
|
2028
2285
|
if include_orders:
|
|
2029
2286
|
raw_orders, raw_trigger_orders, raw_positions, raw_balance = await asyncio.gather(
|
|
2030
2287
|
ex.fetch_open_orders(),
|
|
2031
|
-
self.
|
|
2288
|
+
self._fetch_extra_open_orders(),
|
|
2032
2289
|
ex.fetch_positions(),
|
|
2033
2290
|
ex.fetch_balance(),
|
|
2034
2291
|
)
|
|
@@ -2038,6 +2295,7 @@ class CcxtConnector(ChannelEmitter):
|
|
|
2038
2295
|
open_orders = None # - not observed this tick -> reconcile skips order diffing
|
|
2039
2296
|
positions = ccxt_convert_positions(raw_positions, ex.name, ex.markets)
|
|
2040
2297
|
await self._fill_leverage_settings(positions)
|
|
2298
|
+
positions += await self._balance_positions(raw_balance)
|
|
2041
2299
|
balances = self._convert_balances(raw_balance)
|
|
2042
2300
|
figures = self._extract_venue_figures(raw_balance)
|
|
2043
2301
|
except Exception as e: # noqa: BLE001 — AM retries on its next snapshot tick
|