Qubx 0.7.8__tar.gz → 0.7.25__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-0.7.8 → qubx-0.7.25}/PKG-INFO +2 -1
- {qubx-0.7.8 → qubx-0.7.25}/pyproject.toml +2 -1
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/account.py +8 -1
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/broker.py +23 -32
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/data.py +33 -7
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/management.py +1 -1
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/ome.py +5 -2
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/runner.py +23 -11
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/simulated_exchange.py +23 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/transfers.py +13 -8
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/utils.py +9 -2
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/cli/commands.py +52 -25
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/account.py +22 -10
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/broker.py +51 -34
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/data.py +17 -33
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchange_manager.py +33 -79
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +121 -108
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +1 -1
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/factory.py +6 -2
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -4
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/liquidation.py +7 -6
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -12
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -6
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/orderbook.py +1 -3
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/quote.py +1 -5
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/trade.py +0 -4
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/reader.py +361 -165
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/utils.py +30 -41
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/tardis/data.py +9 -3
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/__init__.py +3 -4
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/account.py +179 -98
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/broker.py +201 -93
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/client.py +13 -27
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/constants.py +1 -1
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/data.py +261 -326
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/factory.py +12 -89
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/handlers/__init__.py +0 -2
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/handlers/orderbook.py +67 -16
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/handlers/stats.py +6 -5
- qubx-0.7.25/src/qubx/connectors/xlighter/instruments.py +60 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/parsers.py +8 -2
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/rate_limits.py +2 -2
- qubx-0.7.25/src/qubx/connectors/xlighter/reader.py +453 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/utils.py +22 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/websocket.py +2 -2
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/account.py +220 -39
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/basics.py +116 -12
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/context.py +39 -39
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/detectors/delisting.py +16 -5
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/helpers.py +67 -4
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/initializer.py +2 -1
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/interfaces.py +236 -124
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/loggers.py +14 -10
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/metrics.py +42 -18
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/processing.py +107 -51
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/subscription.py +124 -37
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/trading.py +69 -17
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/universe.py +18 -22
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/series.pyx +27 -6
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/utils.pyx +21 -2
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/composite.py +45 -15
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/helpers.py +9 -5
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/readers.py +9 -6
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/storages/questdb.py +18 -2
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/transformers.py +4 -36
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/base.py +8 -1
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/indicator.py +2 -10
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/questdb.py +42 -13
- qubx-0.7.25/src/qubx/health/__init__.py +4 -0
- qubx-0.7.25/src/qubx/health/base.py +543 -0
- qubx-0.7.25/src/qubx/health/dummy.py +99 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/balance.py +36 -38
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/interfaces.py +2 -2
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/position.py +25 -24
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/ta/indicators.pxd +44 -1
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/ta/indicators.pyi +11 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/ta/indicators.pyx +335 -25
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/misc.py +15 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/rate_limiter.py +3 -6
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/_jupyter_runner.pyt +1 -1
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/configs.py +20 -3
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/runner.py +146 -19
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/app.py +13 -1
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/init_code.py +2 -2
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/repl_output.py +9 -0
- qubx-0.7.25/src/qubx/utils/throttler.py +136 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/time.py +10 -2
- qubx-0.7.8/src/qubx/connectors/xlighter/handlers/quote.py +0 -158
- qubx-0.7.8/src/qubx/connectors/xlighter/instruments.py +0 -253
- qubx-0.7.8/src/qubx/connectors/xlighter/reader.py +0 -490
- qubx-0.7.8/src/qubx/core/deque.py +0 -182
- qubx-0.7.8/src/qubx/features/__init__.py +0 -14
- qubx-0.7.8/src/qubx/features/core.py +0 -254
- qubx-0.7.8/src/qubx/features/orderbook.py +0 -42
- qubx-0.7.8/src/qubx/features/price.py +0 -20
- qubx-0.7.8/src/qubx/features/trades.py +0 -105
- qubx-0.7.8/src/qubx/features/utils.py +0 -10
- qubx-0.7.8/src/qubx/health/__init__.py +0 -3
- qubx-0.7.8/src/qubx/health/base.py +0 -672
- {qubx-0.7.8 → qubx-0.7.25}/LICENSE +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/README.md +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/build.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/sentinels.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/cli/misc.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/cli/release.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/cli/tui.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/extensions.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/handlers/base.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/handlers/trades.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/connectors/xlighter/nonce.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/detectors/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/detectors/stale.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/errors.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/lookups.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/mixins/utils.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/series.pxd +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/series.pyi +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/containers.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/hft.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/registry.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/storage.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/storages/csv.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/storages/utils.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/data/tardis.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/composite.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/inmemory.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/formatters/target_position.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/math/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/math/stats.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/_build.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/crypto-fees.ini +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/state.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/base.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/accounts.toml.j2 +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/config.yml.j2 +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/jlive.sh.j2 +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/project/template.yml +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/__init__.py.j2 +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/config.yml.j2 +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/strategy.py.j2 +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/templates/simple/template.yml +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/charting/orderbook.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/collections.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/hft/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/hft/numba_utils.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/hft/orderbook.pyi +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/hft/orderbook.pyx +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/nonce.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/questdb.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/ringbuffer.pxd +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/ringbuffer.pyi +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/ringbuffer.pyx +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/kernel_service.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/handlers.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/kernel.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/styles.tcss +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/slack.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/version.py +0 -0
- {qubx-0.7.8 → qubx-0.7.25}/src/qubx/utils/websocket_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Qubx
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.25
|
|
4
4
|
Summary: Qubx - Quantitative Trading Framework
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Dmitry Marienko
|
|
@@ -58,6 +58,7 @@ Requires-Dist: textual-serve (>=1.0.0,<2.0.0)
|
|
|
58
58
|
Requires-Dist: textual[syntax] (>=6.0.0,<7.0.0)
|
|
59
59
|
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
60
60
|
Requires-Dist: tqdm
|
|
61
|
+
Requires-Dist: uvloop (>=0.22.1,<0.23.0)
|
|
61
62
|
Requires-Dist: websockets (==15.0.1)
|
|
62
63
|
Project-URL: Repository, https://github.com/xLydianSoftware/Qubx
|
|
63
64
|
Description-Content-Type: text/markdown
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "Qubx"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.25"
|
|
8
8
|
description = "Qubx - Quantitative Trading Framework"
|
|
9
9
|
authors = [ "Dmitry Marienko <dmitry.marienko@xlydian.com>", "Yuriy Arabskyy <yuriy.arabskyy@xlydian.com>",]
|
|
10
10
|
readme = "README.md"
|
|
@@ -79,6 +79,7 @@ textual-serve = "^1.0.0"
|
|
|
79
79
|
rich = "^13.9.4"
|
|
80
80
|
jinja2 = "^3.1.0"
|
|
81
81
|
qubx-lighter-api = "^0.1.4"
|
|
82
|
+
uvloop = "^0.22.1"
|
|
82
83
|
|
|
83
84
|
[tool.ruff.lint]
|
|
84
85
|
extend-select = [ "I",]
|
|
@@ -8,6 +8,7 @@ from qubx.core.basics import (
|
|
|
8
8
|
Timestamped,
|
|
9
9
|
dt_64,
|
|
10
10
|
)
|
|
11
|
+
from qubx.core.interfaces import IHealthMonitor
|
|
11
12
|
from qubx.core.series import OrderBook, Quote, Trade, TradeArray
|
|
12
13
|
from qubx.restorers import RestoredState
|
|
13
14
|
|
|
@@ -21,7 +22,9 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
21
22
|
account_id: str,
|
|
22
23
|
exchange: ISimulatedExchange,
|
|
23
24
|
channel: CtrlChannel,
|
|
25
|
+
health_monitor: IHealthMonitor,
|
|
24
26
|
base_currency: str,
|
|
27
|
+
exchange_name: str,
|
|
25
28
|
initial_capital: float,
|
|
26
29
|
restored_state: RestoredState | None = None,
|
|
27
30
|
) -> None:
|
|
@@ -29,6 +32,8 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
29
32
|
account_id=account_id,
|
|
30
33
|
time_provider=exchange.get_time_provider(),
|
|
31
34
|
base_currency=base_currency,
|
|
35
|
+
health_monitor=health_monitor,
|
|
36
|
+
exchange=exchange_name,
|
|
32
37
|
tcc=exchange.get_transaction_costs_calculator(),
|
|
33
38
|
initial_capital=initial_capital,
|
|
34
39
|
)
|
|
@@ -37,7 +42,9 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
37
42
|
self._channel = channel
|
|
38
43
|
|
|
39
44
|
if restored_state is not None:
|
|
40
|
-
|
|
45
|
+
# Convert list of AssetBalance to dict for internal storage
|
|
46
|
+
for balance in restored_state.balances:
|
|
47
|
+
self._balances[balance.currency] = balance
|
|
41
48
|
for instrument, position in restored_state.positions.items():
|
|
42
49
|
_pos = self.get_position(instrument)
|
|
43
50
|
_pos.reset_by_position(position)
|
|
@@ -5,6 +5,7 @@ from qubx.core.basics import (
|
|
|
5
5
|
CtrlChannel,
|
|
6
6
|
Instrument,
|
|
7
7
|
Order,
|
|
8
|
+
OrderRequest,
|
|
8
9
|
)
|
|
9
10
|
from qubx.core.exceptions import BadRequest, OrderNotFound
|
|
10
11
|
from qubx.core.interfaces import IBroker
|
|
@@ -32,18 +33,17 @@ class SimulatedBroker(IBroker):
|
|
|
32
33
|
def is_simulated_trading(self) -> bool:
|
|
33
34
|
return True
|
|
34
35
|
|
|
35
|
-
def send_order(
|
|
36
|
-
|
|
37
|
-
instrument
|
|
38
|
-
order_side
|
|
39
|
-
order_type
|
|
40
|
-
amount
|
|
41
|
-
price
|
|
42
|
-
client_id
|
|
43
|
-
time_in_force
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# - place order at exchange and send exec report to data channel
|
|
36
|
+
def send_order(self, request: OrderRequest) -> Order:
|
|
37
|
+
"""Submit order synchronously in simulation."""
|
|
38
|
+
instrument = request.instrument
|
|
39
|
+
order_side = request.side
|
|
40
|
+
order_type = request.order_type
|
|
41
|
+
amount = request.quantity
|
|
42
|
+
price = request.price
|
|
43
|
+
client_id = request.client_id
|
|
44
|
+
time_in_force = request.time_in_force
|
|
45
|
+
options = request.options
|
|
46
|
+
|
|
47
47
|
self._send_execution_report(
|
|
48
48
|
report := self._exchange.place_order(
|
|
49
49
|
instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
|
|
@@ -51,18 +51,9 @@ class SimulatedBroker(IBroker):
|
|
|
51
51
|
)
|
|
52
52
|
return report.order
|
|
53
53
|
|
|
54
|
-
def send_order_async(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
order_side: str,
|
|
58
|
-
order_type: str,
|
|
59
|
-
amount: float,
|
|
60
|
-
price: float | None = None,
|
|
61
|
-
client_id: str | None = None,
|
|
62
|
-
time_in_force: str = "gtc",
|
|
63
|
-
**optional,
|
|
64
|
-
) -> None:
|
|
65
|
-
self.send_order(instrument, order_side, order_type, amount, price, client_id, time_in_force, **optional)
|
|
54
|
+
def send_order_async(self, request: OrderRequest) -> None:
|
|
55
|
+
"""Submit order asynchronously (same as sync in simulation)."""
|
|
56
|
+
self.send_order(request)
|
|
66
57
|
|
|
67
58
|
def cancel_order(self, order_id: str) -> bool:
|
|
68
59
|
"""Cancel an order synchronously and return success status."""
|
|
@@ -104,25 +95,25 @@ class SimulatedBroker(IBroker):
|
|
|
104
95
|
raise OrderNotFound(f"Order {order_id} not found")
|
|
105
96
|
|
|
106
97
|
# Validate that it's a limit order
|
|
107
|
-
if existing_order.type
|
|
98
|
+
if existing_order.type != "LIMIT":
|
|
108
99
|
raise BadRequest(
|
|
109
100
|
f"Order {order_id} is not a limit order (type: {existing_order.type}). Only limit orders can be updated."
|
|
110
101
|
)
|
|
111
102
|
|
|
112
|
-
# Cancel the existing order first
|
|
113
103
|
self.cancel_order(order_id)
|
|
114
104
|
|
|
115
|
-
|
|
116
|
-
updated_order = self.send_order(
|
|
105
|
+
request = OrderRequest(
|
|
117
106
|
instrument=existing_order.instrument,
|
|
118
|
-
|
|
119
|
-
order_type="limit",
|
|
120
|
-
amount=abs(amount),
|
|
107
|
+
quantity=abs(amount),
|
|
121
108
|
price=price,
|
|
122
|
-
|
|
109
|
+
order_type="LIMIT",
|
|
110
|
+
side=existing_order.side,
|
|
123
111
|
time_in_force=existing_order.time_in_force or "gtc",
|
|
112
|
+
options={},
|
|
124
113
|
)
|
|
125
114
|
|
|
115
|
+
updated_order = self.send_order(request)
|
|
116
|
+
|
|
126
117
|
return updated_order
|
|
127
118
|
|
|
128
119
|
def _send_execution_report(self, report: SimulatedExecutionReport | None):
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import TypeVar
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
|
|
@@ -74,16 +74,35 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
74
74
|
def is_simulation(self) -> bool:
|
|
75
75
|
return True
|
|
76
76
|
|
|
77
|
+
def is_connected(self) -> bool:
|
|
78
|
+
"""
|
|
79
|
+
Check if the data provider is currently connected to the exchange.
|
|
80
|
+
|
|
81
|
+
For simulated data provider, always returns True since data is loaded from files.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
bool: Always True for simulated data
|
|
85
|
+
"""
|
|
86
|
+
return True
|
|
87
|
+
|
|
77
88
|
def subscribe(self, subscription_type: str, instruments: set[Instrument], reset: bool) -> None:
|
|
78
89
|
_new_instr = [i for i in instruments if not self.has_subscription(i, subscription_type)]
|
|
90
|
+
|
|
79
91
|
self._data_source.add_instruments_for_subscription(subscription_type, list(instruments))
|
|
80
92
|
|
|
81
93
|
# - provide historical data and last quote for subscribed instruments
|
|
82
94
|
for i in _new_instr:
|
|
83
|
-
#
|
|
95
|
+
# - check if the instrument was actually subscribed (not filtered out)
|
|
84
96
|
if not self.has_subscription(i, subscription_type):
|
|
85
97
|
continue
|
|
86
98
|
|
|
99
|
+
# - notify simulating exchange that instrument is subscribed
|
|
100
|
+
self._account._exchange.on_subscribe(i)
|
|
101
|
+
|
|
102
|
+
# - we need to clear last quote as it can be staled
|
|
103
|
+
self._last_quotes.pop(i, None)
|
|
104
|
+
|
|
105
|
+
# - try to peek most recent market data
|
|
87
106
|
h_data = self._data_source.peek_historical_data(i, subscription_type)
|
|
88
107
|
if h_data:
|
|
89
108
|
# _s_type = DataType.from_str(subscription_type)[0]
|
|
@@ -103,9 +122,16 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
103
122
|
def unsubscribe(self, subscription_type: str, instruments: set[Instrument] | Instrument | None = None) -> None:
|
|
104
123
|
# logger.debug(f" | unsubscribe: {subscription_type} -> {instruments}")
|
|
105
124
|
if instruments is not None:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
_instruments = [instruments] if isinstance(instruments, Instrument) else list(instruments)
|
|
126
|
+
self._data_source.remove_instruments_from_subscription(subscription_type, _instruments)
|
|
127
|
+
|
|
128
|
+
# - Clear last quotes for unsubscribed instruments
|
|
129
|
+
for instr in _instruments:
|
|
130
|
+
# - clear last quote
|
|
131
|
+
self._last_quotes.pop(instr, None)
|
|
132
|
+
|
|
133
|
+
# - Notify simulating exchange that instrument is unsubscribed
|
|
134
|
+
self._account._exchange.on_unsubscribe(instr)
|
|
109
135
|
|
|
110
136
|
def has_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
|
|
111
137
|
return self._data_source.has_subscription(instrument, subscription_type)
|
|
@@ -174,11 +200,11 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
174
200
|
high_price = r.data["high"]
|
|
175
201
|
low_price = r.data["low"]
|
|
176
202
|
close_price = r.data["close"]
|
|
177
|
-
|
|
203
|
+
|
|
178
204
|
# Skip this record if any OHLC value is None
|
|
179
205
|
if open_price is None or high_price is None or low_price is None or close_price is None:
|
|
180
206
|
continue
|
|
181
|
-
|
|
207
|
+
|
|
182
208
|
bars.append(
|
|
183
209
|
Bar(
|
|
184
210
|
_b_ts_0,
|
|
@@ -382,7 +382,7 @@ class BacktestsResultsManager:
|
|
|
382
382
|
_mtrx[_nm] = v.get("performance", {})
|
|
383
383
|
|
|
384
384
|
_m_repr = pd.DataFrame.from_dict(_mtrx, orient="index")[
|
|
385
|
-
["gain", "cagr", "sharpe", "qr", "
|
|
385
|
+
["gain", "cagr", "sharpe", "qr", "mdd_pct", "mdd_usd", "fees", "execs"]
|
|
386
386
|
].astype(float)
|
|
387
387
|
_m_repr = _m_repr.round(3)
|
|
388
388
|
_m_repr = _m_repr.sort_values(by=sort_by, ascending=ascending) if sort_by else _m_repr
|
|
@@ -79,8 +79,11 @@ class OrdersManagementEngine:
|
|
|
79
79
|
self.active_orders = dict()
|
|
80
80
|
self.stop_orders = dict()
|
|
81
81
|
self.bbo = None
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
# - 2025-12-09: OME always started with fixed order_id / trade_id
|
|
83
|
+
# executions stopped to be accounted in portfolio when OME re-created
|
|
84
|
+
_start_id = int(time_provider.time().view("i8")) + 1000
|
|
85
|
+
self.__order_id = _start_id
|
|
86
|
+
self.__trade_id = _start_id
|
|
84
87
|
self._fill_stops_at_price = fill_stop_order_at_price
|
|
85
88
|
self._tick_size = instrument.tick_size
|
|
86
89
|
self._last_update_time = np.datetime64(0, "ns")
|
|
@@ -16,6 +16,7 @@ from qubx.core.initializer import BasicStrategyInitializer
|
|
|
16
16
|
from qubx.core.interfaces import (
|
|
17
17
|
CtrlChannel,
|
|
18
18
|
IDataProvider,
|
|
19
|
+
IHealthMonitor,
|
|
19
20
|
IMetricEmitter,
|
|
20
21
|
IStrategy,
|
|
21
22
|
IStrategyContext,
|
|
@@ -26,6 +27,7 @@ from qubx.core.interfaces import (
|
|
|
26
27
|
from qubx.core.loggers import StrategyLogging
|
|
27
28
|
from qubx.core.lookups import lookup
|
|
28
29
|
from qubx.data.helpers import CachedPrefetchReader
|
|
30
|
+
from qubx.health import DummyHealthMonitor
|
|
29
31
|
from qubx.loggers.inmemory import InMemoryLogsWriter
|
|
30
32
|
from qubx.pandaz.utils import _frame_to_str
|
|
31
33
|
from qubx.utils.time import now_ns
|
|
@@ -140,18 +142,20 @@ class SimulationRunner:
|
|
|
140
142
|
|
|
141
143
|
self._prefetch_aux_data()
|
|
142
144
|
|
|
143
|
-
#
|
|
145
|
+
# - Apply warmup periods before the start
|
|
146
|
+
# - merge default warmups with strategy warmups (strategy warmups take precedence)
|
|
147
|
+
_merged_warmups = {
|
|
148
|
+
**(self.data_config.default_warmups or {}),
|
|
149
|
+
**self.ctx.initializer.get_subscription_warmup(),
|
|
150
|
+
}
|
|
151
|
+
if _merged_warmups:
|
|
152
|
+
logger.debug(f"[<y>SimulationRunner</y>] :: Setting warmups: {_merged_warmups}")
|
|
153
|
+
self.ctx.set_warmup(_merged_warmups)
|
|
154
|
+
|
|
155
|
+
# - Start the context
|
|
144
156
|
self.ctx.start()
|
|
145
157
|
|
|
146
|
-
#
|
|
147
|
-
for s in self.ctx.get_subscriptions():
|
|
148
|
-
if not self.ctx.get_warmup(s) and (_d_wt := self.data_config.default_warmups.get(s)):
|
|
149
|
-
logger.debug(
|
|
150
|
-
f"[<y>SimulationRunner</y>] :: Strategy didn't set warmup period for <c>{s}</c> so default <c>{_d_wt}</c> will be used"
|
|
151
|
-
)
|
|
152
|
-
self.ctx.set_warmup({s: _d_wt})
|
|
153
|
-
|
|
154
|
-
# Subscribe to any custom data types if needed
|
|
158
|
+
# - Subscribe to any custom data types if needed
|
|
155
159
|
def _is_known_type(t: str):
|
|
156
160
|
try:
|
|
157
161
|
DataType(t)
|
|
@@ -387,9 +391,14 @@ class SimulationRunner:
|
|
|
387
391
|
|
|
388
392
|
channel = SimulatedCtrlChannel("databus", sentinel=(None, None, None, None))
|
|
389
393
|
simulated_clock = SimulatedTimeProvider(np.datetime64(self.start, "ns"))
|
|
394
|
+
health_monitor = DummyHealthMonitor()
|
|
390
395
|
|
|
391
396
|
account = self._construct_account_processor(
|
|
392
|
-
self.setup.exchanges,
|
|
397
|
+
self.setup.exchanges,
|
|
398
|
+
self.setup.commissions,
|
|
399
|
+
simulated_clock,
|
|
400
|
+
channel,
|
|
401
|
+
health_monitor,
|
|
393
402
|
)
|
|
394
403
|
|
|
395
404
|
scheduler = SimulatedScheduler(channel, lambda: simulated_clock.time().item())
|
|
@@ -528,6 +537,7 @@ class SimulationRunner:
|
|
|
528
537
|
commissions: str | dict[str, str | None] | None,
|
|
529
538
|
time_provider: ITimeProvider,
|
|
530
539
|
channel: CtrlChannel,
|
|
540
|
+
health_monitor: IHealthMonitor,
|
|
531
541
|
) -> CompositeAccountProcessor:
|
|
532
542
|
_exchange_to_tcc = self._construct_tcc(exchanges, commissions)
|
|
533
543
|
for tcc in _exchange_to_tcc.values():
|
|
@@ -555,7 +565,9 @@ class SimulationRunner:
|
|
|
555
565
|
account_id=self.account_id,
|
|
556
566
|
exchange=_exchange_to_simulated_exchange[exchange],
|
|
557
567
|
channel=channel,
|
|
568
|
+
health_monitor=health_monitor,
|
|
558
569
|
base_currency=self.setup.base_currency,
|
|
570
|
+
exchange_name=exchange,
|
|
559
571
|
initial_capital=_initial_capital,
|
|
560
572
|
)
|
|
561
573
|
|
|
@@ -47,6 +47,10 @@ class ISimulatedExchange:
|
|
|
47
47
|
|
|
48
48
|
def get_open_orders(self, instrument: Instrument | None = None) -> dict[str, Order]: ...
|
|
49
49
|
|
|
50
|
+
def on_unsubscribe(self, instrument: Instrument) -> None: ...
|
|
51
|
+
|
|
52
|
+
def on_subscribe(self, instrument: Instrument) -> None: ...
|
|
53
|
+
|
|
50
54
|
def process_market_data(
|
|
51
55
|
self, instrument: Instrument, data: Quote | OrderBook | Trade | TradeArray
|
|
52
56
|
) -> Generator[SimulatedExecutionReport]: ...
|
|
@@ -93,6 +97,7 @@ class BasicSimulatedExchange(ISimulatedExchange):
|
|
|
93
97
|
"""
|
|
94
98
|
|
|
95
99
|
_ome: dict[Instrument, OrdersManagementEngine]
|
|
100
|
+
_half_tick_size: dict[Instrument, float]
|
|
96
101
|
_order_to_instrument: dict[str, Instrument]
|
|
97
102
|
_fill_stop_order_at_price: bool
|
|
98
103
|
_time_provider: ITimeProvider
|
|
@@ -112,6 +117,7 @@ class BasicSimulatedExchange(ISimulatedExchange):
|
|
|
112
117
|
self._fill_stop_order_at_price = accurate_stop_orders_execution
|
|
113
118
|
self._time_provider = time_provider
|
|
114
119
|
self._tcc = tcc
|
|
120
|
+
|
|
115
121
|
if self._fill_stop_order_at_price:
|
|
116
122
|
logger.info(
|
|
117
123
|
f"[<y>{self.__class__.__name__}</y>] :: emulation of stop orders executions at exact price is ON"
|
|
@@ -193,6 +199,23 @@ class BasicSimulatedExchange(ISimulatedExchange):
|
|
|
193
199
|
|
|
194
200
|
return {o.id: o for ome in self._ome.values() for o in ome.get_open_orders()}
|
|
195
201
|
|
|
202
|
+
def on_unsubscribe(self, instrument: Instrument) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Called when an instrument is unsubscribed.
|
|
205
|
+
"""
|
|
206
|
+
# - clears the OME to remove stale BBO data.
|
|
207
|
+
self._ome.pop(instrument, None)
|
|
208
|
+
self._half_tick_size.pop(instrument, None)
|
|
209
|
+
|
|
210
|
+
def on_subscribe(self, instrument: Instrument) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Called when new instrument is subscribed.
|
|
213
|
+
"""
|
|
214
|
+
# - just for sanity: remove OME for this instrument if it wasn't removed in on_unsubscribe call
|
|
215
|
+
if instrument in self._ome:
|
|
216
|
+
self._ome.pop(instrument, None)
|
|
217
|
+
self._half_tick_size.pop(instrument, None)
|
|
218
|
+
|
|
196
219
|
def _get_ome(self, instrument: Instrument) -> OrdersManagementEngine:
|
|
197
220
|
if (ome := self._ome.get(instrument)) is None:
|
|
198
221
|
self._half_tick_size[instrument] = instrument.tick_size / 2 # type: ignore
|
|
@@ -63,11 +63,13 @@ class SimulationTransferManager(ITransferManager):
|
|
|
63
63
|
raise ValueError(f"Exchange not found: {e}")
|
|
64
64
|
|
|
65
65
|
# Validate sufficient funds
|
|
66
|
-
|
|
67
|
-
if currency
|
|
66
|
+
from_balances_list = from_processor.get_balances()
|
|
67
|
+
from_balance = next((b for b in from_balances_list if b.currency == currency), None)
|
|
68
|
+
|
|
69
|
+
if from_balance is None:
|
|
68
70
|
raise ValueError(f"Currency '{currency}' not found in {from_exchange}")
|
|
69
71
|
|
|
70
|
-
available =
|
|
72
|
+
available = from_balance.free
|
|
71
73
|
if available < amount:
|
|
72
74
|
raise ValueError(
|
|
73
75
|
f"Insufficient funds in {from_exchange}: "
|
|
@@ -75,12 +77,15 @@ class SimulationTransferManager(ITransferManager):
|
|
|
75
77
|
)
|
|
76
78
|
|
|
77
79
|
# Execute transfer (instant balance manipulation)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
from_balance.total -= amount
|
|
81
|
+
from_balance.free -= amount
|
|
82
|
+
|
|
83
|
+
to_balances_list = to_processor.get_balances()
|
|
84
|
+
to_balance = next((b for b in to_balances_list if b.currency == currency), None)
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
total_amount = to_balance.total + amount if to_balance is not None else amount
|
|
87
|
+
locked_amount = to_balance.locked if to_balance is not None else 0
|
|
88
|
+
to_processor.update_balance(currency, total_amount, locked_amount, to_exchange)
|
|
84
89
|
|
|
85
90
|
# Record transfer
|
|
86
91
|
transfer_record = {
|
|
@@ -746,8 +746,15 @@ def _detect_defaults_from_subscriptions(
|
|
|
746
746
|
_out_tf_tdelta, open_close_time_indent_secs
|
|
747
747
|
)
|
|
748
748
|
|
|
749
|
-
# - default warmups
|
|
750
|
-
_warmups = {
|
|
749
|
+
# - default warmups - populate for all subscription types in readers
|
|
750
|
+
_warmups = {}
|
|
751
|
+
for _sub_type in _t_readers.keys():
|
|
752
|
+
_sub_tf = _tf(_sub_type) # - extract timeframe if exists (None for quote/trade/orderbook)
|
|
753
|
+
_warmups[str(_sub_type)] = time_delta_to_str(_get_default_warmup_period(str(_sub_type), _sub_tf).asm8.item())
|
|
754
|
+
|
|
755
|
+
# - ensure base subscription has warmup (in case it's not in readers)
|
|
756
|
+
if str(_base_subscr) not in _warmups:
|
|
757
|
+
_warmups[str(_base_subscr)] = time_delta_to_str(_get_default_warmup_period(_base_subscr, _in_base_tf).asm8.item())
|
|
751
758
|
|
|
752
759
|
return SimulationDataConfig(
|
|
753
760
|
_default_trigger_schedule,
|
|
@@ -34,8 +34,9 @@ def main(debug: bool, debug_port: int, log_level: str):
|
|
|
34
34
|
"""
|
|
35
35
|
# Suppress syntax warnings from AST parsing during import resolution
|
|
36
36
|
import warnings
|
|
37
|
+
|
|
37
38
|
warnings.filterwarnings("ignore", category=SyntaxWarning)
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
|
|
40
41
|
log_level = log_level.upper() if not debug else "DEBUG"
|
|
41
42
|
|
|
@@ -69,23 +70,29 @@ def main(debug: bool, debug_port: int, log_level: str):
|
|
|
69
70
|
@click.option(
|
|
70
71
|
"--jupyter", "-j", is_flag=True, default=False, help="Run strategy in jupyter console.", show_default=True
|
|
71
72
|
)
|
|
73
|
+
@click.option("--textual", "-t", is_flag=True, default=False, help="Run strategy in textual TUI.", show_default=True)
|
|
72
74
|
@click.option(
|
|
73
|
-
"--textual
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@click.option(
|
|
79
|
-
"--textual-web", is_flag=True, default=False, help="Serve Textual app in web browser.", show_default=True
|
|
80
|
-
)
|
|
81
|
-
@click.option(
|
|
82
|
-
"--textual-port", type=int, default=None, help="Port for Textual (web server: 8000, devtools: 8081).", show_default=False
|
|
75
|
+
"--textual-dev",
|
|
76
|
+
is_flag=True,
|
|
77
|
+
default=False,
|
|
78
|
+
help="Enable Textual dev mode (use with 'textual console').",
|
|
79
|
+
show_default=True,
|
|
83
80
|
)
|
|
81
|
+
@click.option("--textual-web", is_flag=True, default=False, help="Serve Textual app in web browser.", show_default=True)
|
|
84
82
|
@click.option(
|
|
85
|
-
"--textual-
|
|
83
|
+
"--textual-port",
|
|
84
|
+
type=int,
|
|
85
|
+
default=None,
|
|
86
|
+
help="Port for Textual (web server: 8000, devtools: 8081).",
|
|
87
|
+
show_default=False,
|
|
86
88
|
)
|
|
89
|
+
@click.option("--textual-host", type=str, default="0.0.0.0", help="Host for Textual web server.", show_default=True)
|
|
87
90
|
@click.option(
|
|
88
|
-
"--kernel-only",
|
|
91
|
+
"--kernel-only",
|
|
92
|
+
is_flag=True,
|
|
93
|
+
default=False,
|
|
94
|
+
help="Start kernel without UI (returns connection file).",
|
|
95
|
+
show_default=True,
|
|
89
96
|
)
|
|
90
97
|
@click.option(
|
|
91
98
|
"--connect", type=Path, default=None, help="Connect to existing kernel via connection file.", show_default=False
|
|
@@ -94,7 +101,21 @@ def main(debug: bool, debug_port: int, log_level: str):
|
|
|
94
101
|
"--restore", "-r", is_flag=True, default=False, help="Restore strategy state from previous run.", show_default=True
|
|
95
102
|
)
|
|
96
103
|
@click.option("--no-color", is_flag=True, default=False, help="Disable colored logging output.", show_default=True)
|
|
97
|
-
def run(
|
|
104
|
+
def run(
|
|
105
|
+
config_file: Path,
|
|
106
|
+
account_file: Path | None,
|
|
107
|
+
paper: bool,
|
|
108
|
+
jupyter: bool,
|
|
109
|
+
textual: bool,
|
|
110
|
+
textual_dev: bool,
|
|
111
|
+
textual_web: bool,
|
|
112
|
+
textual_port: int | None,
|
|
113
|
+
textual_host: str,
|
|
114
|
+
kernel_only: bool,
|
|
115
|
+
connect: Path | None,
|
|
116
|
+
restore: bool,
|
|
117
|
+
no_color: bool,
|
|
118
|
+
):
|
|
98
119
|
"""
|
|
99
120
|
Starts the strategy with the given configuration file. If paper mode is enabled, account is not required.
|
|
100
121
|
|
|
@@ -138,6 +159,7 @@ def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool
|
|
|
138
159
|
# Keep the process alive until interrupted
|
|
139
160
|
try:
|
|
140
161
|
import signal
|
|
162
|
+
|
|
141
163
|
signal.pause()
|
|
142
164
|
except KeyboardInterrupt:
|
|
143
165
|
click.echo("\nShutting down kernel...")
|
|
@@ -152,7 +174,9 @@ def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool
|
|
|
152
174
|
if jupyter:
|
|
153
175
|
run_strategy_yaml_in_jupyter(config_file, account_file, paper, restore)
|
|
154
176
|
elif textual:
|
|
155
|
-
run_strategy_yaml_in_textual(
|
|
177
|
+
run_strategy_yaml_in_textual(
|
|
178
|
+
config_file, account_file, paper, restore, textual_dev, textual_web, textual_port, textual_host, connect
|
|
179
|
+
)
|
|
156
180
|
else:
|
|
157
181
|
logo()
|
|
158
182
|
run_strategy_yaml(config_file, account_file, paper=paper, restore=restore, blocking=True, no_color=no_color)
|
|
@@ -169,7 +193,10 @@ def run(config_file: Path, account_file: Path | None, paper: bool, jupyter: bool
|
|
|
169
193
|
@click.option(
|
|
170
194
|
"--output", "-o", default="results", type=str, help="Output directory for simulation results.", show_default=True
|
|
171
195
|
)
|
|
172
|
-
|
|
196
|
+
@click.option(
|
|
197
|
+
"--report", "-r", default=None, type=str, help="Output directory for simulation reports.", show_default=True
|
|
198
|
+
)
|
|
199
|
+
def simulate(config_file: Path, start: str | None, end: str | None, output: str | None, report: str | None):
|
|
173
200
|
"""
|
|
174
201
|
Simulates the strategy with the given configuration file.
|
|
175
202
|
"""
|
|
@@ -179,7 +206,7 @@ def simulate(config_file: Path, start: str | None, end: str | None, output: str
|
|
|
179
206
|
add_project_to_system_path()
|
|
180
207
|
add_project_to_system_path(str(config_file.parent))
|
|
181
208
|
logo()
|
|
182
|
-
simulate_strategy(config_file, output, start, end)
|
|
209
|
+
simulate_strategy(config_file, output, start, end, report)
|
|
183
210
|
|
|
184
211
|
|
|
185
212
|
@main.command()
|
|
@@ -457,32 +484,32 @@ def init(
|
|
|
457
484
|
):
|
|
458
485
|
"""
|
|
459
486
|
Create a new strategy from a template.
|
|
460
|
-
|
|
487
|
+
|
|
461
488
|
This command generates a complete strategy project structure with:
|
|
462
489
|
- Strategy class implementing IStrategy interface
|
|
463
490
|
- Configuration file for qubx run command
|
|
464
491
|
- Package structure for proper imports
|
|
465
|
-
|
|
492
|
+
|
|
466
493
|
The generated strategy can be run immediately with:
|
|
467
494
|
poetry run qubx run --config config.yml --paper
|
|
468
495
|
"""
|
|
469
496
|
from qubx.templates import TemplateError, TemplateManager
|
|
470
|
-
|
|
497
|
+
|
|
471
498
|
try:
|
|
472
499
|
manager = TemplateManager()
|
|
473
|
-
|
|
500
|
+
|
|
474
501
|
if list_templates:
|
|
475
502
|
templates = manager.list_templates()
|
|
476
503
|
if not templates:
|
|
477
504
|
click.echo("No templates available.")
|
|
478
505
|
return
|
|
479
|
-
|
|
506
|
+
|
|
480
507
|
click.echo("Available templates:")
|
|
481
508
|
for template_name, metadata in templates.items():
|
|
482
509
|
description = metadata.get("description", "No description")
|
|
483
510
|
click.echo(f" {template_name:<15} - {description}")
|
|
484
511
|
return
|
|
485
|
-
|
|
512
|
+
|
|
486
513
|
# Generate strategy
|
|
487
514
|
strategy_path = manager.generate_strategy(
|
|
488
515
|
template_name=template if not template_path else None,
|
|
@@ -493,7 +520,7 @@ def init(
|
|
|
493
520
|
symbols=symbols,
|
|
494
521
|
timeframe=timeframe,
|
|
495
522
|
)
|
|
496
|
-
|
|
523
|
+
|
|
497
524
|
click.echo(f"✅ Strategy '{name}' created successfully!")
|
|
498
525
|
click.echo(f"📁 Location: {strategy_path}")
|
|
499
526
|
click.echo()
|
|
@@ -503,7 +530,7 @@ def init(
|
|
|
503
530
|
click.echo()
|
|
504
531
|
click.echo("To run in Jupyter mode:")
|
|
505
532
|
click.echo(" ./jpaper.sh")
|
|
506
|
-
|
|
533
|
+
|
|
507
534
|
except TemplateError as e:
|
|
508
535
|
click.echo(f"❌ Template error: {e}", err=True)
|
|
509
536
|
raise click.Abort()
|