Qubx 3.6.1__tar.gz → 3.6.2.dev1__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.1 → qubx-3.6.2.dev1}/PKG-INFO +1 -1
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/_version.py +2 -2
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/commands.py +6 -1
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/diffs.py +10 -1
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/state.py +9 -1
- qubx-3.6.2.dev1/src/qubx/core/boot.py +103 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/fit_executor.py +3 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/initializer.py +7 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/interfaces.py +16 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/processing.py +142 -75
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restarts/state_resolvers.py +55 -36
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/runner.py +0 -4
- {qubx-3.6.1 → qubx-3.6.2.dev1}/.gitignore +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/LICENSE +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/README.md +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/pyproject.toml +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/scripts/build.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/_nb_magic.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/connector.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/data.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/iteratedstream.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/management.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/ome.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/optimization.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/runner.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/simulator.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/transfers.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/backtester/utils.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/deploy.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/misc.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/release.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/resolver.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/s3.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/theme.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/tui.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/cli/user_config.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/config.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/connector.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/_two_stream.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/binance/connector.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/bitfinex/connector.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/gateio/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/gateio/gateio.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/connector.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/exchanges/okx/okx.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/plugin.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/rate_limits.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/plugin.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/registry.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/tardis/plugin.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/builtin.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/decorator.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/executor.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/interfaces.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/server.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/control/types.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/config.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/manager.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/reconciler.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/reducer.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/account_manager/state_machine.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/basics.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/connector.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/context.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/detectors/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/detectors/delisting.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/detectors/stale.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/errors.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/events.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/exceptions.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/fit_context.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/helpers.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/instrument_service.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/loggers.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/lookups.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/metrics.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/instrument_service.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/market.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/series.pxd +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/series.pyi +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/series.pyx +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/status.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/utils.pyi +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/core/utils.pyx +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/cache.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/containers.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/guards.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/registry.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storage.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/ccxt.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/csv.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/handy.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/multi.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/questdb.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/stub.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/storages/utils.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/data/transformers.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/base.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/composite.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/csv.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/indicator.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/emitters/questdb.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/composite.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/exporters/slack.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/gathering/simplest.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/health/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/health/base.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/health/dummy.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/loggers/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/loggers/csv.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/loggers/factory.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/loggers/mongo.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/loggers/postgres.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/notifications/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/notifications/composite.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/notifications/slack.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/notifications/throttler.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/pandaz/stats.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/pandaz/ta.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/pandaz/utils.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/plugins/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/plugins/loader.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/backend.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/config.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/engine.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/ip_resolver.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/manager.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/pools.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/redis_backend.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/rate_limiting/resilient.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/_build.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restarts/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/balance.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/factory.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/position.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/signal.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/state.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/restorers/utils.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/state/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/state/dummy.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/state/redis.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/state/safe.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/ta/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/base.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/project/template.yml +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.claude/skills/qubx-cli/SKILL.md +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.claude/skills/qubx-indicators/SKILL.md +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.claude/skills/simulation-explorer/SKILL.md +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.claude/skills/strategy-release/SKILL.md +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.github/workflows/ci.yml.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.gitignore.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.python-version +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.vscode/launch.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.vscode/settings.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/.vscode/tasks.json +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/CLAUDE.md.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/README.md.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/configs/.gitkeep +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/justfile.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/pyproject.toml.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/research/.gitkeep +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/src/{{ strategy_name }}/cli.py.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/template.yml +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/repo/tests/test_strategy.py.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/trackers/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/trackers/advanced.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/trackers/composite.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/trackers/sizers.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/clock.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/collections.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/hft/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/hft/numba_utils.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/hft/orderbook.pyi +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/hft/orderbook.pyx +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/misc.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/nonce.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/orderbook.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/questdb.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/rate_limiter.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/results.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/ringbuffer.pxd +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/ringbuffer.pyi +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/ringbuffer.pyx +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/app.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/handlers.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/init_code.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/styles.tcss +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/account_summary.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/log_filter.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/s3.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/slack.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/threading.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/throttler.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/time.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/src/qubx/utils/websocket_manager.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/tests/strategies/macd_crossover/src/macd_crossover/indicators/macd.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/tests/strategies/macd_crossover/src/macd_crossover/models/macd_crossover.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/tests/strategies/macd_crossover/src/macd_crossover/models/utils.py +0 -0
- {qubx-3.6.1 → qubx-3.6.2.dev1}/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.6.
|
|
22
|
-
__version_tuple__ = version_tuple = (3, 6,
|
|
21
|
+
__version__ = version = '3.6.2.dev1'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 6, 2, 'dev1')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -332,6 +332,9 @@ def run(
|
|
|
332
332
|
help="Write simulation logs to a specific file path.",
|
|
333
333
|
show_default=True,
|
|
334
334
|
)
|
|
335
|
+
@click.option(
|
|
336
|
+
"--dev", is_flag=True, default=False, help="Enable dev mode (adds ~/projects to path).", show_default=True
|
|
337
|
+
)
|
|
335
338
|
def simulate(
|
|
336
339
|
config_file: Path,
|
|
337
340
|
start: str | None,
|
|
@@ -340,6 +343,7 @@ def simulate(
|
|
|
340
343
|
report: str | None,
|
|
341
344
|
name: str | None,
|
|
342
345
|
log_file: str | None,
|
|
346
|
+
dev: bool,
|
|
343
347
|
):
|
|
344
348
|
"""
|
|
345
349
|
Simulates the strategy with the given configuration file.
|
|
@@ -347,7 +351,8 @@ def simulate(
|
|
|
347
351
|
from qubx.utils.misc import add_project_to_system_path, logo
|
|
348
352
|
from qubx.utils.runner.runner import simulate_strategy
|
|
349
353
|
|
|
350
|
-
|
|
354
|
+
if dev:
|
|
355
|
+
add_project_to_system_path() # Adds ~/projects in dev mode
|
|
351
356
|
add_project_to_system_path(str(config_file.parent))
|
|
352
357
|
logo()
|
|
353
358
|
logger.info(f"Process PID: <g>{os.getpid()}</g>")
|
|
@@ -28,6 +28,11 @@ RTOL = 1e-9
|
|
|
28
28
|
# under 1 cent is not a reconcilable difference. Absolute, because "1 cent" is absolute.
|
|
29
29
|
BALANCE_ABS_TOL = 0.01
|
|
30
30
|
|
|
31
|
+
# Maintenance margin re-marks with every snapshot's mark price, so RTOL-tight comparison
|
|
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.
|
|
34
|
+
MARGIN_RTOL = 5e-3
|
|
35
|
+
|
|
31
36
|
|
|
32
37
|
# - Diff atoms ------------------------------------------------------------------ #
|
|
33
38
|
@dataclass_transform(frozen_default=True, kw_only_default=True)
|
|
@@ -69,6 +74,10 @@ def _close_bal(a: float, b: float) -> bool:
|
|
|
69
74
|
return abs(a - b) <= BALANCE_ABS_TOL
|
|
70
75
|
|
|
71
76
|
|
|
77
|
+
def _close_margin(a: float, b: float) -> bool:
|
|
78
|
+
return abs(a - b) <= max(BALANCE_ABS_TOL, MARGIN_RTOL * max(abs(a), abs(b)))
|
|
79
|
+
|
|
80
|
+
|
|
72
81
|
def _material_pos(position: Position) -> bool:
|
|
73
82
|
# a position smaller than half a lot is effectively flat — never a presence diff
|
|
74
83
|
return abs(position.quantity) > _qeps(position.instrument)
|
|
@@ -377,7 +386,7 @@ class Differ:
|
|
|
377
386
|
if abs(lp.position_avg_price - sp.position_avg_price) > _peps(sp.instrument):
|
|
378
387
|
diffs.append(PositionAvgPriceMismatch(local=lp, origin=sp))
|
|
379
388
|
|
|
380
|
-
if not
|
|
389
|
+
if not _close_margin(lp.maint_margin, sp.maint_margin):
|
|
381
390
|
diffs.append(PositionMarginMismatch(local=lp, origin=sp))
|
|
382
391
|
|
|
383
392
|
for inst, lp in local_pos.items():
|
|
@@ -26,6 +26,10 @@ from qubx.core.basics import STABLE_CURRENCIES, Balance, Deal, Instrument, Order
|
|
|
26
26
|
# sessions. A re-delivered funding event only needs RECENT buckets to dedup against.
|
|
27
27
|
_FUNDING_BUCKET_CAP: int = 4096
|
|
28
28
|
|
|
29
|
+
# Snapshot balance-apply log level: a total move above this is real money (fill/funding/
|
|
30
|
+
# transfer) and logs INFO; below it (incl. pure free/locked reshuffles) logs DEBUG.
|
|
31
|
+
_BALANCE_LOG_ABS_TOL: float = 0.01
|
|
32
|
+
|
|
29
33
|
|
|
30
34
|
@dataclass
|
|
31
35
|
class VenueAccountFigures:
|
|
@@ -527,7 +531,11 @@ class AccountState:
|
|
|
527
531
|
)
|
|
528
532
|
if changed:
|
|
529
533
|
old = "—" if existing is None else f"total={existing.total} free={existing.free} locked={existing.locked}"
|
|
530
|
-
|
|
534
|
+
# - free/locked reshuffle with total unchanged is margin mark-to-market noise
|
|
535
|
+
# (every poll on a leveraged book); only a real total move is INFO-worthy
|
|
536
|
+
material = existing is None or abs(balance.total - existing.total) > _BALANCE_LOG_ABS_TOL
|
|
537
|
+
log = logger.info if material else logger.debug
|
|
538
|
+
log(
|
|
531
539
|
f"[{self.exchange}] reconcile: balance <y>{balance.currency}</y> from snapshot -> "
|
|
532
540
|
f"total=<g>{balance.total}</g> free={balance.free} locked={balance.locked} (was {old})"
|
|
533
541
|
)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
from qubx import logger
|
|
4
|
+
from qubx.core.basics import dt_64, td_64
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BootPhase(IntEnum):
|
|
8
|
+
BLOCKED = -1
|
|
9
|
+
WAIT_READY = 0
|
|
10
|
+
ON_START = 1
|
|
11
|
+
RESOLVE = 2
|
|
12
|
+
RESTORE = 3
|
|
13
|
+
WARMUP_FINISHED = 4
|
|
14
|
+
BOOT_FIT = 5
|
|
15
|
+
TRADING = 6
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BootStateMachine:
|
|
19
|
+
"""Live-boot phase bookkeeping: current phase, boot-fit retry policy, health gauges.
|
|
20
|
+
|
|
21
|
+
Pure state — the ProcessingManager drives transitions and executes side effects
|
|
22
|
+
(see _advance_boot). BLOCKED is sticky for trading; a later successful fit releases
|
|
23
|
+
it (record_fit_success), a late account snapshot releases WAIT_READY (caller-side).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, health_monitor, *, fit_max_attempts: int = 3, fit_retry_delay: td_64 = td_64(60, "s")) -> None:
|
|
27
|
+
self.phase = BootPhase.WAIT_READY
|
|
28
|
+
self.blocked_reason: str | None = None
|
|
29
|
+
self.fit_attempts = 0
|
|
30
|
+
self._health = health_monitor
|
|
31
|
+
self._fit_max_attempts = fit_max_attempts
|
|
32
|
+
self._fit_retry_delay = fit_retry_delay
|
|
33
|
+
self._next_fit_attempt: dt_64 | None = None
|
|
34
|
+
self._sync_alerted = False
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def is_trading(self) -> bool:
|
|
38
|
+
return self.phase == BootPhase.TRADING
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def is_blocked(self) -> bool:
|
|
42
|
+
return self.phase == BootPhase.BLOCKED
|
|
43
|
+
|
|
44
|
+
def advance(self, phase: BootPhase) -> None:
|
|
45
|
+
if phase == self.phase:
|
|
46
|
+
return
|
|
47
|
+
logger.info(f"<yellow>boot</yellow> :: {self.phase.name} -> <green>{phase.name}</green>")
|
|
48
|
+
self.phase = phase
|
|
49
|
+
self._gauge("boot.state", float(phase))
|
|
50
|
+
|
|
51
|
+
def account_sync_alert(self) -> None:
|
|
52
|
+
if self._sync_alerted:
|
|
53
|
+
return
|
|
54
|
+
self._sync_alerted = True
|
|
55
|
+
self._gauge("boot.account_sync_blocked", 1.0)
|
|
56
|
+
logger.warning("<yellow>Boot blocked: initial account snapshot not applied — holding until synced</yellow>")
|
|
57
|
+
|
|
58
|
+
def account_synced(self) -> None:
|
|
59
|
+
if not self._sync_alerted:
|
|
60
|
+
return
|
|
61
|
+
self._sync_alerted = False
|
|
62
|
+
self._gauge("boot.account_sync_blocked", 0.0)
|
|
63
|
+
|
|
64
|
+
def fit_attempt_allowed(self, now: dt_64) -> bool:
|
|
65
|
+
if self.phase != BootPhase.BOOT_FIT:
|
|
66
|
+
return False
|
|
67
|
+
return self._next_fit_attempt is None or now >= self._next_fit_attempt
|
|
68
|
+
|
|
69
|
+
def record_fit_attempt(self) -> None:
|
|
70
|
+
self.fit_attempts += 1
|
|
71
|
+
self._gauge("boot.fit_attempts", float(self.fit_attempts))
|
|
72
|
+
|
|
73
|
+
def record_fit_failure(self, now: dt_64) -> None:
|
|
74
|
+
if self.is_blocked:
|
|
75
|
+
return
|
|
76
|
+
if self.fit_attempts >= self._fit_max_attempts:
|
|
77
|
+
self.blocked_reason = "boot fit failed"
|
|
78
|
+
self.advance(BootPhase.BLOCKED)
|
|
79
|
+
self._gauge("boot.fit_failed", 1.0)
|
|
80
|
+
logger.error(
|
|
81
|
+
f"<red>Boot fit failed after {self.fit_attempts} attempts — trading blocked, "
|
|
82
|
+
"book unreconciled; a later successful fit will unblock</red>"
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
self._next_fit_attempt = now + self._fit_retry_delay
|
|
86
|
+
logger.warning(
|
|
87
|
+
f"<yellow>Boot fit failed (attempt {self.fit_attempts}/{self._fit_max_attempts}) — retrying</yellow>"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def record_fit_success(self) -> None:
|
|
91
|
+
if self.is_blocked:
|
|
92
|
+
self._gauge("boot.fit_failed", 0.0)
|
|
93
|
+
self.blocked_reason = None
|
|
94
|
+
self.advance(BootPhase.TRADING)
|
|
95
|
+
|
|
96
|
+
def record_warmup_finished_failure(self) -> None:
|
|
97
|
+
self._gauge("boot.warmup_finished_failed", 1.0)
|
|
98
|
+
|
|
99
|
+
def _gauge(self, name: str, value: float) -> None:
|
|
100
|
+
try:
|
|
101
|
+
self._health.record_gauge(name, value)
|
|
102
|
+
except Exception:
|
|
103
|
+
logger.exception(f"failed to record {name} gauge")
|
|
@@ -47,11 +47,14 @@ class FitCommitData:
|
|
|
47
47
|
order (they run on the ProcessorThread, where the fit-thread interception no
|
|
48
48
|
longer triggers, so each takes today's normal path). ``signals`` are the
|
|
49
49
|
fit-emitted signals, drained into the normal pipeline in emission order.
|
|
50
|
+
``error`` carries whatever made the fit fail (strategy raise or framework raise):
|
|
51
|
+
the ProcessorThread latches ``is_on_fit_called`` only when it is None.
|
|
50
52
|
"""
|
|
51
53
|
|
|
52
54
|
ops: tuple[Callable[[], None], ...] = ()
|
|
53
55
|
signals: tuple[Signal, ...] = ()
|
|
54
56
|
duration_s: float = 0.0
|
|
57
|
+
error: BaseException | None = None
|
|
55
58
|
|
|
56
59
|
|
|
57
60
|
class FitCycleState:
|
|
@@ -37,6 +37,7 @@ class BasicStrategyInitializer(IStrategyInitializer):
|
|
|
37
37
|
default_instrument_leverage: float | None = None
|
|
38
38
|
start_time_finder: StartTimeFinderProtocol | None = None
|
|
39
39
|
mismatch_resolver: StateResolverProtocol | None = None
|
|
40
|
+
_fit_on_start: bool = False
|
|
40
41
|
auto_subscribe: bool | None = None
|
|
41
42
|
simulation: bool | None = None
|
|
42
43
|
subscription_warmup: dict[Any, str] | None = None
|
|
@@ -131,6 +132,12 @@ class BasicStrategyInitializer(IStrategyInitializer):
|
|
|
131
132
|
def set_state_resolver(self, resolver: StateResolverProtocol) -> None:
|
|
132
133
|
self.mismatch_resolver = resolver
|
|
133
134
|
|
|
135
|
+
def set_fit_on_start(self, enabled: bool = True) -> None:
|
|
136
|
+
self._fit_on_start = enabled
|
|
137
|
+
|
|
138
|
+
def get_fit_on_start(self) -> bool:
|
|
139
|
+
return self._fit_on_start
|
|
140
|
+
|
|
134
141
|
def set_config(self, key: str, value: Any) -> None:
|
|
135
142
|
self.config[key] = value
|
|
136
143
|
|
|
@@ -2311,6 +2311,22 @@ class IStrategyInitializer:
|
|
|
2311
2311
|
"""
|
|
2312
2312
|
...
|
|
2313
2313
|
|
|
2314
|
+
def set_fit_on_start(self, enabled: bool = True) -> None:
|
|
2315
|
+
"""
|
|
2316
|
+
Force the first on_fit to run in the live context even when a warmup-sim fit ran.
|
|
2317
|
+
|
|
2318
|
+
The warmup simulation fits against sim state; strategies that reconcile positions
|
|
2319
|
+
through their tracker on fit (e.g. buffered trackers) need the first fit to run
|
|
2320
|
+
live, against live positions. Opting in guarantees exactly one live fit after
|
|
2321
|
+
on_warmup_finished and replaces calling ctx.trigger_fit() there (doing both
|
|
2322
|
+
double-fits). No-op when no warmup sim runs — the first fit is live anyway.
|
|
2323
|
+
"""
|
|
2324
|
+
...
|
|
2325
|
+
|
|
2326
|
+
def get_fit_on_start(self) -> bool:
|
|
2327
|
+
"""Whether the first on_fit is forced to run in the live context (default False)."""
|
|
2328
|
+
...
|
|
2329
|
+
|
|
2314
2330
|
def set_config(self, key: str, value: Any) -> None:
|
|
2315
2331
|
"""
|
|
2316
2332
|
Set an additional configuration value.
|
|
@@ -28,6 +28,7 @@ from qubx.core.basics import (
|
|
|
28
28
|
dt_64,
|
|
29
29
|
td_64,
|
|
30
30
|
)
|
|
31
|
+
from qubx.core.boot import BootPhase, BootStateMachine
|
|
31
32
|
from qubx.core.connector import IConnector, IMarketDataSink
|
|
32
33
|
from qubx.core.detectors import DelistingDetector, StaleDataDetector
|
|
33
34
|
from qubx.core.errors import BaseErrorEvent
|
|
@@ -59,6 +60,7 @@ from qubx.core.interfaces import (
|
|
|
59
60
|
)
|
|
60
61
|
from qubx.core.loggers import StrategyLogging
|
|
61
62
|
from qubx.core.series import Bar, OrderBook, Quote, Trade
|
|
63
|
+
from qubx.restarts.state_resolvers import StateResolver
|
|
62
64
|
from qubx.state.dummy import DummyStatePersistence
|
|
63
65
|
from qubx.trackers.riskctrl import _InitializationStageTracker
|
|
64
66
|
from qubx.utils.throttler import InstrumentThrottler
|
|
@@ -165,7 +167,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
165
167
|
_last_data_ready_log_time: dt_64 | None = None
|
|
166
168
|
_all_instruments_ready_logged: bool = False
|
|
167
169
|
_account_sync_deadline: dt_64 | None = None # - startup: bound the wait for the initial snapshot
|
|
168
|
-
|
|
170
|
+
_boot: BootStateMachine
|
|
169
171
|
_emitted_signals: list[Signal] = [] # signals that were emitted
|
|
170
172
|
_active_targets: dict[Instrument, TargetPosition] = {}
|
|
171
173
|
_pending_no_quote_signals: dict[Instrument, Signal] = {} # signals waiting for quote to arrive
|
|
@@ -240,8 +242,8 @@ class ProcessingManager(IProcessingManager):
|
|
|
240
242
|
self._last_data_ready_log_time = None
|
|
241
243
|
self._all_instruments_ready_logged = False
|
|
242
244
|
self._account_sync_deadline = None
|
|
243
|
-
self._account_sync_timeout_logged = False
|
|
244
245
|
self._emitted_signals = []
|
|
246
|
+
self._boot = BootStateMachine(self._health_monitor)
|
|
245
247
|
|
|
246
248
|
# - special tracker for post-warmup initialization signals
|
|
247
249
|
self._init_stage_position_tracker = _InitializationStageTracker()
|
|
@@ -497,48 +499,14 @@ class ProcessingManager(IProcessingManager):
|
|
|
497
499
|
return self._run_strategy_pipeline(event)
|
|
498
500
|
|
|
499
501
|
def _run_strategy_pipeline(self, event: Any) -> bool:
|
|
500
|
-
"""
|
|
502
|
+
"""Boot advance, strategy callback firing and signal processing — the
|
|
501
503
|
shared tail of the tuple data path (__process_data)."""
|
|
502
|
-
if not self.
|
|
503
|
-
self._handle_start()
|
|
504
|
-
|
|
505
|
-
if (
|
|
506
|
-
not self._context._strategy_state.is_on_warmup_finished_called
|
|
507
|
-
and not self._context._strategy_state.is_warmup_in_progress
|
|
508
|
-
and not self._warmup_finished_is_running
|
|
509
|
-
):
|
|
510
|
-
if self._context.get_warmup_positions() or self._context.get_warmup_orders():
|
|
511
|
-
self._handle_state_resolution()
|
|
512
|
-
|
|
513
|
-
# Restore tracker and gatherer state if available
|
|
514
|
-
restored_state = self._context.get_restored_state()
|
|
515
|
-
if restored_state is not None:
|
|
516
|
-
self._restore_tracker_and_gatherer_state(restored_state)
|
|
517
|
-
|
|
518
|
-
self._handle_warmup_finished()
|
|
519
|
-
|
|
520
|
-
# - check if it still didn't call on_fit() for first time
|
|
521
|
-
if (
|
|
522
|
-
not self._context._strategy_state.is_on_fit_called
|
|
523
|
-
and not self._fit_is_running
|
|
524
|
-
and not self._warmup_finished_is_running
|
|
525
|
-
):
|
|
526
|
-
self._handle_fit(None, "fit", (None, self._time_provider.time()))
|
|
504
|
+
if not self._advance_boot():
|
|
527
505
|
return False
|
|
528
506
|
|
|
529
507
|
if not event and not self._emitted_signals:
|
|
530
508
|
return False
|
|
531
509
|
|
|
532
|
-
# - if fit was not called - skip on_event call
|
|
533
|
-
if not self._context._strategy_state.is_on_fit_called or (
|
|
534
|
-
not self._is_simulation and not self._context._strategy_state.is_on_warmup_finished_called
|
|
535
|
-
):
|
|
536
|
-
return False
|
|
537
|
-
|
|
538
|
-
# - warmup-finished still running (inline): skip on_event call
|
|
539
|
-
if self._warmup_finished_is_running:
|
|
540
|
-
return False
|
|
541
|
-
|
|
542
510
|
# - strategy still fitting: inline mode (and simulation) skips the strategy
|
|
543
511
|
# callbacks — the fit owns the thread, this is a re-entrancy guard. Thread
|
|
544
512
|
# mode DELIVERS instead: handlers run here on the ProcessorThread concurrently
|
|
@@ -730,7 +698,8 @@ class ProcessingManager(IProcessingManager):
|
|
|
730
698
|
self._time_provider.time(), signals, self._account_manager, _targets_from_trackers
|
|
731
699
|
)
|
|
732
700
|
|
|
733
|
-
def __invoke_on_fit(self) ->
|
|
701
|
+
def __invoke_on_fit(self) -> bool:
|
|
702
|
+
"""Run on_fit inline. True ⇔ it completed without raising."""
|
|
734
703
|
with self._health_monitor("ctx.on_fit"):
|
|
735
704
|
try:
|
|
736
705
|
# - enforce the blacklist before fitting: refresh the cache AND force-close
|
|
@@ -743,15 +712,21 @@ class ProcessingManager(IProcessingManager):
|
|
|
743
712
|
self._strategy.on_fit(self._context)
|
|
744
713
|
self._subscription_manager.commit() # apply pending operations
|
|
745
714
|
logger.debug(f"[<y>{self.__class__.__name__}</y>] :: <g>{self._strategy_name}</g> is fitted")
|
|
715
|
+
self._context._strategy_state.is_on_fit_called = True
|
|
716
|
+
return True
|
|
746
717
|
except Exception as strat_error:
|
|
747
718
|
logger.error(
|
|
748
719
|
f"[{self.__class__.__name__}] :: Strategy {self._strategy_name} on_fit raised an exception: {strat_error}"
|
|
749
720
|
)
|
|
750
721
|
logger.opt(colors=False).error(traceback.format_exc())
|
|
751
|
-
|
|
752
|
-
|
|
722
|
+
# - simulation keeps the latch: no retry machinery there, and a raising fit
|
|
723
|
+
# must not re-fire on every tick
|
|
724
|
+
if self._is_simulation:
|
|
725
|
+
self._context._strategy_state.is_on_fit_called = True
|
|
726
|
+
return False
|
|
753
727
|
|
|
754
|
-
def __invoke_on_warmup_finished(self) ->
|
|
728
|
+
def __invoke_on_warmup_finished(self) -> bool:
|
|
729
|
+
"""Run on_warmup_finished inline. True ⇔ it completed without raising."""
|
|
755
730
|
with self._health_monitor("ctx.on_warmup_finished"):
|
|
756
731
|
try:
|
|
757
732
|
logger.debug(
|
|
@@ -762,12 +737,15 @@ class ProcessingManager(IProcessingManager):
|
|
|
762
737
|
logger.debug(
|
|
763
738
|
f"[<y>{self.__class__.__name__}</y>] :: <g>{self._strategy_name}</g> warmup finished completed"
|
|
764
739
|
)
|
|
740
|
+
return True
|
|
765
741
|
except Exception as strat_error:
|
|
766
742
|
logger.error(
|
|
767
743
|
f"[{self.__class__.__name__}] :: Strategy {self._strategy_name} on_warmup_finished raised an exception: {strat_error}"
|
|
768
744
|
)
|
|
769
745
|
logger.opt(colors=False).error(traceback.format_exc())
|
|
746
|
+
return False
|
|
770
747
|
finally:
|
|
748
|
+
# - the hook is not guaranteed idempotent: latch it either way, no auto-retry
|
|
771
749
|
self._context._strategy_state.is_on_warmup_finished_called = True
|
|
772
750
|
|
|
773
751
|
def __preprocess_and_log_target_positions(self, target_positions: list[TargetPosition]) -> list[TargetPosition]:
|
|
@@ -921,27 +899,25 @@ class ProcessingManager(IProcessingManager):
|
|
|
921
899
|
Gates on_start / on_fit / state-resolution / restore so those callbacks see REAL capital +
|
|
922
900
|
venue positions instead of the pre-sync zero state (which would produce 0-capital sizing and
|
|
923
901
|
transiently-flat restored positions). No account requirement in simulation (no venue snapshot).
|
|
924
|
-
|
|
925
|
-
|
|
902
|
+
Never falls through: trading against phantom-zero positions can double-open the book.
|
|
903
|
+
ACCOUNT_SYNC_TIMEOUT only arms an alert (once), the boot keeps holding until synced.
|
|
926
904
|
"""
|
|
927
905
|
if not self._is_data_ready():
|
|
928
906
|
return False
|
|
929
907
|
# - simulation & paper have a locally-seeded account (no venue snapshot to wait for)
|
|
930
|
-
if self._context.is_simulation or self._context.is_paper_trading
|
|
908
|
+
if self._context.is_simulation or self._context.is_paper_trading:
|
|
931
909
|
return True
|
|
932
|
-
|
|
910
|
+
if self._account_manager.is_synced():
|
|
911
|
+
self._boot.account_synced()
|
|
912
|
+
return True
|
|
913
|
+
# - data ready but the initial venue snapshot hasn't applied: hold the boot.
|
|
914
|
+
# ACCOUNT_SYNC_TIMEOUT is the alert threshold, not a fall-through — trading
|
|
915
|
+
# against phantom-zero positions can double-open the book.
|
|
933
916
|
now = self._time_provider.time()
|
|
934
917
|
if self._account_sync_deadline is None:
|
|
935
918
|
self._account_sync_deadline = now + self.ACCOUNT_SYNC_TIMEOUT
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
if not self._account_sync_timeout_logged:
|
|
939
|
-
logger.warning(
|
|
940
|
-
"Initial account snapshot not applied within timeout — starting without it; "
|
|
941
|
-
"capital/positions may be incomplete until the next reconcile"
|
|
942
|
-
)
|
|
943
|
-
self._account_sync_timeout_logged = True
|
|
944
|
-
return True
|
|
919
|
+
elif now >= self._account_sync_deadline:
|
|
920
|
+
self._boot.account_sync_alert()
|
|
945
921
|
return False
|
|
946
922
|
|
|
947
923
|
def __update_base_data(
|
|
@@ -1087,21 +1063,98 @@ class ProcessingManager(IProcessingManager):
|
|
|
1087
1063
|
"""It is used by simulation as a dummy to trigger actual time events."""
|
|
1088
1064
|
pass
|
|
1089
1065
|
|
|
1090
|
-
def
|
|
1091
|
-
|
|
1092
|
-
return
|
|
1093
|
-
self._strategy.on_start(self._context)
|
|
1094
|
-
self._context._strategy_state.is_on_start_called = True
|
|
1066
|
+
def _advance_boot(self) -> bool:
|
|
1067
|
+
"""Drive the boot machine one pipeline pass. True ⇔ the strategy may trade.
|
|
1095
1068
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1069
|
+
Linearizes the old flag checks: WAIT_READY → ON_START → RESOLVE → RESTORE →
|
|
1070
|
+
WARMUP_FINISHED → BOOT_FIT → TRADING. RESOLVE/RESTORE/WARMUP_FINISHED are
|
|
1071
|
+
live-boot steps: skipped in simulation (RESOLVE) and while the warmup sim is
|
|
1072
|
+
running this pipeline (is_warmup_in_progress). The boot-fit pass returns False
|
|
1073
|
+
so no event is processed on it.
|
|
1074
|
+
"""
|
|
1075
|
+
boot = self._boot
|
|
1076
|
+
if boot.is_trading:
|
|
1077
|
+
return True
|
|
1078
|
+
if boot.is_blocked:
|
|
1079
|
+
return False
|
|
1080
|
+
state = self._context._strategy_state
|
|
1081
|
+
|
|
1082
|
+
if boot.phase == BootPhase.WAIT_READY:
|
|
1083
|
+
if not self._is_ready():
|
|
1084
|
+
return False
|
|
1085
|
+
boot.advance(BootPhase.ON_START)
|
|
1086
|
+
|
|
1087
|
+
if boot.phase == BootPhase.ON_START:
|
|
1088
|
+
if not state.is_on_start_called:
|
|
1089
|
+
self._strategy.on_start(self._context)
|
|
1090
|
+
state.is_on_start_called = True
|
|
1091
|
+
boot.advance(BootPhase.RESOLVE)
|
|
1092
|
+
|
|
1093
|
+
if boot.phase == BootPhase.RESOLVE:
|
|
1094
|
+
if not self._is_simulation and not state.is_on_warmup_finished_called:
|
|
1095
|
+
self._handle_state_resolution()
|
|
1096
|
+
boot.advance(BootPhase.RESTORE)
|
|
1097
|
+
|
|
1098
|
+
if boot.phase == BootPhase.RESTORE:
|
|
1099
|
+
if not state.is_warmup_in_progress and not state.is_on_warmup_finished_called:
|
|
1100
|
+
restored_state = self._context.get_restored_state()
|
|
1101
|
+
if restored_state is not None:
|
|
1102
|
+
self._restore_tracker_and_gatherer_state(restored_state)
|
|
1103
|
+
boot.advance(BootPhase.WARMUP_FINISHED)
|
|
1104
|
+
|
|
1105
|
+
if boot.phase == BootPhase.WARMUP_FINISHED:
|
|
1106
|
+
if state.is_warmup_in_progress:
|
|
1107
|
+
boot.advance(BootPhase.BOOT_FIT) # warmup-sim ctx: the hook belongs to the live boot
|
|
1108
|
+
else:
|
|
1109
|
+
if not state.is_on_warmup_finished_called and not self._warmup_finished_is_running:
|
|
1110
|
+
self._handle_warmup_finished()
|
|
1111
|
+
if not state.is_on_warmup_finished_called:
|
|
1112
|
+
return False
|
|
1113
|
+
if not self._is_simulation and state.is_on_fit_called and self._context.initializer.get_fit_on_start():
|
|
1114
|
+
state.is_on_fit_called = False # warmup fit doesn't count: force one live fit
|
|
1115
|
+
boot.advance(BootPhase.BOOT_FIT)
|
|
1116
|
+
|
|
1117
|
+
if boot.phase == BootPhase.BOOT_FIT:
|
|
1118
|
+
if state.is_on_fit_called:
|
|
1119
|
+
boot.record_fit_success()
|
|
1120
|
+
return True
|
|
1121
|
+
if self._fit_is_running or self._warmup_finished_is_running:
|
|
1122
|
+
return False
|
|
1123
|
+
if boot.fit_attempt_allowed(self._time_provider.time()):
|
|
1124
|
+
# - _handle_fit no-ops when readiness lapsed (account desync): such a pass
|
|
1125
|
+
# must not consume an attempt, or three of them would block an unrun fit
|
|
1126
|
+
if not self._is_ready():
|
|
1127
|
+
return False
|
|
1128
|
+
boot.record_fit_attempt()
|
|
1129
|
+
try:
|
|
1130
|
+
self._handle_fit(None, "fit", (None, self._time_provider.time()))
|
|
1131
|
+
except Exception:
|
|
1132
|
+
# - framework raise (cache finalize, health monitor, executor submit):
|
|
1133
|
+
# stays loud, but the attempt must arm the retry deadline like any
|
|
1134
|
+
# other failure instead of hot-looping on an unaccounted budget
|
|
1135
|
+
self._report_boot_fit(False)
|
|
1136
|
+
raise
|
|
1137
|
+
return False
|
|
1138
|
+
|
|
1139
|
+
def _report_boot_fit(self, ok: bool) -> None:
|
|
1140
|
+
"""Hand a fit outcome to the boot machine — only while it still owns the fit
|
|
1141
|
+
(BOOT_FIT) or waits for a self-heal (BLOCKED). A fit triggered mid-boot (handlers
|
|
1142
|
+
run ahead of the pipeline) would otherwise jump the machine straight to TRADING.
|
|
1143
|
+
Failures are live-only: simulation latches instead of retrying.
|
|
1144
|
+
"""
|
|
1145
|
+
boot = self._boot
|
|
1146
|
+
if boot.phase != BootPhase.BOOT_FIT and not boot.is_blocked:
|
|
1098
1147
|
return
|
|
1148
|
+
if ok:
|
|
1149
|
+
boot.record_fit_success()
|
|
1150
|
+
elif not self._is_simulation:
|
|
1151
|
+
boot.record_fit_failure(self._time_provider.time())
|
|
1099
1152
|
|
|
1153
|
+
def _handle_state_resolution(self) -> None:
|
|
1100
1154
|
_ctx = self._context
|
|
1101
1155
|
resolver = _ctx.initializer.get_state_resolver()
|
|
1102
1156
|
if resolver is None:
|
|
1103
|
-
|
|
1104
|
-
return
|
|
1157
|
+
resolver = StateResolver.REDUCE_ONLY
|
|
1105
1158
|
|
|
1106
1159
|
self._log_state_mismatch()
|
|
1107
1160
|
|
|
@@ -1122,9 +1175,6 @@ class ProcessingManager(IProcessingManager):
|
|
|
1122
1175
|
Args:
|
|
1123
1176
|
restored_state: The restored state containing signals and target positions
|
|
1124
1177
|
"""
|
|
1125
|
-
if not self._is_ready():
|
|
1126
|
-
return
|
|
1127
|
-
|
|
1128
1178
|
# Restore tracker state from signals.
|
|
1129
1179
|
# NB: unlike targets below, signals are NOT re-persisted on restart, so a run that
|
|
1130
1180
|
# hasn't re-emitted yet restores no signals (state restore is run-scoped). Harmless
|
|
@@ -1159,15 +1209,14 @@ class ProcessingManager(IProcessingManager):
|
|
|
1159
1209
|
self._logging.save_targets(latest_targets)
|
|
1160
1210
|
|
|
1161
1211
|
def _handle_warmup_finished(self) -> None:
|
|
1162
|
-
if not self._is_ready():
|
|
1163
|
-
return
|
|
1164
1212
|
# The flag reset lives in an outer finally so no raise (health monitor included)
|
|
1165
1213
|
# can strand it True — a stranded flag silently disables the strategy forever.
|
|
1166
1214
|
self._warmup_finished_is_running = True
|
|
1167
1215
|
try:
|
|
1168
1216
|
# Runs inline on the processing thread (single-mutator invariant): account events
|
|
1169
1217
|
# queue in the channel until the callback returns.
|
|
1170
|
-
self.__invoke_on_warmup_finished()
|
|
1218
|
+
if not self.__invoke_on_warmup_finished():
|
|
1219
|
+
self._boot.record_warmup_finished_failure()
|
|
1171
1220
|
finally:
|
|
1172
1221
|
self._warmup_finished_is_running = False
|
|
1173
1222
|
|
|
@@ -1191,9 +1240,10 @@ class ProcessingManager(IProcessingManager):
|
|
|
1191
1240
|
self._cache.finalize_ohlc_for_instruments(current_time, self._context.instruments)
|
|
1192
1241
|
# Runs inline on the processing thread (single-mutator invariant): a long fit
|
|
1193
1242
|
# blocks event processing until it returns.
|
|
1194
|
-
self.__invoke_on_fit()
|
|
1243
|
+
ok = self.__invoke_on_fit()
|
|
1195
1244
|
finally:
|
|
1196
1245
|
self._fit_is_running = False
|
|
1246
|
+
self._report_boot_fit(ok)
|
|
1197
1247
|
|
|
1198
1248
|
def _submit_threaded_fit(self, data: tuple[dt_64 | None, dt_64]) -> None:
|
|
1199
1249
|
"""Thread-mode fit (live only): submit the fit body to the single-thread
|
|
@@ -1236,6 +1286,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
1236
1286
|
_t_start = time.monotonic()
|
|
1237
1287
|
_deadline_timer: threading.Timer | None = None
|
|
1238
1288
|
fit_ctx: FitContext | None = None
|
|
1289
|
+
_fit_error: BaseException | None = None
|
|
1239
1290
|
try:
|
|
1240
1291
|
# Everything from here — including proxy construction and the timer start
|
|
1241
1292
|
# (Timer.start can raise "can't start new thread" under resource pressure) —
|
|
@@ -1257,10 +1308,16 @@ class ProcessingManager(IProcessingManager):
|
|
|
1257
1308
|
self._strategy.on_fit(fit_ctx) # type: ignore[arg-type]
|
|
1258
1309
|
logger.debug(f"[<y>{self.__class__.__name__}</y>] :: <g>{self._strategy_name}</g> is fitted")
|
|
1259
1310
|
except Exception as strat_error:
|
|
1311
|
+
_fit_error = strat_error
|
|
1260
1312
|
logger.error(
|
|
1261
1313
|
f"[{self.__class__.__name__}] :: Strategy {self._strategy_name} on_fit raised an exception: {strat_error}"
|
|
1262
1314
|
)
|
|
1263
1315
|
logger.opt(colors=False).error(traceback.format_exc())
|
|
1316
|
+
except BaseException as framework_error:
|
|
1317
|
+
# - the body never ran (proxy/timer/health-monitor raise): report it as a
|
|
1318
|
+
# failed fit too, so nothing latches as fitted
|
|
1319
|
+
_fit_error = framework_error
|
|
1320
|
+
raise
|
|
1264
1321
|
finally:
|
|
1265
1322
|
if _deadline_timer is not None:
|
|
1266
1323
|
_deadline_timer.cancel()
|
|
@@ -1277,7 +1334,14 @@ class ProcessingManager(IProcessingManager):
|
|
|
1277
1334
|
except Exception:
|
|
1278
1335
|
logger.exception("failed to record fit_duration_s gauge")
|
|
1279
1336
|
_ops, _signals = self._fit_state.end()
|
|
1280
|
-
channel.send(
|
|
1337
|
+
channel.send(
|
|
1338
|
+
(
|
|
1339
|
+
None,
|
|
1340
|
+
FIT_COMMIT_EVENT,
|
|
1341
|
+
FitCommitData(ops=_ops, signals=_signals, duration_s=_duration_s, error=_fit_error),
|
|
1342
|
+
False,
|
|
1343
|
+
)
|
|
1344
|
+
)
|
|
1281
1345
|
|
|
1282
1346
|
def _warn_fit_soft_deadline(self) -> None:
|
|
1283
1347
|
logger.warning(
|
|
@@ -1295,8 +1359,9 @@ class ProcessingManager(IProcessingManager):
|
|
|
1295
1359
|
by the subscription commit to its background worker, so the replay stays fast and
|
|
1296
1360
|
the added instruments go live only after their history landed), commit any
|
|
1297
1361
|
remaining deferred subscriptions, then — in a finally so no replay failure can
|
|
1298
|
-
strand the flag — drain fit-emitted signals into the normal pipeline,
|
|
1299
|
-
is_on_fit_called
|
|
1362
|
+
strand the flag — drain fit-emitted signals into the normal pipeline, latch
|
|
1363
|
+
is_on_fit_called (only when the fit itself did not fail) and clear
|
|
1364
|
+
_fit_is_running. Returning None hands control to
|
|
1300
1365
|
_run_strategy_pipeline, which processes the drained signals immediately.
|
|
1301
1366
|
"""
|
|
1302
1367
|
try:
|
|
@@ -1314,8 +1379,10 @@ class ProcessingManager(IProcessingManager):
|
|
|
1314
1379
|
finally:
|
|
1315
1380
|
if commit.signals:
|
|
1316
1381
|
self._emitted_signals.extend(commit.signals)
|
|
1317
|
-
|
|
1382
|
+
if commit.error is None:
|
|
1383
|
+
self._context._strategy_state.is_on_fit_called = True
|
|
1318
1384
|
self._fit_is_running = False
|
|
1385
|
+
self._report_boot_fit(commit.error is None)
|
|
1319
1386
|
|
|
1320
1387
|
def _handle_subscription_swap(self, instrument: Instrument | None, event_type: str, apply_swap: Callable) -> None:
|
|
1321
1388
|
"""Apply a deferred subscription commit's swap on the ProcessorThread. Posted by
|