Qubx 3.6.2.dev1__tar.gz → 3.8.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-3.6.2.dev1 → qubx-3.8.0}/PKG-INFO +1 -1
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/_version.py +2 -2
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/connector.py +6 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +14 -1
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/utils.py +21 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/diffs.py +2 -1
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/reconciler.py +60 -7
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/reducer.py +12 -5
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/state.py +13 -3
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/basics.py +36 -2
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/events.py +4 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/utils.pyx +41 -2
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/app.py +2 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/orders_table.py +8 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/positions_table.py +7 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/.gitignore +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/LICENSE +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/README.md +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/pyproject.toml +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/scripts/build.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/_nb_magic.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/connector.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/data.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/iteratedstream.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/management.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/ome.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/optimization.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/runner.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/simulator.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/transfers.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/backtester/utils.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/commands.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/deploy.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/misc.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/release.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/resolver.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/s3.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/theme.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/tui.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/cli/user_config.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/config.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/_two_stream.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/binance/connector.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/bitfinex/connector.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/okx/connector.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/plugin.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/rate_limits.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/plugin.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/registry.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/tardis/plugin.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/builtin.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/decorator.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/executor.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/interfaces.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/server.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/control/types.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/config.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/manager.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/account_manager/state_machine.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/boot.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/connector.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/context.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/detectors/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/detectors/delisting.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/detectors/stale.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/errors.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/exceptions.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/fit_context.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/fit_executor.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/helpers.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/initializer.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/instrument_service.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/interfaces.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/loggers.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/lookups.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/metrics.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/instrument_service.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/market.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/processing.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/series.pxd +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/series.pyi +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/series.pyx +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/status.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/core/utils.pyi +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/cache.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/containers.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/guards.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/registry.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storage.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/ccxt.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/csv.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/handy.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/multi.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/questdb.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/stub.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/storages/utils.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/data/transformers.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/base.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/composite.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/csv.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/indicator.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/emitters/questdb.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/composite.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/exporters/slack.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/gathering/simplest.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/health/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/health/base.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/health/dummy.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/loggers/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/loggers/csv.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/loggers/factory.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/loggers/mongo.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/loggers/postgres.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/notifications/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/notifications/composite.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/notifications/slack.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/notifications/throttler.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/pandaz/stats.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/pandaz/ta.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/pandaz/utils.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/plugins/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/plugins/loader.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/backend.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/config.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/engine.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/ip_resolver.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/manager.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/pools.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/redis_backend.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/rate_limiting/resilient.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/_build.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restarts/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/balance.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/factory.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/position.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/signal.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/state.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/restorers/utils.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/state/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/state/dummy.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/state/redis.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/state/safe.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/ta/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/base.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/project/template.yml +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.gitignore.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.python-version +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.vscode/launch.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.vscode/settings.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/README.md.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/configs/.gitkeep +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/justfile.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/research/.gitkeep +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/template.yml +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/trackers/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/trackers/advanced.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/trackers/composite.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/trackers/sizers.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/clock.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/collections.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/hft/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/hft/numba_utils.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/hft/orderbook.pyi +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/hft/orderbook.pyx +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/misc.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/nonce.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/orderbook.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/questdb.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/rate_limiter.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/results.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/ringbuffer.pxd +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/ringbuffer.pyi +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/ringbuffer.pyx +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/runner.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/handlers.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/init_code.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/styles.tcss +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/log_filter.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/s3.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/slack.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/threading.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/throttler.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/time.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/src/qubx/utils/websocket_manager.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.0}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
- {qubx-3.6.2.dev1 → qubx-3.8.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 = '3.
|
|
22
|
-
__version_tuple__ = version_tuple = (3,
|
|
21
|
+
__version__ = version = '3.8.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 8, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -52,6 +52,7 @@ from qubx.core.basics import (
|
|
|
52
52
|
OrderStatus,
|
|
53
53
|
OrderType,
|
|
54
54
|
Position,
|
|
55
|
+
RejectCause,
|
|
55
56
|
dt_64,
|
|
56
57
|
resolve_reduce_only,
|
|
57
58
|
)
|
|
@@ -94,6 +95,7 @@ from .utils import (
|
|
|
94
95
|
instrument_to_ccxt_symbol,
|
|
95
96
|
normalize_margin_mode,
|
|
96
97
|
prepare_ccxt_order_payload,
|
|
98
|
+
reject_cause_of,
|
|
97
99
|
)
|
|
98
100
|
|
|
99
101
|
# Venue-verdict exceptions: every one of these is the venue refusing the order,
|
|
@@ -380,6 +382,7 @@ class CcxtConnector(ChannelEmitter):
|
|
|
380
382
|
client_order_id=client_id,
|
|
381
383
|
reason=str(error),
|
|
382
384
|
code=type(error).__name__,
|
|
385
|
+
cause=reject_cause_of(error),
|
|
383
386
|
)
|
|
384
387
|
)
|
|
385
388
|
|
|
@@ -441,6 +444,7 @@ class CcxtConnector(ChannelEmitter):
|
|
|
441
444
|
venue_order_id: str | None,
|
|
442
445
|
reason: str | None = None,
|
|
443
446
|
code: str | None = None,
|
|
447
|
+
cause: RejectCause = RejectCause.UNKNOWN,
|
|
444
448
|
) -> None:
|
|
445
449
|
# Carry both ids: AM's reject handler resolves the order by cid first, then venue id,
|
|
446
450
|
# so the order can revert out of PENDING_CANCEL regardless of which id the caller had.
|
|
@@ -451,6 +455,7 @@ class CcxtConnector(ChannelEmitter):
|
|
|
451
455
|
venue_order_id=venue_order_id,
|
|
452
456
|
reason=reason or f"venue rejected cancel for {venue_order_id or client_order_id}",
|
|
453
457
|
code=code,
|
|
458
|
+
cause=cause,
|
|
454
459
|
)
|
|
455
460
|
)
|
|
456
461
|
|
|
@@ -772,6 +777,7 @@ class CcxtConnector(ChannelEmitter):
|
|
|
772
777
|
venue_order_id=venue_order_id,
|
|
773
778
|
reason=str(error),
|
|
774
779
|
code=type(error).__name__,
|
|
780
|
+
cause=reject_cause_of(error),
|
|
775
781
|
)
|
|
776
782
|
)
|
|
777
783
|
|
|
@@ -731,7 +731,20 @@ class BinancePortfolioMargin(BinanceQVUSDM):
|
|
|
731
731
|
"defaultType": "swap",
|
|
732
732
|
"portfolioMargin": True,
|
|
733
733
|
"fetchMarkets": ["spot", "linear", "inverse"],
|
|
734
|
-
}
|
|
734
|
+
},
|
|
735
|
+
# ccxt's handle_errors reads the URL-scoped map BEFORE the global one, and its
|
|
736
|
+
# portfolioMargin group maps -2019 to OperationFailed — so a papi order refused
|
|
737
|
+
# for margin never reached BinanceQV's global "-2019": InsufficientFunds. It has
|
|
738
|
+
# to sit here, in the LAST describe of the chain: binanceusdm's own describe runs
|
|
739
|
+
# after BinanceQV's and would overwrite the group. Measured on BINANCE.PM: 105
|
|
740
|
+
# margin refusals arrived as OperationFailed and were logged as unexpected errors.
|
|
741
|
+
"exceptions": {
|
|
742
|
+
"portfolioMargin": {
|
|
743
|
+
"exact": {
|
|
744
|
+
"-2019": InsufficientFunds,
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
},
|
|
735
748
|
},
|
|
736
749
|
)
|
|
737
750
|
|
|
@@ -22,6 +22,7 @@ from qubx.core.basics import (
|
|
|
22
22
|
OrderSide,
|
|
23
23
|
OrderStatus,
|
|
24
24
|
Position,
|
|
25
|
+
RejectCause,
|
|
25
26
|
classify_origin,
|
|
26
27
|
dt_64,
|
|
27
28
|
)
|
|
@@ -58,6 +59,19 @@ _CCXT_STATUS_MAP: dict[str, OrderStatus] = {
|
|
|
58
59
|
"rejected": OrderStatus.REJECTED,
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
# - ccxt normalises every venue's error into its own exception classes, so the class name is
|
|
63
|
+
# already a portable vocabulary; this is the reading of it the framework acts on. Names, not
|
|
64
|
+
# classes, so a ccxt version that drops one does not break the import.
|
|
65
|
+
_REJECT_CAUSE_BY_CCXT_ERROR: dict[str, RejectCause] = {
|
|
66
|
+
"InsufficientFunds": RejectCause.INSUFFICIENT_MARGIN,
|
|
67
|
+
"OrderNotFillable": RejectCause.NOT_FILLABLE,
|
|
68
|
+
"OrderImmediatelyFillable": RejectCause.NOT_FILLABLE,
|
|
69
|
+
"RateLimitExceeded": RejectCause.RATE_LIMITED,
|
|
70
|
+
"DDoSProtection": RejectCause.RATE_LIMITED,
|
|
71
|
+
"OrderNotFound": RejectCause.NOT_FOUND,
|
|
72
|
+
"InvalidOrder": RejectCause.TOO_SMALL,
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
|
|
62
76
|
def info_float(info: dict[str, Any], key: str) -> float | None:
|
|
63
77
|
"""Parse an optional numeric field from a raw venue payload; None when absent/malformed.
|
|
@@ -693,3 +707,10 @@ def ccxt_convert_timeframe_to_exchange_format(timeframe: str) -> str | None:
|
|
|
693
707
|
_t = re.match(r"(\d+)(\w+)", timeframe)
|
|
694
708
|
timeframe = f"{_t[1]}{_t[2][0].lower()}" if _t and len(_t.groups()) > 1 else timeframe
|
|
695
709
|
return timeframe
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def reject_cause_of(error: Exception) -> RejectCause:
|
|
713
|
+
"""
|
|
714
|
+
Classify a ccxt error for callers that cannot read venue codes. UNKNOWN when unmapped.
|
|
715
|
+
"""
|
|
716
|
+
return _REJECT_CAUSE_BY_CCXT_ERROR.get(type(error).__name__, RejectCause.UNKNOWN)
|
|
@@ -30,7 +30,8 @@ BALANCE_ABS_TOL = 0.01
|
|
|
30
30
|
|
|
31
31
|
# Maintenance margin re-marks with every snapshot's mark price, so RTOL-tight comparison
|
|
32
32
|
# emits a diff per position per poll. Only a material move (leverage/size change) is worth
|
|
33
|
-
# re-syncing; sub-0.5% (and sub-cent) drift is mark noise.
|
|
33
|
+
# re-syncing; sub-0.5% (and sub-cent) drift is mark noise. Measured over one live run: 175
|
|
34
|
+
# diffs, median $0.000052, only one above a cent — and that one was a real position change.
|
|
34
35
|
MARGIN_RTOL = 5e-3
|
|
35
36
|
|
|
36
37
|
|
|
@@ -19,6 +19,7 @@ import numpy as np
|
|
|
19
19
|
from qubx import area_logger
|
|
20
20
|
from qubx.core.account_manager.diffs import (
|
|
21
21
|
BalanceMismatch,
|
|
22
|
+
Diff,
|
|
22
23
|
Differ,
|
|
23
24
|
LocalOrderMissing,
|
|
24
25
|
LocalPositionMissing,
|
|
@@ -433,8 +434,30 @@ class Reconciler:
|
|
|
433
434
|
state.mark_snapshot_applied(snap.as_of)
|
|
434
435
|
changed = changed_positions if changed_positions is not None else []
|
|
435
436
|
actions: list[Action] = []
|
|
436
|
-
|
|
437
|
+
differences = self._differ.diff(state, snap)
|
|
438
|
+
# - a snapshot requested before a deal was booked cannot contain that deal, so letting it
|
|
439
|
+
# write the position rewinds a real fill. The decision is per instrument, not per diff:
|
|
440
|
+
# a size change always brings a margin change, and PositionFieldMismatch routes through
|
|
441
|
+
# reconcile_position_from_snapshot, which is authoritative for size and avg-price too.
|
|
442
|
+
# Measured LIGHTER 2026-08-28 (ENAUSDC 272.6 -> 181.8, 84ms after the deal booked) and
|
|
443
|
+
# BINANCE.UM (TRXUSDT 146 -> 127 against a snapshot 1.58s stale); on ALGOUSDT the
|
|
444
|
+
# executor sized its next clip off the rewound number and overshot by 16.9%.
|
|
445
|
+
stale_for = {
|
|
446
|
+
instr
|
|
447
|
+
for instr in {self._position_instrument(d) for d in differences}
|
|
448
|
+
if instr is not None
|
|
449
|
+
and (booked := state.get_position_deal_booked_at(instr)) is not None
|
|
450
|
+
and booked > snap.as_of
|
|
451
|
+
}
|
|
452
|
+
for instr in stale_for:
|
|
453
|
+
_log.info(
|
|
454
|
+
f"[{state.exchange}] reconcile: <y>{instr}</y> snapshot as_of={snap.as_of} predates a deal "
|
|
455
|
+
f"booked at {state.get_position_deal_booked_at(instr)} — keeping the local position"
|
|
456
|
+
)
|
|
457
|
+
for difference in differences:
|
|
437
458
|
_log.debug(difference.describe())
|
|
459
|
+
if self._position_instrument(difference) in stale_for:
|
|
460
|
+
continue
|
|
438
461
|
# - each atom is applied in isolation: a handler that raises (e.g. a venue/state
|
|
439
462
|
# surprise) must not sink the rest of the snapshot reconcile (balances/figures/
|
|
440
463
|
# other positions). Log and move on; the next snapshot re-derives the diff.
|
|
@@ -452,7 +475,7 @@ class Reconciler:
|
|
|
452
475
|
self._recover_order(state, snap_order, snap.as_of)
|
|
453
476
|
|
|
454
477
|
case OrderFieldMismatch(origin=snap_order):
|
|
455
|
-
if (ev := self._reconcile_order(state, snap_order)) is not None:
|
|
478
|
+
if (ev := self._reconcile_order(state, snap_order, now)) is not None:
|
|
456
479
|
actions.append(RouteEvent(ev))
|
|
457
480
|
|
|
458
481
|
# - size diff = missed deals
|
|
@@ -494,6 +517,18 @@ class Reconciler:
|
|
|
494
517
|
|
|
495
518
|
return actions + self._dispatch(SnapshotIn(snap), state, now)
|
|
496
519
|
|
|
520
|
+
@staticmethod
|
|
521
|
+
def _position_instrument(difference: Diff) -> Instrument | None:
|
|
522
|
+
"""
|
|
523
|
+
The instrument a position diff is about, or None for the order and balance diffs.
|
|
524
|
+
"""
|
|
525
|
+
match difference:
|
|
526
|
+
case PositionFieldMismatch(origin=pos):
|
|
527
|
+
return pos.instrument
|
|
528
|
+
case OriginalPositionMissing(position=pos) | LocalPositionMissing(position=pos):
|
|
529
|
+
return pos.instrument
|
|
530
|
+
return None
|
|
531
|
+
|
|
497
532
|
@staticmethod
|
|
498
533
|
def _recover_order(state: AccountState, snap_order: Order, as_of: np.datetime64) -> Order:
|
|
499
534
|
# - venue order absent locally: trust the producer-assigned origin (only the connector
|
|
@@ -592,20 +627,38 @@ class Reconciler:
|
|
|
592
627
|
)
|
|
593
628
|
return state.get_position(instrument)
|
|
594
629
|
|
|
595
|
-
def _reconcile_order(self, state: AccountState, snap_order: Order) -> Action | None:
|
|
630
|
+
def _reconcile_order(self, state: AccountState, snap_order: Order, now: np.datetime64) -> Action | None:
|
|
596
631
|
"""Apply a snapshot order's status/filled to local state; return the fill event to route.
|
|
597
632
|
|
|
598
|
-
Snapshot is authoritative for the order's own state.
|
|
599
|
-
|
|
633
|
+
Snapshot is authoritative for the order's own state. Skipped when the order is locally
|
|
634
|
+
terminal, and while a pending marker is still inside the confirm window — see
|
|
635
|
+
_pending_expired. For anything else the snapshot has to be venue-newer.
|
|
600
636
|
"""
|
|
601
637
|
local = state.get_active_order(snap_order.client_order_id)
|
|
602
|
-
if local is None or local.status.is_terminal
|
|
638
|
+
if local is None or local.status.is_terminal:
|
|
603
639
|
return None
|
|
604
|
-
if local.status.is_pending
|
|
640
|
+
if local.status.is_pending:
|
|
641
|
+
# - a pending marker carries the local clock while the snapshot carries the venue's, so
|
|
642
|
+
# _venue_newer cannot judge it: the confirm window decides instead
|
|
643
|
+
if not snap_order.status.is_terminal and not self._pending_expired(local, now):
|
|
644
|
+
return None
|
|
645
|
+
elif not self._venue_newer(snap_order, local):
|
|
605
646
|
return None
|
|
606
647
|
self._apply_order_snapshot(state, local, snap_order)
|
|
607
648
|
return self._fill_event(local)
|
|
608
649
|
|
|
650
|
+
def _pending_expired(self, local: Order, now: np.datetime64) -> bool:
|
|
651
|
+
"""
|
|
652
|
+
True once a pending marker has outlived the confirm window with the venue still holding the
|
|
653
|
+
order live — the cancel or update never reached it, so the snapshot has to win.
|
|
654
|
+
|
|
655
|
+
Inside the window the marker is defended: a snapshot fetched before the cancel landed shows
|
|
656
|
+
the order alive, and reverting then would undo a correct PENDING_CANCEL. Measured cancel
|
|
657
|
+
confirm on LIGHTER is 568 ms median, so the window is ~9x headroom. Both stamps are the
|
|
658
|
+
local clock — transition_order stamps a locally-driven transition with `now`.
|
|
659
|
+
"""
|
|
660
|
+
return local.last_update_time is not None and (now - local.last_update_time) >= self._order_confirm_wait # type: ignore
|
|
661
|
+
|
|
609
662
|
@staticmethod
|
|
610
663
|
def _venue_newer(snap: Order, local: Order) -> bool:
|
|
611
664
|
# - venue-vs-venue only (the connector stamps last_update_time on snapshot orders); no
|
|
@@ -280,6 +280,7 @@ def _handle_rejected(state: AccountState, event: OrderRejectedEvent, now: np.dat
|
|
|
280
280
|
return ApplyResult()
|
|
281
281
|
order.rejected_reason = event.reason
|
|
282
282
|
order.error_code = event.code
|
|
283
|
+
order.reject_cause = event.cause
|
|
283
284
|
order = transition(state, order.client_order_id, OrderStatus.REJECTED, now, update_time=event.last_update_time)
|
|
284
285
|
return ApplyResult(order=order, order_change=OrderChange.REJECTED)
|
|
285
286
|
|
|
@@ -300,7 +301,7 @@ def _reconciled_past(state: AccountState, instrument: Instrument, deal: Deal) ->
|
|
|
300
301
|
return watermark is not None and deal.time <= watermark
|
|
301
302
|
|
|
302
303
|
|
|
303
|
-
def _book_deal(state: AccountState, instrument: Instrument, deal: Deal) -> Position:
|
|
304
|
+
def _book_deal(state: AccountState, instrument: Instrument, deal: Deal, now: np.datetime64) -> Position:
|
|
304
305
|
"""Apply a deal's effect to the position and balances. Caller dedups first.
|
|
305
306
|
|
|
306
307
|
Futures/swap: credits realized PnL and debits the fee to the settle-currency
|
|
@@ -314,6 +315,8 @@ def _book_deal(state: AccountState, instrument: Instrument, deal: Deal) -> Posit
|
|
|
314
315
|
_before = pos.quantity
|
|
315
316
|
realized_pnl, fee = pos.update_position_by_deal(deal, state.conversion_rate(instrument))
|
|
316
317
|
logger.debug("deal {} amt={} {}->{} tid={}", instrument.symbol, deal.amount, _before, pos.quantity, deal.trade_id)
|
|
318
|
+
# - a position snapshot requested before this moment cannot contain this deal
|
|
319
|
+
state.mark_position_deal_booked(instrument, now)
|
|
317
320
|
if instrument.is_futures():
|
|
318
321
|
state.mark_cash_currency(instrument.settle)
|
|
319
322
|
# TODO(account-mgmt): fee is folded into settle here (correct when
|
|
@@ -387,11 +390,13 @@ def _handle_fill(state: AccountState, event: OrderFilledEvent, now: np.datetime6
|
|
|
387
390
|
if event.fill is not None
|
|
388
391
|
else None
|
|
389
392
|
)
|
|
390
|
-
position =
|
|
393
|
+
position = (
|
|
394
|
+
_book_deal(state, order.instrument, deal, now) if deal is not None and order.instrument is not None else None
|
|
395
|
+
)
|
|
391
396
|
# Terminal fill-gap: book executions the venue counted but never delivered as deals so
|
|
392
397
|
# position AND realized PnL converge now, not size-only at the next snapshot.
|
|
393
398
|
if (gap := _reconcile_fill_gap(state, order, event, now)) is not None and order.instrument is not None:
|
|
394
|
-
position = _book_deal(state, order.instrument, gap)
|
|
399
|
+
position = _book_deal(state, order.instrument, gap, now)
|
|
395
400
|
deal = deal or gap
|
|
396
401
|
order = transition(state, order.client_order_id, OrderStatus.FILLED, now, update_time=event.last_update_time)
|
|
397
402
|
return ApplyResult(order=order, order_change=OrderChange.FILLED, deal=deal, position=position)
|
|
@@ -410,7 +415,9 @@ def _handle_partial_fill(state: AccountState, event: OrderPartiallyFilledEvent,
|
|
|
410
415
|
if event.fill is not None
|
|
411
416
|
else None
|
|
412
417
|
)
|
|
413
|
-
position =
|
|
418
|
+
position = (
|
|
419
|
+
_book_deal(state, order.instrument, deal, now) if deal is not None and order.instrument is not None else None
|
|
420
|
+
)
|
|
414
421
|
# a pending cancel/update is resolved by the venue separately — don't disturb its status
|
|
415
422
|
pending = order.status.is_pending
|
|
416
423
|
if pending:
|
|
@@ -460,7 +467,7 @@ def _handle_deal(state: AccountState, event: DealEvent, now: np.datetime64) -> A
|
|
|
460
467
|
pos = state.ensure_position(order.instrument)
|
|
461
468
|
pos.update_position_by_deal(deal, state.conversion_rate(order.instrument), realize_only=True)
|
|
462
469
|
return ApplyResult(deal=deal, position=pos)
|
|
463
|
-
return ApplyResult(deal=deal, position=_book_deal(state, order.instrument, deal))
|
|
470
|
+
return ApplyResult(deal=deal, position=_book_deal(state, order.instrument, deal, now))
|
|
464
471
|
|
|
465
472
|
|
|
466
473
|
def _handle_updated(state: AccountState, event: OrderUpdatedEvent, now: np.datetime64) -> ApplyResult:
|
|
@@ -69,6 +69,7 @@ class AccountState:
|
|
|
69
69
|
"_applied_funding_buckets",
|
|
70
70
|
"_balance_push_as_of",
|
|
71
71
|
"_position_reconcile_as_of",
|
|
72
|
+
"_position_deal_booked_at",
|
|
72
73
|
)
|
|
73
74
|
|
|
74
75
|
def __init__(self, exchange: str, base_currency: str, *, terminal_history_size: int = 10_000):
|
|
@@ -117,6 +118,11 @@ class AccountState:
|
|
|
117
118
|
# - watermark: snapshot reconciled the size up to this venue time; a deal at/under it is
|
|
118
119
|
# already in that size → reducer records but doesn't re-book it
|
|
119
120
|
self._position_reconcile_as_of: dict[Instrument, np.datetime64] = {}
|
|
121
|
+
# - local clock of the most recent deal booked into the size, per instrument. A position
|
|
122
|
+
# snapshot requested before it cannot contain that deal, so it must not overwrite the
|
|
123
|
+
# size. Same domain as AccountSnapshot.as_of, which both connectors stamp before the
|
|
124
|
+
# fetch (ccxt connector.py:1708, lighter connector.py:487).
|
|
125
|
+
self._position_deal_booked_at: dict[Instrument, np.datetime64] = {}
|
|
120
126
|
|
|
121
127
|
def __repr__(self) -> str:
|
|
122
128
|
return (
|
|
@@ -179,6 +185,12 @@ class AccountState:
|
|
|
179
185
|
def get_balance_push_as_of(self, currency: str) -> np.datetime64 | None:
|
|
180
186
|
return self._balance_push_as_of.get(currency)
|
|
181
187
|
|
|
188
|
+
def get_position_deal_booked_at(self, instrument: Instrument) -> np.datetime64 | None:
|
|
189
|
+
return self._position_deal_booked_at.get(instrument)
|
|
190
|
+
|
|
191
|
+
def mark_position_deal_booked(self, instrument: Instrument, at: np.datetime64) -> None:
|
|
192
|
+
self._position_deal_booked_at[instrument] = at
|
|
193
|
+
|
|
182
194
|
def get_transition_counts(self) -> dict[str, int]:
|
|
183
195
|
"""Audit counter snapshot: status.value -> number of transitions into it."""
|
|
184
196
|
return dict(self._transition_counts)
|
|
@@ -457,9 +469,7 @@ class AccountState:
|
|
|
457
469
|
f"[{self.exchange}] reconcile: position <y>{snapshot.instrument}</y> from snapshot -> "
|
|
458
470
|
f"size {existing.quantity}->{snapshot.quantity} avg {existing.position_avg_price}->{snapshot.position_avg_price}"
|
|
459
471
|
)
|
|
460
|
-
existing.reconcile_size(
|
|
461
|
-
snapshot.quantity, snapshot.position_avg_price, timestamp=snapshot.last_update_time
|
|
462
|
-
)
|
|
472
|
+
existing.reconcile_size(snapshot.quantity, snapshot.position_avg_price, timestamp=snapshot.last_update_time)
|
|
463
473
|
|
|
464
474
|
# external margins first, so the mark refresh below skips recalculating them
|
|
465
475
|
if snapshot._maint_margin_external:
|
|
@@ -10,9 +10,10 @@ from typing import Any, Literal, TypeAlias
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
import pandas as pd
|
|
12
12
|
|
|
13
|
+
from qubx import logger
|
|
13
14
|
from qubx.core.exceptions import QueueTimeout
|
|
14
15
|
from qubx.core.series import Bar, OrderBook, Quote, Trade, time_as_nsec
|
|
15
|
-
from qubx.core.utils import prec_ceil, prec_floor, time_delta_to_str, time_to_str
|
|
16
|
+
from qubx.core.utils import add_in_lots, is_lot_multiple, prec_ceil, prec_floor, time_delta_to_str, time_to_str
|
|
16
17
|
from qubx.utils.clock import start_clock_discipline, time_now
|
|
17
18
|
from qubx.utils.misc import Stopwatch
|
|
18
19
|
from qubx.utils.time import to_timedelta
|
|
@@ -789,6 +790,25 @@ def resolve_reduce_only(options: dict[str, Any]) -> bool | None:
|
|
|
789
790
|
return None
|
|
790
791
|
|
|
791
792
|
|
|
793
|
+
class RejectCause(StrEnum):
|
|
794
|
+
"""
|
|
795
|
+
Why a venue refused an order, cancel or update, in terms a caller can act on.
|
|
796
|
+
|
|
797
|
+
``OrderRejectedEvent.code`` keeps the venue's own value — a ccxt exception class, a Lighter
|
|
798
|
+
numeric code — which is what a log reader wants. This is the portable reading of it, so a
|
|
799
|
+
strategy can say "give up on repeated INSUFFICIENT_MARGIN, keep retrying NOT_FILLABLE"
|
|
800
|
+
without a per-venue lookup table. Anything a connector does not map stays UNKNOWN.
|
|
801
|
+
"""
|
|
802
|
+
|
|
803
|
+
INSUFFICIENT_MARGIN = "INSUFFICIENT_MARGIN"
|
|
804
|
+
NOT_FILLABLE = "NOT_FILLABLE" # - post-only that would have crossed
|
|
805
|
+
RATE_LIMITED = "RATE_LIMITED"
|
|
806
|
+
TOO_SMALL = "TOO_SMALL" # - below min size / min notional
|
|
807
|
+
NOT_FOUND = "NOT_FOUND" # - cancel/update of an order the venue does not have
|
|
808
|
+
SEQUENCE = "SEQUENCE" # - venue-ordering refusal; the connector rewrites it
|
|
809
|
+
UNKNOWN = "UNKNOWN"
|
|
810
|
+
|
|
811
|
+
|
|
792
812
|
class OrderChange(StrEnum):
|
|
793
813
|
"""What happened to an order, paired with it on ApplyResult. Covers the cases
|
|
794
814
|
order.status can't express on its own: UPDATED (status unchanged), CANCEL_REJECTED/
|
|
@@ -870,6 +890,8 @@ class Order:
|
|
|
870
890
|
# venue/connector error code accompanying a reject (e.g. the ccxt error class name);
|
|
871
891
|
# None when the reject path carries no code (synthetic reconcile rejects).
|
|
872
892
|
error_code: str | None = None
|
|
893
|
+
# the portable reading of that code; UNKNOWN when the connector does not map it
|
|
894
|
+
reject_cause: RejectCause = RejectCause.UNKNOWN
|
|
873
895
|
reduce_only: bool = False
|
|
874
896
|
post_only: bool = False
|
|
875
897
|
# Defaults to FRAMEWORK (the common case); the snapshot/external materialization
|
|
@@ -1212,9 +1234,21 @@ class Position:
|
|
|
1212
1234
|
*,
|
|
1213
1235
|
realize_only: bool = False,
|
|
1214
1236
|
) -> tuple[float, float]:
|
|
1237
|
+
# - both operands are lot multiples, so add in whole lots: 28.2 + (-28.1) is 282 + (-281)
|
|
1238
|
+
# ticks, exactly one tick. Flooring the float sum loses a lot whenever subtraction leaves
|
|
1239
|
+
# the result a few ulp short of the grid, which is how a position reached 0.0 locally
|
|
1240
|
+
# while the venue still held one lot.
|
|
1241
|
+
lot = self.instrument.lot_size
|
|
1242
|
+
if not is_lot_multiple(self.quantity, lot) or not is_lot_multiple(amount, lot):
|
|
1243
|
+
# - stale instrument metadata or a connector fault. Reported, then booked to the
|
|
1244
|
+
# nearest lot: dropping the deal is the worse error.
|
|
1245
|
+
logger.error(
|
|
1246
|
+
f"[{self.instrument.symbol}] booking {self.quantity} + {amount} is not on the "
|
|
1247
|
+
f"lot grid (lot_size {lot}) — rounding to the nearest lot"
|
|
1248
|
+
)
|
|
1215
1249
|
return self.update_position(
|
|
1216
1250
|
timestamp,
|
|
1217
|
-
|
|
1251
|
+
add_in_lots(self.quantity, amount, lot),
|
|
1218
1252
|
exec_price,
|
|
1219
1253
|
fee_amount,
|
|
1220
1254
|
conversion_rate=conversion_rate,
|
|
@@ -15,6 +15,7 @@ from qubx.core.basics import (
|
|
|
15
15
|
OrderBook,
|
|
16
16
|
Position,
|
|
17
17
|
Quote,
|
|
18
|
+
RejectCause,
|
|
18
19
|
Trade,
|
|
19
20
|
)
|
|
20
21
|
|
|
@@ -71,6 +72,7 @@ class OrderAcceptedEvent(OrderEvent):
|
|
|
71
72
|
class OrderRejectedEvent(OrderEvent):
|
|
72
73
|
reason: str
|
|
73
74
|
code: str | None = None
|
|
75
|
+
cause: RejectCause = RejectCause.UNKNOWN
|
|
74
76
|
|
|
75
77
|
|
|
76
78
|
@msg
|
|
@@ -137,12 +139,14 @@ class OrderUpdatedEvent(OrderEvent):
|
|
|
137
139
|
class OrderCancelRejectedEvent(OrderEvent):
|
|
138
140
|
reason: str
|
|
139
141
|
code: str | None = None
|
|
142
|
+
cause: RejectCause = RejectCause.UNKNOWN
|
|
140
143
|
|
|
141
144
|
|
|
142
145
|
@msg
|
|
143
146
|
class OrderUpdateRejectedEvent(OrderEvent):
|
|
144
147
|
reason: str
|
|
145
148
|
code: str | None = None
|
|
149
|
+
cause: RejectCause = RejectCause.UNKNOWN
|
|
146
150
|
|
|
147
151
|
|
|
148
152
|
@msg
|
|
@@ -3,6 +3,8 @@ import numpy as np
|
|
|
3
3
|
cimport numpy as np
|
|
4
4
|
import pandas as pd
|
|
5
5
|
import datetime
|
|
6
|
+
from libc.float cimport DBL_EPSILON
|
|
7
|
+
from libc.math cimport ceil, copysign, fabs, floor, fmax, pow, round
|
|
6
8
|
|
|
7
9
|
NS = 1_000_000_000
|
|
8
10
|
|
|
@@ -82,9 +84,46 @@ cpdef recognize_timeframe(timeframe):
|
|
|
82
84
|
return tf
|
|
83
85
|
|
|
84
86
|
|
|
87
|
+
cdef inline double _snap_tick_noise(double x) noexcept:
|
|
88
|
+
"""
|
|
89
|
+
Snap x to the nearest integer when it is already within float noise of one. 0.29 * 100 is
|
|
90
|
+
28.999999999999996, which would otherwise floor to 28. The tolerance is relative because
|
|
91
|
+
float error scales with magnitude: 3.6e-15 at x=29, 3.7e-9 at x=2.9e7.
|
|
92
|
+
"""
|
|
93
|
+
cdef double nearest = round(x)
|
|
94
|
+
cdef double tolerance = 16.0 * DBL_EPSILON * fmax(1.0, fabs(x))
|
|
95
|
+
if fabs(x - nearest) <= tolerance:
|
|
96
|
+
return nearest
|
|
97
|
+
return x
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# - previous implementation rounded the SCALED value to `precision` decimals before the floor.
|
|
101
|
+
# At precision 0 that window is half a lot, so prec_floor(3.7, 0) gave 4.0 and
|
|
102
|
+
# prec_ceil(3.4, 0) gave 3.0 — neither was a floor or a ceil any more.
|
|
103
|
+
# return np.sign(a) * np.true_divide(np.ceil(round(abs(a) * 10**precision, precision)), 10**precision)
|
|
104
|
+
# return np.sign(a) * np.true_divide(np.floor(round(abs(a) * 10**precision, precision)), 10**precision)
|
|
85
105
|
cpdef double prec_ceil(double a, int precision):
|
|
86
|
-
|
|
106
|
+
cdef double scale = pow(10.0, precision)
|
|
107
|
+
cdef double ticks = _snap_tick_noise(fabs(a) * scale)
|
|
108
|
+
return copysign(ceil(ticks) / scale, a)
|
|
87
109
|
|
|
88
110
|
|
|
89
111
|
cpdef double prec_floor(double a, int precision):
|
|
90
|
-
|
|
112
|
+
cdef double scale = pow(10.0, precision)
|
|
113
|
+
cdef double ticks = _snap_tick_noise(fabs(a) * scale)
|
|
114
|
+
return copysign(floor(ticks) / scale, a)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
cpdef double add_in_lots(double quantity, double amount, double lot_size) noexcept:
|
|
118
|
+
"""
|
|
119
|
+
Sum two lot multiples in whole lots, so a subtraction cannot land a few ulp off the grid.
|
|
120
|
+
"""
|
|
121
|
+
return (round(quantity / lot_size) + round(amount / lot_size)) * lot_size
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
cpdef bint is_lot_multiple(double quantity, double lot_size) noexcept:
|
|
125
|
+
"""
|
|
126
|
+
True when quantity sits on the lot grid, within the float noise of one division.
|
|
127
|
+
"""
|
|
128
|
+
cdef double raw = quantity / lot_size
|
|
129
|
+
return fabs(raw - round(raw)) <= 16.0 * DBL_EPSILON * fmax(1.0, fabs(raw))
|
|
@@ -287,12 +287,14 @@ class TextualStrategyApp(App[None]):
|
|
|
287
287
|
with Vertical(id="tables-container", classes="tables-column"):
|
|
288
288
|
# Positions panel
|
|
289
289
|
self.positions_panel = Vertical(id="positions-panel", classes="side-panel")
|
|
290
|
+
self.positions_panel.border_title = "Positions"
|
|
290
291
|
with self.positions_panel:
|
|
291
292
|
self.positions_table = PositionsTable(id="positions-table")
|
|
292
293
|
self.positions_table.setup_columns()
|
|
293
294
|
yield self.positions_table
|
|
294
295
|
# Orders panel
|
|
295
296
|
self.orders_panel = Vertical(id="orders-panel", classes="side-panel")
|
|
297
|
+
self.orders_panel.border_title = "Orders"
|
|
296
298
|
with self.orders_panel:
|
|
297
299
|
self.orders_table = OrdersTable(id="orders-table")
|
|
298
300
|
self.orders_table.setup_columns()
|
|
@@ -109,6 +109,14 @@ class OrdersTable(DataTable):
|
|
|
109
109
|
except Exception as e:
|
|
110
110
|
logger.warning(f"Failed to add order row {order_id}: {e}")
|
|
111
111
|
|
|
112
|
+
self._retitle(len(new_orders))
|
|
113
|
+
|
|
114
|
+
def _retitle(self, active_orders: int) -> None:
|
|
115
|
+
# - the border lives on the containing panel, so the count goes there
|
|
116
|
+
panel = self.parent
|
|
117
|
+
if panel is not None:
|
|
118
|
+
panel.border_title = f"Orders ({active_orders})"
|
|
119
|
+
|
|
112
120
|
def _get_price_precision(self, row: dict) -> int:
|
|
113
121
|
"""Get price precision for formatting (default to 2 decimals)."""
|
|
114
122
|
# Could be enhanced to use instrument precision if available
|
|
@@ -142,6 +142,13 @@ class PositionsTable(DataTable):
|
|
|
142
142
|
logger.warning(f"Failed to add position row {pos_key}: {e}")
|
|
143
143
|
|
|
144
144
|
self._sort()
|
|
145
|
+
self._retitle(sum(1 for r in sanitized_rows if r.get("qty")))
|
|
146
|
+
|
|
147
|
+
def _retitle(self, open_positions: int) -> None:
|
|
148
|
+
# - the border lives on the containing panel, so the count goes there
|
|
149
|
+
panel = self.parent
|
|
150
|
+
if panel is not None:
|
|
151
|
+
panel.border_title = f"Positions ({open_positions})"
|
|
145
152
|
|
|
146
153
|
def _get_side(self, r: dict) -> str:
|
|
147
154
|
leverage = r.get("leverage", 0.0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|